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 // Strip expansion modifiers
272 int idx1 = variable.lastIndexOf(":-");
273 int idx2 = variable.lastIndexOf(":+");
274 int idx = idx1 >= 0 ? idx2 >= 0 ? Math.min(idx1, idx2) : idx1 : idx2;
275 String op = null;
276 if (idx >= 0) {
277 op = variable.substring(idx);
278 variable = variable.substring(0, idx);
279 }
280
281 // Verify that this is not a recursive variable reference.
282 if (!cycleMap.add(variable)) {
283 throw new InterpolatorException("recursive variable reference: " + variable);
284 }
285
286 String substValue = null;
287 // Get the value of the deepest nested variable placeholder.
288 // Try to configuration properties first.
289 if (configProps != null) {
290 substValue = configProps.get(variable);
291 }
292 if (substValue == null) {
293 if (!variable.isEmpty()) {
294 if (callback != null) {
295 String s1 = callback.apply(variable);
296 String s2 = doSubstVars(
297 s1, variable, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
298 substValue = postprocessor != null ? postprocessor.apply(variable, s2) : s2;
299 }
300 }
301 }
302
303 if (op != null) {
304 if (op.startsWith(":-")) {
305 if (substValue == null || substValue.isEmpty()) {
306 substValue = op.substring(":-".length());
307 }
308 } else if (op.startsWith(":+")) {
309 if (substValue != null && !substValue.isEmpty()) {
310 substValue = op.substring(":+".length());
311 }
312 } else {
313 throw new InterpolatorException("Bad substitution: ${" + org + "}");
314 }
315 }
316
317 if (substValue == null) {
318 if (defaultsToEmptyString) {
319 substValue = "";
320 } else {
321 // alters the original token to avoid infinite recursion
322 // altered tokens are reverted in unescape()
323 substValue = MARKER + "{" + variable + "}";
324 }
325 }
326
327 // Remove the found variable from the cycle map, since
328 // it may appear more than once in the value and we don't
329 // want such situations to appear as a recursive reference.
330 cycleMap.remove(variable);
331
332 // Append the leading characters, the substituted value of
333 // the variable, and the trailing characters to get the new
334 // value.
335 val = val.substring(0, startDelim) + substValue + val.substring(stopDelim + DELIM_STOP.length());
336
337 // Now perform substitution again, since there could still
338 // be substitutions to make.
339 val = doSubstVars(val, currentKey, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
340
341 cycleMap.remove(currentKey);
342
343 // Return the value.
344 return val;
345 }
346
347 /**
348 * Escapes special characters in the given string to prevent unwanted interpolation.
349 *
350 * @param val The string to be escaped.
351 * @return The escaped string.
352 */
353 @Nullable
354 public static String escape(@Nullable String val) {
355 if (val == null || val.isEmpty()) {
356 return val;
357 }
358 return val.replace("$", MARKER);
359 }
360
361 /**
362 * Unescapes previously escaped characters in the given string.
363 *
364 * @param val The string to be unescaped.
365 * @return The unescaped string.
366 */
367 @Nullable
368 public static String unescape(@Nullable String val) {
369 if (val == null || val.isEmpty()) {
370 return val;
371 }
372 val = val.replace(MARKER, "$");
373 int escape = val.indexOf(ESCAPE_CHAR);
374 while (escape >= 0 && escape < val.length() - 1) {
375 char c = val.charAt(escape + 1);
376 if (c == '{' || c == '}' || c == ESCAPE_CHAR) {
377 val = val.substring(0, escape) + val.substring(escape + 1);
378 }
379 escape = val.indexOf(ESCAPE_CHAR, escape + 1);
380 }
381 return val;
382 }
383 }