View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.cli.props;
20  
21  import java.util.HashMap;
22  import java.util.Map;
23  import java.util.function.Function;
24  
25  public class InterpolationHelper {
26  
27      private InterpolationHelper() {}
28  
29      private static final char ESCAPE_CHAR = '\\';
30      private static final String DELIM_START = "${";
31      private static final String DELIM_STOP = "}";
32      private static final String MARKER = "$__";
33  
34      /**
35       * Perform substitution on a property set
36       *
37       * @param properties the property set to perform substitution on
38       * @param callback Callback for substitution
39       */
40      public static void performSubstitution(Map<String, String> properties, Function<String, String> callback) {
41          performSubstitution(properties, callback, true, true);
42      }
43  
44      /**
45       * Perform substitution on a property set
46       *
47       * @param properties the property set to perform substitution on
48       * @param callback the callback to obtain substitution values
49       * @param substituteFromConfig If substitute from configuration
50       * @param defaultsToEmptyString sets an empty string if a replacement value is not found, leaves intact otherwise
51       */
52      public static void performSubstitution(
53              Map<String, String> properties,
54              Function<String, String> callback,
55              boolean substituteFromConfig,
56              boolean defaultsToEmptyString) {
57          Map<String, String> org = new HashMap<>(properties);
58          for (String name : properties.keySet()) {
59              properties.compute(
60                      name,
61                      (k, value) ->
62                              substVars(value, name, null, org, callback, substituteFromConfig, defaultsToEmptyString));
63          }
64      }
65  
66      /**
67       * <p>
68       * This method performs property variable substitution on the
69       * specified value. If the specified value contains the syntax
70       * {@code ${&lt;prop-name&gt;}}, where {@code &lt;prop-name&gt;}
71       * refers to either a configuration property or a system property,
72       * then the corresponding property value is substituted for the variable
73       * placeholder. Multiple variable placeholders may exist in the
74       * specified value as well as nested variable placeholders, which
75       * are substituted from inner most to outer most. Configuration
76       * properties override system properties.
77       * </p>
78       *
79       * @param val The string on which to perform property substitution.
80       * @param currentKey The key of the property being evaluated used to
81       *        detect cycles.
82       * @param cycleMap Map of variable references used to detect nested cycles.
83       * @param configProps Set of configuration properties.
84       * @return The value of the specified string after system property substitution.
85       * @throws IllegalArgumentException If there was a syntax error in the
86       *         property placeholder syntax or a recursive variable reference.
87       **/
88      public static String substVars(
89              String val, String currentKey, Map<String, String> cycleMap, Map<String, String> configProps) {
90          return substVars(val, currentKey, cycleMap, configProps, null);
91      }
92  
93      /**
94       * <p>
95       * This method performs property variable substitution on the
96       * specified value. If the specified value contains the syntax
97       * {@code ${&lt;prop-name&gt;}}, where {@code &lt;prop-name&gt;}
98       * refers to either a configuration property or a system property,
99       * then the corresponding property value is substituted for the variable
100      * placeholder. Multiple variable placeholders may exist in the
101      * specified value as well as nested variable placeholders, which
102      * are substituted from inner most to outer most. Configuration
103      * properties override system properties.
104      * </p>
105      *
106      * @param val The string on which to perform property substitution.
107      * @param currentKey The key of the property being evaluated used to
108      *        detect cycles.
109      * @param cycleMap Map of variable references used to detect nested cycles.
110      * @param configProps Set of configuration properties.
111      * @param callback the callback to obtain substitution values
112      * @return The value of the specified string after system property substitution.
113      * @throws IllegalArgumentException If there was a syntax error in the
114      *         property placeholder syntax or a recursive variable reference.
115      **/
116     public static String substVars(
117             String val,
118             String currentKey,
119             Map<String, String> cycleMap,
120             Map<String, String> configProps,
121             Function<String, String> callback) {
122         return substVars(val, currentKey, cycleMap, configProps, callback, true, false);
123     }
124 
125     /**
126      * <p>
127      * This method performs property variable substitution on the
128      * specified value. If the specified value contains the syntax
129      * {@code ${&lt;prop-name&gt;}}, where {@code &lt;prop-name&gt;}
130      * refers to either a configuration property or a system property,
131      * then the corresponding property value is substituted for the variable
132      * placeholder. Multiple variable placeholders may exist in the
133      * specified value as well as nested variable placeholders, which
134      * are substituted from inner most to outer most. Configuration
135      * properties override system properties.
136      * </p>
137      *
138      * @param val The string on which to perform property substitution.
139      * @param currentKey The key of the property being evaluated used to
140      *        detect cycles.
141      * @param cycleMap Map of variable references used to detect nested cycles.
142      * @param configProps Set of configuration properties.
143      * @param callback the callback to obtain substitution values
144      * @param substituteFromConfig If substitute from configuration
145      * @param defaultsToEmptyString sets an empty string if a replacement value is not found, leaves intact otherwise
146      * @return The value of the specified string after system property substitution.
147      * @throws IllegalArgumentException If there was a syntax error in the
148      *         property placeholder syntax or a recursive variable reference.
149      **/
150     public static String substVars(
151             String val,
152             String currentKey,
153             Map<String, String> cycleMap,
154             Map<String, String> configProps,
155             Function<String, String> callback,
156             boolean substituteFromConfig,
157             boolean defaultsToEmptyString) {
158         return unescape(doSubstVars(
159                 val, currentKey, cycleMap, configProps, callback, substituteFromConfig, defaultsToEmptyString));
160     }
161 
162     private static String doSubstVars(
163             String val,
164             String currentKey,
165             Map<String, String> cycleMap,
166             Map<String, String> configProps,
167             Function<String, String> callback,
168             boolean substituteFromConfig,
169             boolean defaultsToEmptyString) {
170         if (cycleMap == null) {
171             cycleMap = new HashMap<>();
172         }
173 
174         // Put the current key in the cycle map.
175         cycleMap.put(currentKey, currentKey);
176 
177         // Assume we have a value that is something like:
178         // "leading ${foo.${bar}} middle ${baz} trailing"
179 
180         // Find the first ending '}' variable delimiter, which
181         // will correspond to the first deepest nested variable
182         // placeholder.
183         int startDelim;
184         int stopDelim = -1;
185         do {
186             stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1);
187             while (stopDelim > 0 && val.charAt(stopDelim - 1) == ESCAPE_CHAR) {
188                 stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1);
189             }
190 
191             // Find the matching starting "${" variable delimiter
192             // by looping until we find a start delimiter that is
193             // greater than the stop delimiter we have found.
194             startDelim = val.indexOf(DELIM_START);
195             while (stopDelim >= 0) {
196                 int idx = val.indexOf(DELIM_START, startDelim + DELIM_START.length());
197                 if ((idx < 0) || (idx > stopDelim)) {
198                     break;
199                 } else if (idx < stopDelim) {
200                     startDelim = idx;
201                 }
202             }
203         } while (startDelim >= 0 && stopDelim >= 0 && stopDelim < startDelim + DELIM_START.length());
204 
205         // If we do not have a start or stop delimiter, then just
206         // return the existing value.
207         if ((startDelim < 0) || (stopDelim < 0)) {
208             cycleMap.remove(currentKey);
209             return val;
210         }
211 
212         // At this point, we have found a variable placeholder so
213         // we must perform a variable substitution on it.
214         // Using the start and stop delimiter indices, extract
215         // the first, deepest nested variable placeholder.
216         String variable = val.substring(startDelim + DELIM_START.length(), stopDelim);
217         String org = variable;
218 
219         // Strip expansion modifiers
220         int idx1 = variable.lastIndexOf(":-");
221         int idx2 = variable.lastIndexOf(":+");
222         int idx = idx1 >= 0 && idx2 >= 0 ? Math.min(idx1, idx2) : idx1 >= 0 ? idx1 : idx2;
223         String op = null;
224         if (idx >= 0) {
225             op = variable.substring(idx);
226             variable = variable.substring(0, idx);
227         }
228 
229         // Verify that this is not a recursive variable reference.
230         if (cycleMap.get(variable) != null) {
231             throw new IllegalArgumentException("recursive variable reference: " + variable);
232         }
233 
234         String substValue = null;
235         // Get the value of the deepest nested variable placeholder.
236         // Try to configuration properties first.
237         if (substituteFromConfig && configProps != null) {
238             substValue = configProps.get(variable);
239         }
240         if (substValue == null) {
241             if (!variable.isEmpty()) {
242                 if (callback != null) {
243                     substValue = callback.apply(variable);
244                 }
245             }
246         }
247 
248         if (op != null) {
249             if (op.startsWith(":-")) {
250                 if (substValue == null || substValue.isEmpty()) {
251                     substValue = op.substring(":-".length());
252                 }
253             } else if (op.startsWith(":+")) {
254                 if (substValue != null && !substValue.isEmpty()) {
255                     substValue = op.substring(":+".length());
256                 }
257             } else {
258                 throw new IllegalArgumentException("Bad substitution: ${" + org + "}");
259             }
260         }
261 
262         if (substValue == null) {
263             if (defaultsToEmptyString) {
264                 substValue = "";
265             } else {
266                 // alters the original token to avoid infinite recursion
267                 // altered tokens are reverted in substVarsPreserveUnresolved()
268                 substValue = MARKER + "{" + variable + "}";
269             }
270         }
271 
272         // Remove the found variable from the cycle map, since
273         // it may appear more than once in the value and we don't
274         // want such situations to appear as a recursive reference.
275         cycleMap.remove(variable);
276 
277         // Append the leading characters, the substituted value of
278         // the variable, and the trailing characters to get the new
279         // value.
280         val = val.substring(0, startDelim) + substValue + val.substring(stopDelim + DELIM_STOP.length());
281 
282         // Now perform substitution again, since there could still
283         // be substitutions to make.
284         val = doSubstVars(
285                 val, currentKey, cycleMap, configProps, callback, substituteFromConfig, defaultsToEmptyString);
286 
287         cycleMap.remove(currentKey);
288 
289         // Return the value.
290         return val;
291     }
292 
293     public static String escape(String val) {
294         return val.replace("$", MARKER);
295     }
296 
297     private static String unescape(String val) {
298         val = val.replaceAll("\\" + MARKER, "\\$");
299         int escape = val.indexOf(ESCAPE_CHAR);
300         while (escape >= 0 && escape < val.length() - 1) {
301             char c = val.charAt(escape + 1);
302             if (c == '{' || c == '}' || c == ESCAPE_CHAR) {
303                 val = val.substring(0, escape) + val.substring(escape + 1);
304             }
305             escape = val.indexOf(ESCAPE_CHAR, escape + 1);
306         }
307         return val;
308     }
309 }