/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 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.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.<Boolean>absent(); } } private Optional<Boolean> evaluateServiceRules(@Nonnull String service, @Nonnull Link quickLink) { return serviceRules.containsKey(service) ? serviceRules.get(service).isVisible(quickLink) : Optional.<Boolean>absent(); } static <T> List<T> nullToEmptyList(@Nullable List<T> items) { return items != null ? items : Collections.<T>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); } }