/** * 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.controller.internal; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.ambari.server.controller.spi.Predicate; import org.apache.ambari.server.controller.spi.Request; import org.apache.ambari.server.controller.spi.Resource; import org.apache.ambari.server.controller.utilities.PredicateHelper; import org.apache.ambari.server.controller.utilities.PropertyHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Base provider implementation for both property and resource providers. */ public abstract class BaseProvider { /** * Set of property ids supported by this provider. */ private final Set<String> propertyIds; /** * Set of category ids supported by this provider. */ private final Set<String> categoryIds; /** * Combined property and category ids. */ private final Set<String> combinedIds; /** * Pre-compiled patterns for metric names that contain regular expressions. */ private final Map<String, Pattern> patterns; /** * The logger. */ protected final static Logger LOG = LoggerFactory.getLogger(BaseProvider.class); /** * The regex pattern that will match on all $1, $2.method(0), and * $3.method(\"/\",\"///\") and properly substitute. For example, it can turn * <p/> * {@code * metrics/yarn/Queue/$1.replaceAll(\"([.])\",\"/\")/AppsCompleted/$2.substring(0)/foo/$3/bar/$4 * } * <p/> * into * <p/> * {@code metrics/yarn/Queue/-/AppsCompleted/-/foo/-/bar/-} */ private static final Pattern METRIC_ARGUMENT_METHOD_REPLACEMENT = Pattern.compile("\\$\\d+(\\.\\S+\\(\\S+\\))*"); // ----- Constructors ------------------------------------------------------ /** * Construct a provider. * * @param propertyIds the properties associated with this provider */ public BaseProvider(Set<String> propertyIds) { this.propertyIds = new HashSet<>(propertyIds); categoryIds = PropertyHelper.getCategories(propertyIds); combinedIds = new HashSet<>(propertyIds); combinedIds.addAll(categoryIds); patterns = new HashMap<>(); // convert the argumented metric as it's defined in the JSON file to regex for (String id : combinedIds) { if (containsArguments(id)) { String pattern = METRIC_ARGUMENT_METHOD_REPLACEMENT.matcher(id).replaceAll( "(\\\\S*)"); patterns.put(id, Pattern.compile(pattern)); } } } // ----- BaseProvider -------------------------------------------------- /** * Checks for property ids that are not recognized by the provider. * @param base the base set of properties * @param configCategory the config category that would have a <code>desired_config</code> element. * @return the set of properties that are NOT known to the provider */ protected Set<String> checkConfigPropertyIds(Set<String> base, String configCategory) { if (0 == base.size()) { return base; } Set<String> unsupported = new HashSet<>(); for (String propertyId : base) { if (!propertyId.startsWith(configCategory + "/desired_config")) { unsupported.add(propertyId); } } return unsupported; } public Set<String> checkPropertyIds(Set<String> propertyIds) { if (!this.propertyIds.containsAll(propertyIds)) { Set<String> unsupportedPropertyIds = new HashSet<>(propertyIds); unsupportedPropertyIds.removeAll(combinedIds); // If the property id is not in the set of known property ids we may still allow it if // its parent category is a known property. This allows for Map type properties where // we want to treat property as a category and the entries as individual properties. Set<String> categoryProperties = new HashSet<>(); for (String unsupportedPropertyId : unsupportedPropertyIds) { if (checkCategory(unsupportedPropertyId) || checkRegExp(unsupportedPropertyId)) { categoryProperties.add(unsupportedPropertyId); } } unsupportedPropertyIds.removeAll(categoryProperties); return unsupportedPropertyIds; } return Collections.emptySet(); } /** * Get the set of property ids required to satisfy the given request. * * @param request the request * @param predicate the predicate * * @return the set of property ids needed to satisfy the request */ protected Set<String> getRequestPropertyIds(Request request, Predicate predicate) { Set<String> propertyIds = request.getPropertyIds(); // if no properties are specified, then return them all if (propertyIds == null || propertyIds.isEmpty()) { return new HashSet<>(this.propertyIds); } propertyIds = new HashSet<>(propertyIds); if (predicate != null) { propertyIds.addAll(PredicateHelper.getPropertyIds(predicate)); } if (!combinedIds.containsAll(propertyIds)) { Set<String> keepers = new HashSet<>(); Set<String> unsupportedPropertyIds = new HashSet<>(propertyIds); unsupportedPropertyIds.removeAll(combinedIds); for (String unsupportedPropertyId : unsupportedPropertyIds) { if (checkCategory(unsupportedPropertyId) || checkRegExp(unsupportedPropertyId)) { keepers.add(unsupportedPropertyId); } } propertyIds.retainAll(combinedIds); propertyIds.addAll(keepers); } return propertyIds; } /** * Check the categories to account for map properties where the entries will not be * in the provider property list ids but the map (category) might be. */ protected boolean checkCategory(String unsupportedPropertyId) { String category = PropertyHelper.getPropertyCategory(unsupportedPropertyId); while (category != null) { if(propertyIds.contains(category)) { return true; } category = PropertyHelper.getPropertyCategory(category); } return false; } private boolean checkRegExp(String unsupportedPropertyId) { for (Pattern pattern : patterns.values()) { Matcher matcher = pattern.matcher(unsupportedPropertyId); if (matcher.matches()) { return true; } } return false; } /** * Gets the key/value mapping between a metric string in the JSON files and * the Java regular expression pattern that matches it. * * @param id * @return the entry, or {@code null} if none match the ID. */ protected Map.Entry<String, Pattern> getRegexEntry(String id) { Map.Entry<String, Pattern> regexEntry = null; for (Map.Entry<String, Pattern> entry : patterns.entrySet()) { Pattern pattern = entry.getValue(); Matcher matcher = pattern.matcher(id); if (matcher.matches()) { String key = entry.getKey(); if (regexEntry == null || key.startsWith(regexEntry.getKey())) { regexEntry = entry; } } } return regexEntry; } /** * Extracts set of matcher.group() from id * @param regExpKey * @param id * @return extracted regex groups from id */ protected List<String> getRegexGroups(String regExpKey, String id) { Pattern pattern = patterns.get(regExpKey); List<String> regexGroups = new ArrayList<>(); if (pattern != null) { Matcher matcher = pattern.matcher(id); if (matcher.matches()) { for (int i=0; i<matcher.groupCount(); i++){ regexGroups.add(matcher.group(i + 1)); } } } return regexGroups; } protected boolean isPatternKey(String id) { return patterns.containsKey(id); } /** * Check to see if the given property id contains replacement arguments (e.g. $1) * * @param propertyId the property id to check * * @return true if the given property id contains any replacement arguments */ protected boolean containsArguments(String propertyId) { return PropertyHelper.containsArguments(propertyId); } /** * Determine whether or not the given property id is part the given set of requested ids. This * accounts for the cases where the given properties are actually property categories. * * @param propertyId the property id * @param requestedIds the requested set of property ids * * @return true if the given property id is part of the given set of requested ids */ protected static boolean isPropertyRequested(String propertyId, Set<String> requestedIds) { return requestedIds.contains(propertyId) || isPropertyCategoryRequested(propertyId, requestedIds) || isPropertyEntryRequested(propertyId, requestedIds); } /** * Set a property value on the given resource for the given id and value. * Make sure that the id is in the given set of requested ids. * * @param resource the resource * @param propertyId the property id * @param value the value to set * @param requestedIds the requested set of property ids */ protected static boolean setResourceProperty(Resource resource, String propertyId, Object value, Set<String> requestedIds) { boolean contains = requestedIds.contains(propertyId) || isPropertyCategoryRequested(propertyId, requestedIds); if (contains) { if (LOG.isDebugEnabled()) { LOG.debug("Setting property for resource" + ", resourceType=" + resource.getType() + ", propertyId=" + propertyId + ", value=" + value); } // If the value is a Map then set all of its entries as properties if (!setResourceMapProperty(resource, propertyId, value)){ resource.setProperty(propertyId, value); } } else { if (value instanceof Map<?, ?>) { // This map wasn't requested, but maybe some of its entries were... Map<?, ?> mapValue = (Map) value; for (Map.Entry entry : mapValue.entrySet()) { String entryPropertyId = PropertyHelper.getPropertyId(propertyId, entry.getKey().toString()); Object entryValue = entry.getValue(); contains = setResourceProperty(resource, entryPropertyId, entryValue, requestedIds) || contains; } } if (!contains && LOG.isDebugEnabled()) { LOG.debug("Skipping property for resource as not in requestedIds" + ", resourceType=" + resource.getType() + ", propertyId=" + propertyId + ", value=" + value); } } return contains; } /** * If the given value is a Map then add its entries to the resource as properties. * * @param resource the resource * @param propertyId the property id of the given value * @param value the property value */ private static boolean setResourceMapProperty(Resource resource, String propertyId, Object value) { if (value instanceof Map<?, ?>) { Map<?, ?> mapValue = (Map) value; if (mapValue.isEmpty()) { resource.addCategory(propertyId); } else { for (Map.Entry entry : mapValue.entrySet()) { String entryPropertyId = PropertyHelper.getPropertyId(propertyId, entry.getKey().toString()); Object entryValue = entry.getValue(); // If the value is a Map then set all of its entries as properties if (!setResourceMapProperty(resource, entryPropertyId, entryValue)){ resource.setProperty(entryPropertyId, entryValue); } } } return true; } return false; } /** * Determine whether or not any of the requested ids are an entry for the given property, if * the given property is a category. For example, if the given property is 'category/subcategory' * and the set of requested ids contains 'category/subcategory/property' then this method should * return true. * * @param propertyId the property id * @param requestedIds the requested set of property ids * * @return true if the given property is a category for any of the requested ids */ protected static boolean isPropertyEntryRequested(String propertyId, Set<String> requestedIds) { for (String requestedId : requestedIds) { if (requestedId.startsWith(propertyId)) { return true; } } return false; } /** * Determine whether or not any of the requested ids are a category for the given property. * For example, if the given property is 'category/subcategory/property' and the set of requested ids * contains 'category' or 'category/subcategory' then this method should return true. * * @param propertyId the property id * @param requestedIds the requested set of property ids * * @return true if the given property's category is part of the given set of requested ids */ protected static boolean isPropertyCategoryRequested(String propertyId, Set<String> requestedIds) { String category = PropertyHelper.getPropertyCategory(propertyId); while (category != null ) { if (requestedIds.contains(category)) { return true; } category = PropertyHelper.getPropertyCategory(category); } return false; } // ----- accessors --------------------------------------------------------- /** * Get the property ids supported by this property adapter. * * @return the property ids supported by this provider */ public Set<String> getPropertyIds() { return propertyIds; } /** * Get supported categories */ public Set<String> getCategoryIds() { return categoryIds; } }