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