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.internal.impl.model;
20  
21  import java.util.HashMap;
22  import java.util.HashSet;
23  import java.util.Map;
24  import java.util.Set;
25  import java.util.function.BiFunction;
26  import java.util.function.Function;
27  
28  import org.apache.maven.api.annotations.Nullable;
29  import org.apache.maven.api.di.Named;
30  import org.apache.maven.api.di.Singleton;
31  import org.apache.maven.api.services.Interpolator;
32  import org.apache.maven.api.services.InterpolatorException;
33  
34  @Named
35  @Singleton
36  public class DefaultInterpolator implements Interpolator {
37  
38      private static final char ESCAPE_CHAR = '\\';
39      private static final String DELIM_START = "${";
40      private static final String DELIM_STOP = "}";
41      private static final String MARKER = "$__";
42  
43      @Override
44      public void interpolate(
45              Map<String, String> map,
46              Function<String, String> callback,
47              BiFunction<String, String, String> postprocessor,
48              boolean defaultsToEmpty) {
49          Map<String, String> org = new HashMap<>(map);
50          for (String name : map.keySet()) {
51              map.compute(
52                      name,
53                      (k, value) -> interpolate(
54                              value,
55                              name,
56                              null,
57                              v -> {
58                                  String r = org.get(v);
59                                  if (r == null && callback != null) {
60                                      r = callback.apply(v);
61                                  }
62                                  return r;
63                              },
64                              postprocessor,
65                              defaultsToEmpty));
66          }
67      }
68  
69      @Override
70      public String interpolate(
71              String val,
72              Function<String, String> callback,
73              BiFunction<String, String, String> postprocessor,
74              boolean defaultsToEmpty) {
75          return interpolate(val, null, null, callback, postprocessor, defaultsToEmpty);
76      }
77  
78      @Nullable
79      public String interpolate(
80              @Nullable String val,
81              @Nullable String currentKey,
82              @Nullable Set<String> cycleMap,
83              @Nullable Function<String, String> callback,
84              @Nullable BiFunction<String, String, String> postprocessor,
85              boolean defaultsToEmpty) {
86          return substVars(val, currentKey, cycleMap, null, callback, postprocessor, defaultsToEmpty);
87      }
88  
89      /**
90       * Perform substitution on a property set
91       *
92       * @param properties the property set to perform substitution on
93       * @param callback Callback for substitution
94       */
95      public void performSubstitution(Map<String, String> properties, Function<String, String> callback) {
96          performSubstitution(properties, callback, true);
97      }
98  
99      /**
100      * Perform substitution on a property set
101      *
102      * @param properties the property set to perform substitution on
103      * @param callback the callback to obtain substitution values
104      * @param defaultsToEmptyString sets an empty string if a replacement value is not found, leaves intact otherwise
105      */
106     public void performSubstitution(
107             Map<String, String> properties, Function<String, String> callback, boolean defaultsToEmptyString) {
108         Map<String, String> org = new HashMap<>(properties);
109         for (String name : properties.keySet()) {
110             properties.compute(
111                     name, (k, value) -> substVars(value, name, null, org, callback, null, defaultsToEmptyString));
112         }
113     }
114 
115     /**
116      * <p>
117      * This method performs property variable substitution on the
118      * specified value. If the specified value contains the syntax
119      * {@code ${&lt;prop-name&gt;}}, where {@code &lt;prop-name&gt;}
120      * refers to either a configuration property or a system property,
121      * then the corresponding property value is substituted for the variable
122      * placeholder. Multiple variable placeholders may exist in the
123      * specified value as well as nested variable placeholders, which
124      * are substituted from inner most to outer most. Configuration
125      * properties override system properties.
126      * </p>
127      *
128      * @param val The string on which to perform property substitution.
129      * @param currentKey The key of the property being evaluated used to
130      *        detect cycles.
131      * @param cycleMap Map of variable references used to detect nested cycles.
132      * @param configProps Set of configuration properties.
133      * @return The value of the specified string after system property substitution.
134      * @throws InterpolatorException If there was a syntax error in the
135      *         property placeholder syntax or a recursive variable reference.
136      **/
137     public String substVars(String val, String currentKey, Set<String> cycleMap, Map<String, String> configProps) {
138         return substVars(val, currentKey, cycleMap, configProps, null);
139     }
140 
141     /**
142      * <p>
143      * This method performs property variable substitution on the
144      * specified value. If the specified value contains the syntax
145      * {@code ${&lt;prop-name&gt;}}, where {@code &lt;prop-name&gt;}
146      * refers to either a configuration property or a system property,
147      * then the corresponding property value is substituted for the variable
148      * placeholder. Multiple variable placeholders may exist in the
149      * specified value as well as nested variable placeholders, which
150      * are substituted from inner most to outer most. Configuration
151      * properties override system properties.
152      * </p>
153      *
154      * @param val The string on which to perform property substitution.
155      * @param currentKey The key of the property being evaluated used to
156      *        detect cycles.
157      * @param cycleMap Map of variable references used to detect nested cycles.
158      * @param configProps Set of configuration properties.
159      * @param callback the callback to obtain substitution values
160      * @return The value of the specified string after system property substitution.
161      * @throws InterpolatorException If there was a syntax error in the
162      *         property placeholder syntax or a recursive variable reference.
163      **/
164     public String substVars(
165             String val,
166             String currentKey,
167             Set<String> cycleMap,
168             Map<String, String> configProps,
169             Function<String, String> callback) {
170         return substVars(val, currentKey, cycleMap, configProps, callback, null, false);
171     }
172 
173     /**
174      * <p>
175      * This method performs property variable substitution on the
176      * specified value. If the specified value contains the syntax
177      * {@code ${&lt;prop-name&gt;}}, where {@code &lt;prop-name&gt;}
178      * refers to either a configuration property or a system property,
179      * then the corresponding property value is substituted for the variable
180      * placeholder. Multiple variable placeholders may exist in the
181      * specified value as well as nested variable placeholders, which
182      * are substituted from inner most to outer most. Configuration
183      * properties override system properties.
184      * </p>
185      *
186      * @param val The string on which to perform property substitution.
187      * @param currentKey The key of the property being evaluated used to
188      *        detect cycles.
189      * @param cycleMap Map of variable references used to detect nested cycles.
190      * @param configProps Set of configuration properties.
191      * @param callback the callback to obtain substitution values
192      * @param defaultsToEmptyString sets an empty string if a replacement value is not found, leaves intact otherwise
193      * @return The value of the specified string after system property substitution.
194      * @throws IllegalArgumentException If there was a syntax error in the
195      *         property placeholder syntax or a recursive variable reference.
196      **/
197     public static String substVars(
198             String val,
199             String currentKey,
200             Set<String> cycleMap,
201             Map<String, String> configProps,
202             Function<String, String> callback,
203             BiFunction<String, String, String> postprocessor,
204             boolean defaultsToEmptyString) {
205         return unescape(
206                 doSubstVars(val, currentKey, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString));
207     }
208 
209     private static String doSubstVars(
210             String val,
211             String currentKey,
212             Set<String> cycleMap,
213             Map<String, String> configProps,
214             Function<String, String> callback,
215             BiFunction<String, String, String> postprocessor,
216             boolean defaultsToEmptyString) {
217         if (val == null || val.isEmpty()) {
218             return val;
219         }
220         if (cycleMap == null) {
221             cycleMap = new HashSet<>();
222         }
223 
224         // Put the current key in the cycle map.
225         if (currentKey != null) {
226             cycleMap.add(currentKey);
227         }
228 
229         // Assume we have a value that is something like:
230         // "leading ${foo.${bar}} middle ${baz} trailing"
231 
232         // Find the first ending '}' variable delimiter, which
233         // will correspond to the first deepest nested variable
234         // placeholder.
235         int startDelim;
236         int stopDelim = -1;
237         do {
238             stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1);
239             while (stopDelim > 0 && val.charAt(stopDelim - 1) == ESCAPE_CHAR) {
240                 stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1);
241             }
242 
243             // Find the matching starting "${" variable delimiter
244             // by looping until we find a start delimiter that is
245             // greater than the stop delimiter we have found.
246             startDelim = val.indexOf(DELIM_START);
247             while (stopDelim >= 0) {
248                 int idx = val.indexOf(DELIM_START, startDelim + DELIM_START.length());
249                 if ((idx < 0) || (idx > stopDelim)) {
250                     break;
251                 } else if (idx < stopDelim) {
252                     startDelim = idx;
253                 }
254             }
255         } while (startDelim >= 0 && stopDelim >= 0 && stopDelim < startDelim + DELIM_START.length());
256 
257         // If we do not have a start or stop delimiter, then just
258         // return the existing value.
259         if ((startDelim < 0) || (stopDelim < 0)) {
260             cycleMap.remove(currentKey);
261             return val;
262         }
263 
264         // At this point, we have found a variable placeholder so
265         // we must perform a variable substitution on it.
266         // Using the start and stop delimiter indices, extract
267         // the first, deepest nested variable placeholder.
268         String variable = val.substring(startDelim + DELIM_START.length(), stopDelim);
269         String org = variable;
270 
271         String substValue = processSubstitution(
272                 variable, org, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
273 
274         // Append the leading characters, the substituted value of
275         // the variable, and the trailing characters to get the new
276         // value.
277         val = val.substring(0, startDelim) + substValue + val.substring(stopDelim + DELIM_STOP.length());
278 
279         // Now perform substitution again, since there could still
280         // be substitutions to make.
281         val = doSubstVars(val, currentKey, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
282 
283         cycleMap.remove(currentKey);
284 
285         // Return the value.
286         return val;
287     }
288 
289     private static String processSubstitution(
290             String variable,
291             String org,
292             Set<String> cycleMap,
293             Map<String, String> configProps,
294             Function<String, String> callback,
295             BiFunction<String, String, String> postprocessor,
296             boolean defaultsToEmptyString) {
297 
298         // Process chained operators from left to right
299         int startIdx = 0;
300         String currentVar = variable;
301         String substValue = null;
302 
303         while (startIdx < variable.length()) {
304             int idx1 = variable.indexOf(":-", startIdx);
305             int idx2 = variable.indexOf(":+", startIdx);
306             int idx = idx1 >= 0 ? idx2 >= 0 ? Math.min(idx1, idx2) : idx1 : idx2;
307 
308             if (idx < 0) {
309                 // No more operators, process the final variable
310                 if (substValue == null) {
311                     currentVar = variable.substring(startIdx);
312                     substValue = resolveVariable(
313                             currentVar, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
314                 }
315                 break;
316             }
317 
318             // Get the current variable part before the operator
319             String varPart = variable.substring(startIdx, idx);
320             if (substValue == null) {
321                 substValue =
322                         resolveVariable(varPart, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
323             }
324 
325             // Find the end of the current operator's value
326             int nextIdx1 = variable.indexOf(":-", idx + 2);
327             int nextIdx2 = variable.indexOf(":+", idx + 2);
328             int nextIdx = nextIdx1 >= 0 ? nextIdx2 >= 0 ? Math.min(nextIdx1, nextIdx2) : nextIdx1 : nextIdx2;
329 
330             String op = variable.substring(idx, idx + 2);
331             String opValue = variable.substring(idx + 2, nextIdx >= 0 ? nextIdx : variable.length());
332 
333             // Process the operator value through substitution if it contains variables
334             String processedOpValue =
335                     doSubstVars(opValue, org, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
336 
337             // Apply the operator
338             if (":+".equals(op)) {
339                 if (substValue != null && !substValue.isEmpty()) {
340                     substValue = processedOpValue;
341                 }
342             } else if (":-".equals(op)) {
343                 if (substValue == null || substValue.isEmpty()) {
344                     substValue = processedOpValue;
345                 }
346             } else {
347                 throw new InterpolatorException("Bad substitution operator in: ${" + org + "}");
348             }
349 
350             startIdx = nextIdx >= 0 ? nextIdx : variable.length();
351         }
352 
353         if (substValue == null) {
354             if (defaultsToEmptyString) {
355                 substValue = "";
356             } else {
357                 substValue = MARKER + "{" + variable + "}";
358             }
359         }
360 
361         return substValue;
362     }
363 
364     private static String resolveVariable(
365             String variable,
366             Set<String> cycleMap,
367             Map<String, String> configProps,
368             Function<String, String> callback,
369             BiFunction<String, String, String> postprocessor,
370             boolean defaultsToEmptyString) {
371 
372         // Verify that this is not a recursive variable reference
373         if (!cycleMap.add(variable)) {
374             throw new InterpolatorException("recursive variable reference: " + variable);
375         }
376 
377         String substValue = null;
378         // Try configuration properties first
379         if (configProps != null) {
380             substValue = configProps.get(variable);
381         }
382         if (substValue == null && !variable.isEmpty() && callback != null) {
383             String s1 = callback.apply(variable);
384             String s2 =
385                     doSubstVars(s1, variable, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
386             substValue = postprocessor != null ? postprocessor.apply(variable, s2) : s2;
387         }
388 
389         // Remove the variable from cycle map
390         cycleMap.remove(variable);
391         return substValue;
392     }
393 
394     /**
395      * Escapes special characters in the given string to prevent unwanted interpolation.
396      *
397      * @param val The string to be escaped.
398      * @return The escaped string.
399      */
400     @Nullable
401     public static String escape(@Nullable String val) {
402         if (val == null || val.isEmpty()) {
403             return val;
404         }
405         return val.replace("$", MARKER);
406     }
407 
408     /**
409      * Unescapes previously escaped characters in the given string.
410      *
411      * @param val The string to be unescaped.
412      * @return The unescaped string.
413      */
414     @Nullable
415     public static String unescape(@Nullable String val) {
416         if (val == null || val.isEmpty()) {
417             return val;
418         }
419         val = val.replace(MARKER, "$");
420         int escape = val.indexOf(ESCAPE_CHAR);
421         while (escape >= 0 && escape < val.length() - 1) {
422             char c = val.charAt(escape + 1);
423             if (c == '{' || c == '}' || c == ESCAPE_CHAR) {
424                 val = val.substring(0, escape) + val.substring(escape + 1);
425             }
426             escape = val.indexOf(ESCAPE_CHAR, escape + 1);
427         }
428         return val;
429     }
430 }