You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by be...@apache.org on 2018/11/22 11:21:06 UTC

[ambari] branch trunk updated: [AMBARI-24938] quick link profiles can override link url (benyoka) (#2646)

This is an automated email from the ASF dual-hosted git repository.

benyoka pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 8079522  [AMBARI-24938] quick link profiles can override link url (benyoka) (#2646)
8079522 is described below

commit 8079522317ed9e1f37b913946b97f239ae965ab7
Author: benyoka <be...@users.noreply.github.com>
AuthorDate: Thu Nov 22 12:21:01 2018 +0100

    [AMBARI-24938] quick link profiles can override link url (benyoka) (#2646)
    
    * AMBARI-24938 quick link profiles can override link url (benyoka)
    
    * AMBARI-24938 new unit test in QuickLinkArtifactResourceProviderTest (benyoka)
    
    * AMBARI-24938 review findings + check style issue + unit test fix (benyoka)
---
 .../QuickLinkArtifactResourceProvider.java         |   5 +-
 .../state/quicklinksprofile/AcceptAllFilter.java   |   1 +
 .../server/state/quicklinksprofile/Component.java  |  13 +-
 .../DefaultQuickLinkVisibilityController.java      | 181 ++++++++-------------
 .../server/state/quicklinksprofile/Filter.java     |  11 +-
 ...ibilityController.java => FilterEvaluator.java} | 115 +------------
 .../quicklinksprofile/LinkAttributeFilter.java     |  11 +-
 .../state/quicklinksprofile/LinkNameFilter.java    |  49 +++++-
 .../QuickLinkVisibilityController.java             |   9 +
 .../state/quicklinksprofile/QuickLinksProfile.java |  14 +-
 .../QuickLinksProfileBuilder.java                  |  10 +-
 .../quicklinksprofile/QuickLinksProfileParser.java |  39 ++---
 .../server/state/quicklinksprofile/Service.java    |  15 +-
 .../ShowAllLinksVisibilityController.java          |   6 +
 .../StreamUtils.java}                              |  26 +--
 .../internal/ProvisionClusterRequestTest.java      |   6 +-
 .../QuickLinkArtifactResourceProviderTest.java     |  35 ++--
 .../quicklinksprofile/FilterEvaluatorTest.java     |  10 +-
 .../QuickLinkVisibilityControllerTest.java         |  83 +++++++++-
 .../QuickLinksProfileBuilderTest.java              |  28 +++-
 .../QuickLinksProfileParserTest.java               |  17 +-
 .../test/resources/example_quicklinks_profile.json |   2 +
 .../inconsistent_quicklinks_profile_4.json         |  10 ++
 23 files changed, 375 insertions(+), 321 deletions(-)

diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/QuickLinkArtifactResourceProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/QuickLinkArtifactResourceProvider.java
index 2d9a256..b381023 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/QuickLinkArtifactResourceProvider.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/QuickLinkArtifactResourceProvider.java
@@ -179,7 +179,7 @@ public class QuickLinkArtifactResourceProvider extends AbstractControllerResourc
           }
         }
 
