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 ${<prop-name>}}, where {@code <prop-name>}
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 ${<prop-name>}}, where {@code <prop-name>}
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 ${<prop-name>}}, where {@code <prop-name>}
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 }