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 innermost to outermost. 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 innermost to outermost. 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 innermost to outermost. 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 
267         String substValue =
268                 processSubstitution(variable, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
269 
270         // Append the leading characters, the substituted value of
271         // the variable, and the trailing characters to get the new
272         // value.
273         val = val.substring(0, startDelim) + substValue + val.substring(stopDelim + DELIM_STOP.length());
274 
275         // Now perform substitution again, since there could still
276         // be substitutions to make.
277         val = doSubstVars(val, currentKey, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
278 
279         cycleMap.remove(currentKey);
280 
281         // Return the value.
282         return val;
283     }
284 
285     private static String processSubstitution(
286             String variable,
287             Set<String> cycleMap,
288             Map<String, String> configProps,
289             UnaryOperator<String> callback,
290             BinaryOperator<String> postprocessor,
291             boolean defaultsToEmptyString) {
292 
293         // Process chained operators from left to right
294         int startIdx = 0;
295         String substValue = null;
296 
297         while (startIdx < variable.length()) {
298             int idx1 = variable.indexOf(":-", startIdx);
299             int idx2 = variable.indexOf(":+", startIdx);
300             int idx = idx1 >= 0 ? idx2 >= 0 ? Math.min(idx1, idx2) : idx1 : idx2;
301 
302             if (idx < 0) {
303                 // No more operators, process the final variable
304                 if (substValue == null) {
305                     String currentVar = variable.substring(startIdx);
306                     substValue = resolveVariable(
307                             currentVar, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
308                 }
309                 break;
310             }
311 
312             // Get the current variable part before the operator
313             String varPart = variable.substring(startIdx, idx);
314             if (substValue == null) {
315                 substValue =
316                         resolveVariable(varPart, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
317             }
318 
319             // Find the end of the current operator's value
320             int nextIdx1 = variable.indexOf(":-", idx + 2);
321             int nextIdx2 = variable.indexOf(":+", idx + 2);
322             int nextIdx = nextIdx1 >= 0 ? nextIdx2 >= 0 ? Math.min(nextIdx1, nextIdx2) : nextIdx1 : nextIdx2;
323 
324             String op = variable.substring(idx, idx + 2);
325             String opValue = variable.substring(idx + 2, nextIdx >= 0 ? nextIdx : variable.length());
326 
327             // Process the operator value through substitution if it contains variables
328             String processedOpValue = doSubstVars(
329                     opValue, variable, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
330 
331             // Apply the operator
332             if (":+".equals(op)) {
333                 if (substValue != null && !substValue.isEmpty()) {
334                     substValue = processedOpValue;
335                     // Skip any remaining operators since we've made a decision
336                     break;
337                 }
338             } else if (":-".equals(op)) {
339                 if (substValue == null || substValue.isEmpty()) {
340                     substValue = processedOpValue;
341                     // Skip any remaining operators since we've made a decision
342                     break;
343                 }
344             } else {
345                 throw new InterpolatorException("Bad substitution operator in: ${" + variable + "}");
346             }
347 
348             startIdx = nextIdx >= 0 ? nextIdx : variable.length();
349         }
350 
351         if (substValue == null) {
352             if (defaultsToEmptyString) {
353                 substValue = "";
354             } else {
355                 substValue = MARKER + "{" + variable + "}";
356             }
357         }
358 
359         return substValue;
360     }
361 
362     private static String resolveVariable(
363             String variable,
364             Set<String> cycleMap,
365             Map<String, String> configProps,
366             UnaryOperator<String> callback,
367             BinaryOperator<String> postprocessor,
368             boolean defaultsToEmptyString) {
369 
370         // Verify that this is not a recursive variable reference
371         if (!cycleMap.add(variable)) {
372             throw new InterpolatorException("recursive variable reference: " + variable);
373         }
374 
375         String substValue = null;
376         // Try configuration properties first
377         if (configProps != null) {
378             substValue = configProps.get(variable);
379         }
380         if (substValue == null && !variable.isEmpty() && callback != null) {
381             String s1 = callback.apply(variable);
382             String s2 =
383                     doSubstVars(s1, variable, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
384             substValue = postprocessor != null ? postprocessor.apply(variable, s2) : s2;
385         }
386 
387         // Remove the variable from cycle map
388         cycleMap.remove(variable);
389         return substValue;
390     }
391 
392     /**
393      * Escapes special characters in the given string to prevent unwanted interpolation.
394      *
395      * @param val The string to be escaped.
396      * @return The escaped string.
397      */
398     @Nullable
399     public static String escape(@Nullable String val) {
400         if (val == null || val.isEmpty()) {
401             return val;
402         }
403         return val.replace("$", MARKER);
404     }
405 
406     /**
407      * Unescapes previously escaped characters in the given string.
408      *
409      * @param val The string to be unescaped.
410      * @return The unescaped string.
411      */
412     @Nullable
413     public static String unescape(@Nullable String val) {
414         if (val == null || val.isEmpty()) {
415             return val;
416         }
417         val = val.replace(MARKER, "$");
418         int escape = val.indexOf(ESCAPE_CHAR);
419         while (escape >= 0 && escape < val.length() - 1) {
420             char c = val.charAt(escape + 1);
421             if (c == '{' || c == '}') {
422                 val = val.substring(0, escape) + val.substring(escape + 1);
423             }
424             escape = val.indexOf(ESCAPE_CHAR, escape + 1);
425         }
426         return val;
427     }
428 }