-        setVisibility(serviceInfo.getName(), serviceQuickLinks);
+        setVisibilityAndOverrides(serviceInfo.getName(), serviceQuickLinks);
 
         List<Resource> serviceResources = new ArrayList<>();
         for (QuickLinksConfigurationInfo quickLinksConfigurationInfo : serviceQuickLinks) {
@@ -206,13 +206,14 @@ public class QuickLinkArtifactResourceProvider extends AbstractControllerResourc
    * @param serviceName the name of the service
    * @param serviceQuickLinks the links
    */
-  private void setVisibility(String serviceName, List<QuickLinksConfigurationInfo> serviceQuickLinks) {
+  private void setVisibilityAndOverrides(String serviceName, List<QuickLinksConfigurationInfo> serviceQuickLinks) {
     QuickLinkVisibilityController visibilityController = getManagementController().getQuicklinkVisibilityController();
 
     for(QuickLinksConfigurationInfo configurationInfo: serviceQuickLinks) {
       for (QuickLinks links: configurationInfo.getQuickLinksConfigurationMap().values()) {
         for(Link link: links.getQuickLinksConfiguration().getLinks()) {
           link.setVisible(visibilityController.isVisible(serviceName, link));
+          visibilityController.getUrlOverride(serviceName, link).ifPresent(link::setUrl);
         }
       }
     }
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/AcceptAllFilter.java b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/AcceptAllFilter.java
index 069ae3f..01500bf 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/AcceptAllFilter.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/AcceptAllFilter.java
@@ -48,4 +48,5 @@ public class AcceptAllFilter extends Filter {
   public String toString() {
     return getClass().getSimpleName() + " (visible=" + isVisible() + ")";
   }
+
 }
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/Component.java b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/Component.java
index 729e5d4..f07a05a 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/Component.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/Component.java
@@ -18,18 +18,19 @@
 
 package org.apache.ambari.server.state.quicklinksprofile;
 
-import java.util.List;
+import static java.util.Collections.emptyList;
 
-import org.codehaus.jackson.annotate.JsonIgnoreProperties;
-import org.codehaus.jackson.annotate.JsonProperty;
-import org.codehaus.jackson.map.annotate.JsonSerialize;
+import java.util.List;
 
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.base.Preconditions;
 
 /**
  * Class to represent component-level filter definitions
  */
-@JsonSerialize(include= JsonSerialize.Inclusion.NON_NULL)
+@JsonInclude(JsonInclude.Include.NON_NULL)
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class Component {
   @JsonProperty("name")
@@ -58,7 +59,7 @@ public class Component {
    * @return the quicklink filters for this component
    */
   public List<Filter> getFilters() {
-    return filters;
+    return null != filters ? filters : emptyList();
   }
 
   public void setFilters(List<Filter> filters) {
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/DefaultQuickLinkVisibilityController.java b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/DefaultQuickLinkVisibilityController.java
index d0b0442..b8f6f1b 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/DefaultQuickLinkVisibilityController.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/DefaultQuickLinkVisibilityController.java
@@ -18,45 +18,97 @@
 
 package org.apache.ambari.server.state.quicklinksprofile;
 
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toMap;
+
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import org.apache.ambari.server.state.quicklinks.Link;
+import org.apache.commons.lang3.tuple.Pair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-import com.google.common.base.Optional;
+import com.google.common.collect.Sets;
 
 /**
  * This class can evaluate whether a quicklink has to be shown or hidden based on the received {@link QuickLinksProfile}.
  */
 public class DefaultQuickLinkVisibilityController implements QuickLinkVisibilityController {
+  private static final Logger LOG = LoggerFactory.getLogger(DefaultQuickLinkVisibilityController.class);
+
   private final FilterEvaluator globalRules;
   private final Map<String, FilterEvaluator> serviceRules = new HashMap<>();
-  private final Map<ServiceComponent, FilterEvaluator> componentRules = new HashMap<>();
+  /**
+   * Map of (service name, component name) -> filter evaluator
+   */
+  private final Map<Pair<String, String>, FilterEvaluator> componentRules = new HashMap<>();
+  /**
+   * Map of (service name, link name) -> url
+   */
+  private final Map<Pair<String, String>, String> urlOverrides = new HashMap<>();
+
 
   public DefaultQuickLinkVisibilityController(QuickLinksProfile profile) throws QuickLinksProfileEvaluationException {
     int filterCount = size(profile.getFilters());
     globalRules = new FilterEvaluator(profile.getFilters());
-    for (Service service: nullToEmptyList(profile.getServices())) {
+    for (Service service: profile.getServices()) {
       filterCount += size(service.getFilters());
       serviceRules.put(service.getName(), new FilterEvaluator(service.getFilters()));
-      for (Component component: nullToEmptyList(service.getComponents())) {
+      for (Component component: service.getComponents()) {
         filterCount += size(component.getFilters());
-        componentRules.put(ServiceComponent.of(service.getName(), component.getName()),
+        componentRules.put(Pair.of(service.getName(), component.getName()),
             new FilterEvaluator(component.getFilters()));
       }
     }
     if (filterCount == 0) {
       throw new QuickLinksProfileEvaluationException("At least one filter must be defined.");
     }
+
+    // compute url overrides
+    String globalOverrides = LinkNameFilter.getLinkNameFilters(profile.getFilters().stream())
+      .filter(f -> f.getLinkUrl() != null)
+      .map(f -> f.getLinkName() + " -> " + f.getLinkUrl())
+      .collect(joining(", "));
+    if (!globalOverrides.isEmpty()) {
+      LOG.warn("Link url overrides only work on service and component levels. The following global overrides will be " +
+        "ignored: {}", globalOverrides);
+    }
+    for (Service service : profile.getServices()) {
+      urlOverrides.putAll(getUrlOverrides(service.getName(), service.getFilters()));
+
+      for (Component component : service.getComponents()) {
+        Map<Pair<String, String>, String> componentUrlOverrides = getUrlOverrides(service.getName(), component.getFilters());
+        Set<Pair<String, String>> duplicateOverrides = Sets.intersection(urlOverrides.keySet(), componentUrlOverrides.keySet());
+        if (!duplicateOverrides.isEmpty()) {
+          LOG.warn("Duplicate url overrides in quick links profile: {}", duplicateOverrides);
+        }
+        urlOverrides.putAll(componentUrlOverrides);
+      }
+    }
+  }
+
+  private Map<Pair<String, String>, String> getUrlOverrides(String serviceName, Collection<Filter> filters) {
+    return filters.stream()
+      .filter( f -> f instanceof LinkNameFilter && null != ((LinkNameFilter)f).getLinkUrl() )
+      .map( f -> {
+        LinkNameFilter lnf = (LinkNameFilter)f;
+        return Pair.of(Pair.of(serviceName, lnf.getLinkName()), lnf.getLinkUrl());
+      })
+      .collect( toMap(Pair::getKey, Pair::getValue) );
+  }
+
+  @Override
+  public Optional<String> getUrlOverride(@Nonnull String service, @Nonnull Link quickLink) {
+    return Optional.ofNullable( urlOverrides.get(Pair.of(service, quickLink.getName())) );
   }
 
   /**
@@ -78,7 +130,7 @@ public class DefaultQuickLinkVisibilityController implements QuickLinkVisibility
     }
 
     // Global rules are evaluated lastly. If no rules apply to the link, it will be hidden.
-    return globalRules.isVisible(quickLink).or(false);
+    return globalRules.isVisible(quickLink).orElse(false);
   }
 
   private int size(@Nullable Collection<?> collection) {
@@ -87,127 +139,22 @@ public class DefaultQuickLinkVisibilityController implements QuickLinkVisibility
 
   private Optional<Boolean> evaluateComponentRules(@Nonnull String service, @Nonnull Link quickLink) {
     if (null == quickLink.getComponentName()) {
-      return Optional.absent();
+      return Optional.empty();
     }
     else {
-      FilterEvaluator componentEvaluator = componentRules.get(ServiceComponent.of(service, quickLink.getComponentName()));
-      return componentEvaluator != null ? componentEvaluator.isVisible(quickLink) : Optional.absent();
+      FilterEvaluator componentEvaluator = componentRules.get(Pair.of(service, quickLink.getComponentName()));
+      return componentEvaluator != null ? componentEvaluator.isVisible(quickLink) : Optional.empty();
     }
   }
 
   private Optional<Boolean> evaluateServiceRules(@Nonnull String service, @Nonnull Link quickLink) {
     return serviceRules.containsKey(service) ?
-        serviceRules.get(service).isVisible(quickLink) : Optional.absent();
+        serviceRules.get(service).isVisible(quickLink) : Optional.empty();
   }
 
   static <T> List<T> nullToEmptyList(@Nullable List<T> items) {
     return items != null ? items : Collections.emptyList();
   }
-}
 
-/**
- * Groups quicklink filters that are on the same level (e.g. a global evaluator or an evaluator for the "HDFS" service,
- * etc.). The evaluator pick the most applicable filter for a given quick link. If no applicable filter is found, it
- * returns {@link Optional#absent()}.
- * <p>
- *   Filter evaluation order is the following:
- *   <ol>
- *     <li>First, link name filters are evaluated. These match links by name.</li>
- *     <li>If there is no matching link name filter, link attribute filters are evaluated next. "Hide" type filters
- *     take precedence to "show" type filters.</li>
- *     <li>Finally, the match-all filter is evaluated, provided it exists.</li>
- *   </ol>
- * </p>
- */
-class FilterEvaluator {
-  private final Map<String, Boolean> linkNameFilters = new HashMap<>();
-  private final Set<String> showAttributes = new HashSet<>();
-  private final Set<String> hideAttributes = new HashSet<>();
-  private Optional<Boolean> acceptAllFilter = Optional.absent();
-
-  FilterEvaluator(List<Filter> filters) throws QuickLinksProfileEvaluationException {
-    for (Filter filter: DefaultQuickLinkVisibilityController.nullToEmptyList(filters)) {
-      if (filter instanceof LinkNameFilter) {
-        String linkName = ((LinkNameFilter)filter).getLinkName();
-        if (linkNameFilters.containsKey(linkName) && linkNameFilters.get(linkName) != filter.isVisible()) {
-          throw new QuickLinksProfileEvaluationException("Contradicting filters for link name [" + linkName + "]");
-        }
-        linkNameFilters.put(linkName, filter.isVisible());
-      }
-      else if (filter instanceof LinkAttributeFilter) {
-        String linkAttribute = ((LinkAttributeFilter)filter).getLinkAttribute();
-        if (filter.isVisible()) {
-          showAttributes.add(linkAttribute);
-        }
-        else {
-          hideAttributes.add(linkAttribute);
-        }
-        if (showAttributes.contains(linkAttribute) && hideAttributes.contains(linkAttribute)) {
-          throw new QuickLinksProfileEvaluationException("Contradicting filters for link attribute [" + linkAttribute + "]");
-        }
-      }
-      // If none of the above, it is an accept-all filter. We expect only one of this type for an Evaluator
-      else {
-        if (acceptAllFilter.isPresent() && !acceptAllFilter.get().equals(filter.isVisible())) {
-          throw new QuickLinksProfileEvaluationException("Contradicting accept-all filters.");
-        }
-        acceptAllFilter = Optional.of(filter.isVisible());
-      }
-    }
-  }
-
-  /**
-   * @param quickLink the link to evaluate
-   * @return Three way evaluation result, which can be one of these:
-   *    show: Optional.of(true), hide: Optional.of(false), don't know: absent optional
-   */
-  Optional<Boolean> isVisible(Link quickLink) {
-    // process first priority filters based on link name
-    if (linkNameFilters.containsKey(quickLink.getName())) {
-      return Optional.of(linkNameFilters.get(quickLink.getName()));
-    }
-
-    // process second priority filters based on link attributes
-    // 'hide' rules take precedence over 'show' rules
-    for (String attribute: DefaultQuickLinkVisibilityController.nullToEmptyList(quickLink.getAttributes())) {
-      if (hideAttributes.contains(attribute)) return Optional.of(false);
-    }
-    for (String attribute: DefaultQuickLinkVisibilityController.nullToEmptyList(quickLink.getAttributes())) {
-      if (showAttributes.contains(attribute)) return Optional.of(true);
-    }
-
-    // accept all filter (if exists) is the last priority
-    return acceptAllFilter;
-  }
 }
 
-/**
- * Simple value class encapsulating a link name an component name.
- */
-class ServiceComponent {
-  private final String service;
-  private final String component;
-
-  ServiceComponent(String service, String component) {
-    this.service = service;
-    this.component = component;
-  }
-
-  static ServiceComponent of(String service, String component) {
-    return new ServiceComponent(service, component);
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (this == o) return true;
-    if (o == null || getClass() != o.getClass()) return false;
-    ServiceComponent that = (ServiceComponent) o;
-    return Objects.equals(service, that.service) &&
-        Objects.equals(component, that.component);
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(service, component);
-  }
-}
\ No newline at end of file
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/Filter.java b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/Filter.java
index 26410a5..2b54e2f 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/Filter.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/Filter.java
@@ -19,10 +19,10 @@
 package org.apache.ambari.server.state.quicklinksprofile;
 
 import org.apache.ambari.server.state.quicklinks.Link;
-import org.codehaus.jackson.annotate.JsonIgnoreProperties;
-import org.codehaus.jackson.annotate.JsonProperty;
-import org.codehaus.jackson.map.annotate.JsonSerialize;
 
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import com.google.common.base.Preconditions;
 
 /**
@@ -66,9 +66,14 @@ public abstract class Filter {
   }
 
   static LinkNameFilter linkNameFilter(String linkName, boolean visible) {
+    return linkNameFilter(linkName, null, visible);
+  }
+
+  static LinkNameFilter linkNameFilter(String linkName, String linkUrl, boolean visible) {
     Preconditions.checkNotNull(linkName, "Link name must not be null");
     LinkNameFilter linkNameFilter = new LinkNameFilter();
     linkNameFilter.setLinkName(linkName);
+    linkNameFilter.setLinkUrl(linkUrl);
     linkNameFilter.setVisible(visible);
     return linkNameFilter;
   }
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/DefaultQuickLinkVisibilityController.java b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/FilterEvaluator.java
similarity index 51%
copy from ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/DefaultQuickLinkVisibilityController.java
copy to ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/FilterEvaluator.java
index d0b0442..5b6124a 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/DefaultQuickLinkVisibilityController.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/FilterEvaluator.java
@@ -18,97 +18,19 @@
 
 package org.apache.ambari.server.state.quicklinksprofile;
 
-import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
 import org.apache.ambari.server.state.quicklinks.Link;
 
-import com.google.common.base.Optional;
-
-/**
- * This class can evaluate whether a quicklink has to be shown or hidden based on the received {@link QuickLinksProfile}.
- */
-public class DefaultQuickLinkVisibilityController implements QuickLinkVisibilityController {
-  private final FilterEvaluator globalRules;
-  private final Map<String, FilterEvaluator> serviceRules = new HashMap<>();
-  private final Map<ServiceComponent, FilterEvaluator> componentRules = new HashMap<>();
-
-  public DefaultQuickLinkVisibilityController(QuickLinksProfile profile) throws QuickLinksProfileEvaluationException {
-    int filterCount = size(profile.getFilters());
-    globalRules = new FilterEvaluator(profile.getFilters());
-    for (Service service: nullToEmptyList(profile.getServices())) {
-      filterCount += size(service.getFilters());
-      serviceRules.put(service.getName(), new FilterEvaluator(service.getFilters()));
-      for (Component component: nullToEmptyList(service.getComponents())) {
-        filterCount += size(component.getFilters());
-        componentRules.put(ServiceComponent.of(service.getName(), component.getName()),
-            new FilterEvaluator(component.getFilters()));
-      }
-    }
-    if (filterCount == 0) {
-      throw new QuickLinksProfileEvaluationException("At least one filter must be defined.");
-    }
-  }
-
-  /**
-   * @param service the name of the service
-   * @param quickLink the quicklink
-   * @return a boolean indicating whether the link in the parameter should be visible
-   */
-  public boolean isVisible(@Nonnull String service, @Nonnull Link quickLink) {
-    // First, component rules are evaluated if exist and applicable
-    Optional<Boolean> componentResult = evaluateComponentRules(service, quickLink);
-    if (componentResult.isPresent()) {
-      return componentResult.get();
-    }
-
-    // Secondly, service level rules are applied
-    Optional<Boolean> serviceResult = evaluateServiceRules(service, quickLink);
-    if (serviceResult.isPresent()) {
-      return serviceResult.get();
-    }
-
-    // Global rules are evaluated lastly. If no rules apply to the link, it will be hidden.
-    return globalRules.isVisible(quickLink).or(false);
-  }
-
-  private int size(@Nullable Collection<?> collection) {
-    return null == collection ? 0 : collection.size();
-  }
-
-  private Optional<Boolean> evaluateComponentRules(@Nonnull String service, @Nonnull Link quickLink) {
-    if (null == quickLink.getComponentName()) {
-      return Optional.absent();
-    }
-    else {
-      FilterEvaluator componentEvaluator = componentRules.get(ServiceComponent.of(service, quickLink.getComponentName()));
-      return componentEvaluator != null ? componentEvaluator.isVisible(quickLink) : Optional.absent();
-    }
-  }
-
-  private Optional<Boolean> evaluateServiceRules(@Nonnull String service, @Nonnull Link quickLink) {
-    return serviceRules.containsKey(service) ?
-        serviceRules.get(service).isVisible(quickLink) : Optional.absent();
-  }
-
-  static <T> List<T> nullToEmptyList(@Nullable List<T> items) {
-    return items != null ? items : Collections.emptyList();
-  }
-}
-
 /**
  * Groups quicklink filters that are on the same level (e.g. a global evaluator or an evaluator for the "HDFS" service,
  * etc.). The evaluator pick the most applicable filter for a given quick link. If no applicable filter is found, it
- * returns {@link Optional#absent()}.
+ * returns {@link Optional#empty()}.
  * <p>
  *   Filter evaluation order is the following:
  *   <ol>
@@ -123,7 +45,7 @@ class FilterEvaluator {
   private final Map<String, Boolean> linkNameFilters = new HashMap<>();
   private final Set<String> showAttributes = new HashSet<>();
   private final Set<String> hideAttributes = new HashSet<>();
-  private Optional<Boolean> acceptAllFilter = Optional.absent();
+  private Optional<Boolean> acceptAllFilter = Optional.empty();
 
   FilterEvaluator(List<Filter> filters) throws QuickLinksProfileEvaluationException {
     for (Filter filter: DefaultQuickLinkVisibilityController.nullToEmptyList(filters)) {
@@ -180,34 +102,3 @@ class FilterEvaluator {
     return acceptAllFilter;
   }
 }
-
-/**
- * Simple value class encapsulating a link name an component name.
- */
-class ServiceComponent {
-  private final String service;
-  private final String component;
-
-  ServiceComponent(String service, String component) {
-    this.service = service;
-    this.component = component;
-  }
-
-  static ServiceComponent of(String service, String component) {
-    return new ServiceComponent(service, component);
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (this == o) return true;
-    if (o == null || getClass() != o.getClass()) return false;
-    ServiceComponent that = (ServiceComponent) o;
-    return Objects.equals(service, that.service) &&
-        Objects.equals(component, that.component);
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(service, component);
-  }
-}
\ No newline at end of file
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/LinkAttributeFilter.java b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/LinkAttributeFilter.java
index b10111d..d13b3d5 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/LinkAttributeFilter.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/LinkAttributeFilter.java
@@ -21,7 +21,9 @@ package org.apache.ambari.server.state.quicklinksprofile;
 import java.util.Objects;
 
 import org.apache.ambari.server.state.quicklinks.Link;
-import org.codehaus.jackson.annotate.JsonProperty;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.MoreObjects;
 
 /**
  * A quicklink filter based on link attribute match (the filter's link_attribute is contained by the links set of
@@ -58,4 +60,11 @@ public class LinkAttributeFilter extends Filter {
   public int hashCode() {
     return Objects.hash(isVisible(), linkAttribute);
   }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this)
+      .add("linkAttribute", linkAttribute)
+      .toString();
+  }
 }
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/LinkNameFilter.java b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/LinkNameFilter.java
index b874295..e9d0521 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/LinkNameFilter.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/LinkNameFilter.java
@@ -19,28 +19,56 @@
 package org.apache.ambari.server.state.quicklinksprofile;
 
 import java.util.Objects;
+import java.util.stream.Stream;
+
+import javax.annotation.Nullable;
 
 import org.apache.ambari.server.state.quicklinks.Link;
-import org.codehaus.jackson.annotate.JsonProperty;
+import org.apache.ambari.server.utils.StreamUtils;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.MoreObjects;
+
 
 /**
  * A filter that accepts quicklinks based on name match.
  */
+@JsonInclude(JsonInclude.Include.NON_NULL)
 public class LinkNameFilter extends Filter {
 
   static final String LINK_NAME = "link_name";
+  static final String LINK_URL = "link_url";
 
   @JsonProperty(LINK_NAME)
   private String linkName;
 
+  /**
+   * In addition to filtering this filter allows overriding the link url too.
+   */
+  @JsonProperty(LINK_URL)
+  private String linkUrl;
+
+  @JsonProperty(LINK_NAME)
   public String getLinkName() {
     return linkName;
   }
 
+  @JsonProperty(LINK_NAME)
   public void setLinkName(String linkName) {
     this.linkName = linkName;
   }
 
+  @JsonProperty(LINK_URL)
+  public @Nullable String getLinkUrl() {
+    return linkUrl;
+  }
+
+  @JsonProperty(LINK_URL)
+  public void setLinkUrl(@Nullable String linkUrl) {
+    this.linkUrl = linkUrl;
+  }
+
   @Override
   public boolean accept(Link link) {
     return Objects.equals(link.getName(), linkName);
@@ -51,11 +79,26 @@ public class LinkNameFilter extends Filter {
     if (this == o) return true;
     if (o == null || getClass() != o.getClass()) return false;
     LinkNameFilter that = (LinkNameFilter) o;
-    return isVisible() == that.isVisible() && Objects.equals(linkName, that.linkName);
+    return Objects.equals(linkName, that.linkName) &&
+      Objects.equals(linkUrl, that.linkUrl) &&
+      isVisible() == that.isVisible();
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(isVisible(), linkName);
+    return Objects.hash(linkName, linkUrl);
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this)
+      .add("linkName", linkName)
+      .add("linkUrl", linkUrl)
+      .add("visible", isVisible())
+      .toString();
+  }
+
+  static Stream<LinkNameFilter> getLinkNameFilters(Stream<Filter> input) {
+    return StreamUtils.instancesOf(input, LinkNameFilter.class);
   }
 }
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinkVisibilityController.java b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinkVisibilityController.java
index caa2e2e..4f137e3 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinkVisibilityController.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinkVisibilityController.java
@@ -19,6 +19,8 @@
 package org.apache.ambari.server.state.quicklinksprofile;
 
 
+import java.util.Optional;
+
 import javax.annotation.Nonnull;
 
 import org.apache.ambari.server.state.quicklinks.Link;
@@ -33,5 +35,12 @@ public interface QuickLinkVisibilityController {
    */
   boolean isVisible(@Nonnull String service, @Nonnull Link quickLink);
 
+  /**
+   * @param service The name of the service the quicklink belongs to
+   * @param quickLink the link
+   * @return An optional url override for this link
+   */
+  Optional<String> getUrlOverride(@Nonnull String service, @Nonnull Link quickLink);
+
 }
 
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfile.java b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfile.java
index 7d480e9..9382dab 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfile.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfile.java
@@ -18,11 +18,13 @@
 
 package org.apache.ambari.server.state.quicklinksprofile;
 
+import static java.util.Collections.emptyList;
+
 import java.util.List;
 
-import org.codehaus.jackson.annotate.JsonIgnoreProperties;
-import org.codehaus.jackson.annotate.JsonProperty;
-import org.codehaus.jackson.map.annotate.JsonSerialize;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
 
 /**
  * A quicklinks profile is essentially a set of quick link filters defined on three levels:
@@ -37,7 +39,7 @@ import org.codehaus.jackson.map.annotate.JsonSerialize;
  * before being returned by {@link org.apache.ambari.server.controller.internal.QuickLinkArtifactResourceProvider}.</p>
  * <p>When no profile is set, all quick link's visibility flat will be set to {@code true} by the provider</p>
  */
-@JsonSerialize(include= JsonSerialize.Inclusion.NON_NULL)
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class QuickLinksProfile {
 
@@ -67,7 +69,7 @@ public class QuickLinksProfile {
    * @return service-specific quicklink filter definitions
    */
   public List<Service> getServices() {
-    return services;
+    return services != null ? services : emptyList();
   }
 
   public void setServices(List<Service> services) {
@@ -78,7 +80,7 @@ public class QuickLinksProfile {
    * @return the global quicklink filters
    */
   public List<Filter> getFilters() {
-    return filters;
+    return null != filters ? filters : emptyList();
   }
 
   public void setFilters(List<Filter> filters) {
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileBuilder.java b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileBuilder.java
index 627b1bc..9806caf 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileBuilder.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileBuilder.java
@@ -21,6 +21,7 @@ package org.apache.ambari.server.state.quicklinksprofile;
 import static org.apache.ambari.server.state.quicklinksprofile.Filter.VISIBLE;
 import static org.apache.ambari.server.state.quicklinksprofile.LinkAttributeFilter.LINK_ATTRIBUTE;
 import static org.apache.ambari.server.state.quicklinksprofile.LinkNameFilter.LINK_NAME;
+import static org.apache.ambari.server.state.quicklinksprofile.LinkNameFilter.LINK_URL;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -44,7 +45,7 @@ public class QuickLinksProfileBuilder {
   public static final String COMPONENTS = "components";
   public static final String FILTERS = "filters";
   public static final Set<String> ALLOWED_FILTER_ATTRIBUTES =
-      ImmutableSet.of(VISIBLE, LINK_NAME, LINK_ATTRIBUTE);
+      ImmutableSet.of(VISIBLE, LINK_NAME, LINK_URL, LINK_ATTRIBUTE);
 
   /**
    *
@@ -116,6 +117,7 @@ public class QuickLinksProfileBuilder {
           invalidAttributes);
 
       String linkName = filterAsMap.get(LINK_NAME);
+      String linkUrl = filterAsMap.get(LINK_URL);
       String attributeName = filterAsMap.get(LINK_ATTRIBUTE);
       boolean visible = Boolean.parseBoolean(filterAsMap.get(VISIBLE));
 
@@ -125,8 +127,12 @@ public class QuickLinksProfileBuilder {
           linkName,
           attributeName);
 
+      Preconditions.checkArgument(null == linkUrl || null != linkName,
+        "Invalid filter. Link url can only be applied to link name filters. link_url: %s",
+        linkUrl);
+
       if (null != linkName) {
-        filters.add(Filter.linkNameFilter(linkName, visible));
+        filters.add(Filter.linkNameFilter(linkName, linkUrl, visible));
       }
       else if (null != attributeName) {
         filters.add(Filter.linkAttributeFilter(attributeName, visible));
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileParser.java b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileParser.java
index 1891061..0e0f8b5 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileParser.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileParser.java
@@ -23,16 +23,16 @@ import java.net.URL;
 import java.util.ArrayList;
 import java.util.List;
 
-import org.codehaus.jackson.JsonParseException;
-import org.codehaus.jackson.JsonParser;
-import org.codehaus.jackson.JsonProcessingException;
-import org.codehaus.jackson.Version;
-import org.codehaus.jackson.map.DeserializationContext;
-import org.codehaus.jackson.map.ObjectMapper;
-import org.codehaus.jackson.map.deser.std.StdDeserializer;
-import org.codehaus.jackson.map.module.SimpleModule;
-import org.codehaus.jackson.node.ObjectNode;
-
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.Resources;
 
@@ -44,12 +44,12 @@ public class QuickLinksProfileParser {
 
   public QuickLinksProfileParser() {
     SimpleModule module =
-        new SimpleModule("Quick Links Parser", new Version(1, 0, 0, null));
+        new SimpleModule("Quick Links Parser", new Version(1, 0, 0, null, null, null));
     module.addDeserializer(Filter.class, new QuickLinksFilterDeserializer());
     mapper.registerModule(module);
+    mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
   }
 
-
   public QuickLinksProfile parse(byte[] input) throws IOException {
     return mapper.readValue(input, QuickLinksProfile.class);
   }
@@ -91,20 +91,21 @@ class QuickLinksFilterDeserializer extends StdDeserializer<Filter> {
   @Override
   public Filter deserialize (JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException {
     ObjectMapper mapper = (ObjectMapper) parser.getCodec();
-    ObjectNode root = (ObjectNode) mapper.readTree(parser);
+    ObjectNode root = mapper.readTree(parser);
     Class<? extends Filter> filterClass = null;
     List<String> invalidAttributes = new ArrayList<>();
-    for (String fieldName: ImmutableList.copyOf(root.getFieldNames())) {
+    for (String fieldName: ImmutableList.copyOf(root.fieldNames())) {
       switch(fieldName) {
         case LinkAttributeFilter.LINK_ATTRIBUTE:
           if (null != filterClass) {
-            throw new JsonParseException(PARSE_ERROR_MESSAGE_AMBIGUOUS_FILTER, parser.getCurrentLocation());
+            throw new JsonParseException(parser, PARSE_ERROR_MESSAGE_AMBIGUOUS_FILTER, parser.getCurrentLocation());
           }
           filterClass = LinkAttributeFilter.class;
           break;
         case LinkNameFilter.LINK_NAME:
-          if (null != filterClass) {
-            throw new JsonParseException(PARSE_ERROR_MESSAGE_AMBIGUOUS_FILTER, parser.getCurrentLocation());
+        case LinkNameFilter.LINK_URL:
+          if (null != filterClass && !filterClass.equals(LinkNameFilter.class)) {
+            throw new JsonParseException(parser, PARSE_ERROR_MESSAGE_AMBIGUOUS_FILTER, parser.getCurrentLocation());
           }
           filterClass = LinkNameFilter.class;
           break;
@@ -116,12 +117,12 @@ class QuickLinksFilterDeserializer extends StdDeserializer<Filter> {
       }
     }
     if (!invalidAttributes.isEmpty()) {
-      throw new JsonParseException(PARSE_ERROR_MESSAGE_INVALID_JSON_TAG + invalidAttributes,
+      throw new JsonParseException(parser, PARSE_ERROR_MESSAGE_INVALID_JSON_TAG + invalidAttributes,
           parser.getCurrentLocation());
     }
     if (null == filterClass) {
       filterClass = AcceptAllFilter.class;
     }
-    return mapper.readValue(root, filterClass);
+    return mapper.readValue(root.traverse(), filterClass);
   }
 }
\ No newline at end of file
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/Service.java b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/Service.java
index 07cce29..b3ef612 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/Service.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/Service.java
@@ -18,18 +18,19 @@
 
 package org.apache.ambari.server.state.quicklinksprofile;
 
-import java.util.List;
+import static java.util.Collections.emptyList;
 
-import org.codehaus.jackson.annotate.JsonIgnoreProperties;
-import org.codehaus.jackson.annotate.JsonProperty;
-import org.codehaus.jackson.map.annotate.JsonSerialize;
+import java.util.List;
 
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.base.Preconditions;
 
 /**
  * Class to represent component-level filter definitions
  */
-@JsonSerialize(include= JsonSerialize.Inclusion.NON_NULL)
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class Service {
   @JsonProperty("name")
@@ -62,7 +63,7 @@ public class Service {
    * @return component-specific quicklink filter definitions for components of this service
    */
   public List<Component> getComponents() {
-    return components;
+    return null != components ? components : emptyList();
   }
 
   public void setComponents(List<Component> components) {
@@ -73,7 +74,7 @@ public class Service {
    * @return service-specific filters for this service
    */
   public List<Filter> getFilters() {
-    return filters;
+    return null != filters ? filters : emptyList();
   }
 
   public void setFilters(List<Filter> filters) {
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/ShowAllLinksVisibilityController.java b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/ShowAllLinksVisibilityController.java
index 286a10c..250e816 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/ShowAllLinksVisibilityController.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/ShowAllLinksVisibilityController.java
@@ -19,6 +19,8 @@
 package org.apache.ambari.server.state.quicklinksprofile;
 
 
+import java.util.Optional;
+
 import javax.annotation.Nonnull;
 
 import org.apache.ambari.server.state.quicklinks.Link;
@@ -35,4 +37,8 @@ public class ShowAllLinksVisibilityController implements QuickLinkVisibilityCont
     return true;
   }
 
+  @Override
+  public Optional<String> getUrlOverride(@Nonnull String service, @Nonnull Link quickLink) {
+    return Optional.empty();
+  }
 }
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinkVisibilityController.java b/ambari-server/src/main/java/org/apache/ambari/server/utils/StreamUtils.java
similarity index 59%
copy from ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinkVisibilityController.java
copy to ambari-server/src/main/java/org/apache/ambari/server/utils/StreamUtils.java
index caa2e2e..5a846ed 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinkVisibilityController.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/utils/StreamUtils.java
@@ -16,22 +16,24 @@
  * limitations under the License.
  */
 
-package org.apache.ambari.server.state.quicklinksprofile;
+package org.apache.ambari.server.utils;
 
+import java.util.stream.Stream;
 
-import javax.annotation.Nonnull;
-
-import org.apache.ambari.server.state.quicklinks.Link;
-
-
-public interface QuickLinkVisibilityController {
+/**
+ * Utilities for Streams
+ */
+public class StreamUtils {
 
   /**
-   * @param service The name of the service the quicklink belongs to
-   * @param quickLink the link
-   * @return a boolean indicating if the link should be visible
+   * Filters a stream for instances of a class and returns a typed stream
+   * @param stream the stream to filter
+   * @param clazz stream will be filtered to instances of this class
+   * @param <T> the type of the class
+   * @return A stream of containing only instances of {@link T}
    */
-  boolean isVisible(@Nonnull String service, @Nonnull Link quickLink);
+  public static <T> Stream<T> instancesOf(Stream<?> stream, Class<? extends T> clazz) {
+    return stream.filter(clazz::isInstance).map(clazz::cast);
+  }
 
 }
-
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ProvisionClusterRequestTest.java b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ProvisionClusterRequestTest.java
index 5ed582f..ecd6a5a 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ProvisionClusterRequestTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ProvisionClusterRequestTest.java
@@ -426,7 +426,7 @@ public class ProvisionClusterRequestTest {
 
     ProvisionClusterRequest request = new ProvisionClusterRequest(properties, null);
     assertEquals("Quick links profile doesn't match expected",
-        "{\"filters\":[{\"visible\":true}],\"services\":[]}",
+        "{\"filters\":[{\"visible\":true}]}",
         request.getQuickLinksProfileJson());
   }
 
@@ -441,7 +441,7 @@ public class ProvisionClusterRequestTest {
 
     ProvisionClusterRequest request = new ProvisionClusterRequest(properties, null);
     assertEquals("Quick links profile doesn't match expected",
-        "{\"filters\":[],\"services\":[{\"name\":\"HDFS\",\"components\":[],\"filters\":[{\"visible\":true}]}]}",
+        "{\"services\":[{\"name\":\"HDFS\",\"filters\":[{\"visible\":true}]}]}",
         request.getQuickLinksProfileJson());
   }
 
@@ -460,7 +460,7 @@ public class ProvisionClusterRequestTest {
     ProvisionClusterRequest request = new ProvisionClusterRequest(properties, null);
     System.out.println(request.getQuickLinksProfileJson());
     assertEquals("Quick links profile doesn't match expected",
-        "{\"filters\":[{\"visible\":true}],\"services\":[{\"name\":\"HDFS\",\"components\":[],\"filters\":[{\"visible\":true}]}]}",
+        "{\"filters\":[{\"visible\":true}],\"services\":[{\"name\":\"HDFS\",\"filters\":[{\"visible\":true}]}]}",
         request.getQuickLinksProfileJson());
   }
 
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/QuickLinkArtifactResourceProviderTest.java b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/QuickLinkArtifactResourceProviderTest.java
index 9ce6471..a2cb231 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/QuickLinkArtifactResourceProviderTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/QuickLinkArtifactResourceProviderTest.java
@@ -21,6 +21,7 @@ import static org.easymock.EasyMock.anyString;
 import static org.easymock.EasyMock.createMock;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
@@ -41,9 +42,7 @@ import org.apache.ambari.server.state.ServiceInfo;
 import org.apache.ambari.server.state.StackInfo;
 import org.apache.ambari.server.state.quicklinks.Link;
 import org.apache.ambari.server.state.quicklinks.QuickLinks;
-import org.apache.ambari.server.state.quicklinksprofile.QuickLinkVisibilityController;
 import org.apache.ambari.server.state.quicklinksprofile.QuickLinkVisibilityControllerFactory;
-import org.easymock.IAnswer;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -97,6 +96,28 @@ public class QuickLinkArtifactResourceProviderTest {
   }
 
   /**
+   * Test the application of link url override in the quick links profile
+   */
+  @Test
+  public void getResourcesWithUrlOverride() throws Exception {
+    quicklinkProfile = Resources.toString(Resources.getResource("example_quicklinks_profile.json"), Charsets.UTF_8);
+
+    QuickLinkArtifactResourceProvider provider = createProvider();
+    Predicate predicate = new PredicateBuilder().property(
+      QuickLinkArtifactResourceProvider.STACK_NAME_PROPERTY_ID).equals("HDP").
+      and().
+      property(QuickLinkArtifactResourceProvider.STACK_VERSION_PROPERTY_ID).equals("2.0.6").
+      and().
+      property(QuickLinkArtifactResourceProvider.STACK_SERVICE_NAME_PROPERTY_ID).equals("YARN").
+      toPredicate();
+    Set<Resource> resources =
+      provider.getResources(PropertyHelper.getReadRequest(Sets.newHashSet()), predicate);
+    Map<String, Link> linkMap = getLinks(resources);
+
+    assertEquals("http://customlink.org/resourcemanager", linkMap.get("resourcemanager_ui").getUrl());
+  }
+
+  /**
    * Test to prove the all links are visible if no profile is set
    */
   @Test
@@ -171,14 +192,8 @@ public class QuickLinkArtifactResourceProviderTest {
 
       AmbariManagementController amc = createMock(AmbariManagementController.class);
       expect(amc.getAmbariMetaInfo()).andReturn(metaInfo).anyTimes();
-      expect(amc.getQuicklinkVisibilityController()).andAnswer(
-          new IAnswer<QuickLinkVisibilityController>() {
-            @Override
-            public QuickLinkVisibilityController answer() throws Throwable {
-              return QuickLinkVisibilityControllerFactory.get(quicklinkProfile);
-            }
-          }
-      ).anyTimes();
+      expect(amc.getQuicklinkVisibilityController())
+        .andAnswer(() -> QuickLinkVisibilityControllerFactory.get(quicklinkProfile)).anyTimes();
 
       try {
         expect(metaInfo.getStack(anyString(), anyString())).andReturn(stack).anyTimes();
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/FilterEvaluatorTest.java b/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/FilterEvaluatorTest.java
index 5821006..d69a167 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/FilterEvaluatorTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/FilterEvaluatorTest.java
@@ -25,11 +25,11 @@ import static org.junit.Assert.assertEquals;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 
 import org.apache.ambari.server.state.quicklinks.Link;
 import org.junit.Test;
 
-import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 
@@ -62,14 +62,14 @@ public class FilterEvaluatorTest {
   @Test
   public void testWithEmptyFilters() throws Exception {
     FilterEvaluator evaluator = new FilterEvaluator(new ArrayList<>());
-    assertEquals(Optional.absent(), evaluator.isVisible(namenodeUi));
+    assertEquals(Optional.empty(), evaluator.isVisible(namenodeUi));
 
     FilterEvaluator evaluator2 = new FilterEvaluator(null);
-    assertEquals(Optional.absent(), evaluator2.isVisible(namenodeUi));
+    assertEquals(Optional.empty(), evaluator2.isVisible(namenodeUi));
   }
 
   /**
-   * FilterEvaluator should return {@link Optional#absent()} when the link doesn't match any filters
+   * FilterEvaluator should return {@link Optional.empty()} when the link doesn't match any filters
    */
   @Test
   public void testNoMatchingFilter() throws Exception {
@@ -77,7 +77,7 @@ public class FilterEvaluatorTest {
         linkNameFilter(NAMENODE_JMX, true),
         linkAttributeFilter(SSO, false));
     FilterEvaluator evaluator = new FilterEvaluator(filters);
-    assertEquals(Optional.absent(), evaluator.isVisible(namenodeUi));
+    assertEquals(Optional.empty(), evaluator.isVisible(namenodeUi));
   }
 
   /**
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinkVisibilityControllerTest.java b/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinkVisibilityControllerTest.java
index 1a36b9d..aa5a2df 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinkVisibilityControllerTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinkVisibilityControllerTest.java
@@ -18,9 +18,13 @@
 
 package org.apache.ambari.server.state.quicklinksprofile;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import java.util.List;
+import java.util.Optional;
+
 import org.apache.ambari.server.state.quicklinks.Link;
 import org.junit.Test;
 
@@ -32,16 +36,26 @@ public class QuickLinkVisibilityControllerTest {
   static final String SSO = "sso";
   static final String NAMENODE = "NAMENODE";
   static final String HDFS = "HDFS";
+  public static final String YARN = "YARN";
   static final String NAMENODE_UI = "namenode_ui";
+  static final String NAMENODE_LOGS = "namenode_logs";
+  static final String NAMENODE_JMX = "namenode_jmx";
+  static final String THREAD_STACKS = "Thread Stacks";
+  static final String LINK_URL_1 = "www.overridden.org/1";
+  static final String LINK_URL_2 = "www.overridden.org/2";
+  static final String LINK_URL_3 = "www.overridden.org/3";
 
 
   private Link namenodeUi;
+  private Link namenodeLogs;
+  private Link namenodeJmx;
+  private Link threadStacks;
 
   public QuickLinkVisibilityControllerTest() {
-    namenodeUi = new Link();
-    namenodeUi.setComponentName(NAMENODE);
-    namenodeUi.setName(NAMENODE_UI);
-    namenodeUi.setAttributes(ImmutableList.of(AUTHENTICATED));
+    namenodeUi = link(NAMENODE_UI, NAMENODE, ImmutableList.of(AUTHENTICATED));
+    namenodeLogs = link(NAMENODE_LOGS, NAMENODE, null);
+    namenodeJmx = link(NAMENODE_JMX, NAMENODE, null);
+    threadStacks = link(THREAD_STACKS, NAMENODE, null);
   }
 
   /**
@@ -178,4 +192,65 @@ public class QuickLinkVisibilityControllerTest {
         evaluator.isVisible(HDFS, namenodeUi));
   }
 
+  @Test
+  public void testUrlOverride() throws Exception {
+    Component nameNode = Component.create(
+      NAMENODE,
+      ImmutableList.of(
+        Filter.linkNameFilter(NAMENODE_UI, true),
+        Filter.linkNameFilter(NAMENODE_LOGS, LINK_URL_1, true)));
+    Service hdfs = Service.create(
+      HDFS,
+      ImmutableList.of(Filter.linkNameFilter(NAMENODE_JMX, LINK_URL_2, true)),
+      ImmutableList.of(nameNode));
+    QuickLinksProfile profile = QuickLinksProfile.create(
+      ImmutableList.of(Filter.linkNameFilter(THREAD_STACKS, LINK_URL_3, true)),
+      ImmutableList.of(hdfs));
+
+    DefaultQuickLinkVisibilityController evaluator = new DefaultQuickLinkVisibilityController(profile);
+    assertEquals(Optional.empty(), evaluator.getUrlOverride(HDFS, namenodeUi));
+    assertEquals(Optional.of(LINK_URL_1), evaluator.getUrlOverride(HDFS, namenodeLogs));
+    assertEquals(Optional.of(LINK_URL_2), evaluator.getUrlOverride(HDFS, namenodeJmx));
+    // component name doesn't matter
+    namenodeLogs.setComponentName(null);
+    assertEquals(Optional.of(LINK_URL_1), evaluator.getUrlOverride(HDFS, namenodeLogs));
+    // no override for links not in the profile
+    assertEquals(Optional.empty(), evaluator.getUrlOverride(YARN, link("resourcemanager_ui", "RESOURCEMANAGER", null)));
+    // url overrides in global filters are ignored
+    assertEquals(Optional.empty(), evaluator.getUrlOverride(HDFS, threadStacks));
+  }
+
+  @Test
+  public void testUrlOverride_duplicateDefinitions() throws Exception {
+    // same link is defined twice for a service
+    Component nameNode = Component.create(
+      NAMENODE,
+      ImmutableList.of(
+        Filter.linkNameFilter(NAMENODE_UI, LINK_URL_1, true))); // this will override service level setting for the same link
+    Service hdfs = Service.create(
+      HDFS,
+      ImmutableList.of(Filter.linkNameFilter(NAMENODE_UI, LINK_URL_2, true)), // same link on service level with different url
+      ImmutableList.of(nameNode));
+    Service yarn = Service.create(
+      YARN,
+      ImmutableList.of(Filter.linkNameFilter(NAMENODE_UI, LINK_URL_3, true)), // this belongs to an other service so doesn't affect outcome
+      ImmutableList.of(nameNode));
+
+    QuickLinksProfile profile = QuickLinksProfile.create(
+      ImmutableList.of(),
+      ImmutableList.of(hdfs));
+
+    DefaultQuickLinkVisibilityController evaluator = new DefaultQuickLinkVisibilityController(profile);
+    assertEquals(Optional.of(LINK_URL_1), evaluator.getUrlOverride(HDFS, namenodeUi));
+  }
+
+
+  private static final Link link(String name, String componentName, List<String> attributes) {
+    Link link = new Link();
+    link.setName(name);
+    link.setComponentName(componentName);
+    link.setAttributes(attributes);
+    return link;
+  }
+
 }
\ No newline at end of file
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileBuilderTest.java b/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileBuilderTest.java
index 49244d4..9dba51d 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileBuilderTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileBuilderTest.java
@@ -79,8 +79,13 @@ public class QuickLinksProfileBuilderTest {
   public void testBuildProfileBothGlobalAndServiceFilters() throws Exception {
     Set<Map<String, String>> globalFilters = newHashSet( filter(null, null, false) );
 
-    Map<String, Object> nameNode = component("NAMENODE",
-        newHashSet(filter("namenode_ui", null, false)));
+    Map<String, Object> nameNode = component(
+      "NAMENODE",
+      newHashSet(
+        filter("namenode_ui", null, false),
+        filter("namenode_logs", null, "http://customlink.org/namenode_logs", true)
+      )
+    );
 
     Map<String, Object> hdfs = service("HDFS",
         newHashSet(nameNode),
@@ -94,6 +99,9 @@ public class QuickLinksProfileBuilderTest {
     QuickLinksProfile profile = new QuickLinksProfileParser().parse(profileJson.getBytes());
     assertFilterExists(profile, null, null, Filter.acceptAllFilter(false));
     assertFilterExists(profile, "HDFS", "NAMENODE", Filter.linkNameFilter("namenode_ui", false));
+    assertFilterExists(profile, "HDFS", "NAMENODE", Filter.linkNameFilter("namenode_ui", false));
+    assertFilterExists(profile, "HDFS", "NAMENODE", Filter.linkNameFilter("namenode_logs",
+      "http://customlink.org/namenode_logs", true));
     assertFilterExists(profile, "HDFS", null, Filter.linkAttributeFilter("sso", true));
   }
 
@@ -208,11 +216,23 @@ public class QuickLinksProfileBuilderTest {
     throw new AssertionError("Expected service not found: " + serviceName);
   }
 
-  public static Map<String, String> filter(@Nullable String linkName, @Nullable String attributeName, boolean visible) {
-    Map<String, String> map = new HashMap<>(3);
+  public static Map<String, String> filter(@Nullable String linkName,
+                                           @Nullable String attributeName,
+                                           boolean visible) {
+    return filter(linkName, attributeName, null, visible);
+  }
+
+  public static Map<String, String> filter(@Nullable String linkName,
+                                           @Nullable String attributeName,
+                                           @Nullable String linkUrl,
+                                           boolean visible) {
+    Map<String, String> map = new HashMap<>(4);
     if (null != linkName) {
       map.put(LinkNameFilter.LINK_NAME, linkName);
     }
+    if (null != linkUrl) {
+      map.put(LinkNameFilter.LINK_URL, linkUrl);
+    }
     if (null != attributeName) {
       map.put(LinkAttributeFilter.LINK_ATTRIBUTE, attributeName);
     }
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileParserTest.java b/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileParserTest.java
index 8b01ca5..abe55bf 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileParserTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/state/quicklinksprofile/QuickLinksProfileParserTest.java
@@ -20,9 +20,9 @@ package org.apache.ambari.server.state.quicklinksprofile;
 
 import static org.junit.Assert.assertEquals;
 
-import org.codehaus.jackson.JsonParseException;
 import org.junit.Test;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.google.common.io.Resources;
 
 public class QuickLinksProfileParserTest {
@@ -50,7 +50,7 @@ public class QuickLinksProfileParserTest {
     Component nameNode = hdfs.getComponents().get(0);
     assertEquals(2, nameNode.getFilters().size());
     assertEquals(
-        Filter.linkNameFilter("namenode_ui", false),
+        Filter.linkNameFilter("namenode_ui", "http://customlink.org/namenode", false),
         nameNode.getFilters().get(0));
 
     Component historyServer = profile.getServices().get(1).getComponents().get(0);
@@ -62,18 +62,25 @@ public class QuickLinksProfileParserTest {
     Service yarn = profile.getServices().get(2);
     assertEquals(1, yarn.getFilters().size());
     assertEquals(
-        Filter.linkNameFilter("resourcemanager_ui", true),
+        Filter.linkNameFilter("resourcemanager_ui", "http://customlink.org/resourcemanager", true),
         yarn.getFilters().get(0));
   }
 
-  @Test(expected = JsonParseException.class)
+  @Test(expected = JsonProcessingException.class)
   public void testParseInconsistentProfile_ambigousFilterDefinition() throws Exception {
     String profileName = "inconsistent_quicklinks_profile.json";
     QuickLinksProfileParser parser = new QuickLinksProfileParser();
     parser.parse(Resources.getResource(profileName));
   }
 
-  @Test(expected = JsonParseException.class)
+  @Test(expected = JsonProcessingException.class)
+  public void testParseInconsistentProfile_invalidLinkUrl() throws Exception {
+    String profileName = "inconsistent_quicklinks_profile_4.json";
+    QuickLinksProfileParser parser = new QuickLinksProfileParser();
+    parser.parse(Resources.getResource(profileName));
+  }
+
+  @Test(expected = JsonProcessingException.class)
   public void testParseInconsistentProfile_misspelledFilerDefinition() throws Exception {
     String profileName = "inconsistent_quicklinks_profile_3.json";
     QuickLinksProfileParser parser = new QuickLinksProfileParser();
diff --git a/ambari-server/src/test/resources/example_quicklinks_profile.json b/ambari-server/src/test/resources/example_quicklinks_profile.json
index 50ea5e8..227761f 100644
--- a/ambari-server/src/test/resources/example_quicklinks_profile.json
+++ b/ambari-server/src/test/resources/example_quicklinks_profile.json
@@ -20,6 +20,7 @@
           "filters": [
             {
               "link_name": "namenode_ui",
+              "link_url" : "http://customlink.org/namenode",
               "visible": false
             },
             {
@@ -49,6 +50,7 @@
       "filters": [
         {
           "link_name": "resourcemanager_ui",
+          "link_url" : "http://customlink.org/resourcemanager",
           "visible": true
         }
       ]
diff --git a/ambari-server/src/test/resources/inconsistent_quicklinks_profile_4.json b/ambari-server/src/test/resources/inconsistent_quicklinks_profile_4.json
new file mode 100644
index 0000000..7bc04ed
--- /dev/null
+++ b/ambari-server/src/test/resources/inconsistent_quicklinks_profile_4.json
@@ -0,0 +1,10 @@
+{
+  "filters": [
+    {
+      "link_attributes": "sso",
+      "link_url": "http://quicklinks.org/link",
+      "visible": true
+    }
+  ],
+  "services": []
+}
\ No newline at end of file