001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.utils;
021
022import java.util.HashSet;
023import java.util.Iterator;
024import java.util.Map;
025import java.util.Properties;
026import java.util.Set;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029
030import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
031
032/**
033 * Resolves chained properties from a user-defined property file.
034 */
035public final class ChainedPropertyUtil {
036
037    /**
038     * Used to report undefined property in exception message.
039     */
040    public static final String UNDEFINED_PROPERTY_MESSAGE = "Undefined property: ";
041
042    /**
043     * Property variable expression pattern, matches property variables such as {@code ${basedir}}.
044     */
045    private static final Pattern PROPERTY_VARIABLE_PATTERN = Pattern.compile("\\$\\{([^\\s}]+)}");
046
047    /**
048     * Prevent instantiation.
049     */
050    private ChainedPropertyUtil() {
051    }
052
053    /**
054     * Accepts user defined properties and returns new properties
055     * with all chained properties resolved.
056     *
057     * @param properties the underlying properties to use
058     *                   for property resolution.
059     * @return resolved properties
060     * @throws CheckstyleException when chained property is not defined
061     */
062    public static Properties getResolvedProperties(Properties properties)
063            throws CheckstyleException {
064        final Set<String> unresolvedPropertyNames =
065            new HashSet<>(properties.stringPropertyNames());
066        Iterator<String> unresolvedPropertyIterator = unresolvedPropertyNames.iterator();
067        final Map<Object, Object> comparisonProperties = new Properties();
068
069        while (unresolvedPropertyIterator.hasNext()) {
070            final String propertyName = unresolvedPropertyIterator.next();
071            String propertyValue = properties.getProperty(propertyName);
072            final Matcher matcher = PROPERTY_VARIABLE_PATTERN.matcher(propertyValue);
073
074            while (matcher.find()) {
075                final String propertyVariableExpression = matcher.group();
076                final String unresolvedPropertyName =
077                    getPropertyNameFromExpression(propertyVariableExpression);
078
079                final String resolvedPropertyValue =
080                    properties.getProperty(unresolvedPropertyName);
081
082                if (resolvedPropertyValue != null) {
083                    propertyValue = propertyValue.replace(propertyVariableExpression,
084                        resolvedPropertyValue);
085                    properties.setProperty(propertyName, propertyValue);
086                }
087            }
088
089            if (allChainedPropertiesAreResolved(propertyValue)) {
090                unresolvedPropertyIterator.remove();
091            }
092
093            if (!unresolvedPropertyIterator.hasNext()) {
094
095                if (comparisonProperties.equals(properties)) {
096                    // At this point, we will have not resolved any properties in two iterations,
097                    // so unresolvable properties exist.
098                    throw new CheckstyleException(UNDEFINED_PROPERTY_MESSAGE
099                        + unresolvedPropertyNames);
100                }
101                comparisonProperties.putAll(properties);
102                unresolvedPropertyIterator = unresolvedPropertyNames.iterator();
103            }
104
105        }
106        return properties;
107    }
108
109    /**
110     * Gets an unresolved property name from a property variable expression
111     * by stripping the preceding '${' and trailing '}'.
112     *
113     * @param variableExpression the full property variable expression
114     * @return property name
115     */
116    private static String getPropertyNameFromExpression(String variableExpression) {
117        final int propertyStartIndex = variableExpression.lastIndexOf('{') + 1;
118        final int propertyEndIndex = variableExpression.lastIndexOf('}');
119        return variableExpression.substring(propertyStartIndex, propertyEndIndex);
120    }
121
122    /**
123     * Checks if all chained properties have been resolved. Essentially,
124     * this means that there exist no matches for PROPERTY_VARIABLE_PATTERN in the
125     * property value string.
126     *
127     * @param propertyValue the property value to check
128     * @return true if all chained properties are resolved
129     */
130    private static boolean allChainedPropertiesAreResolved(String propertyValue) {
131        return !PROPERTY_VARIABLE_PATTERN.matcher(propertyValue).find();
132    }
133}