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.cli.props;
20
21 import java.util.HashMap;
22 import java.util.Map;
23 import java.util.function.Function;
24
25 public class InterpolationHelper {
26
27 private InterpolationHelper() {}
28
29 private static final char ESCAPE_CHAR = '\\';
30 private static final String DELIM_START = "${";
31 private static final String DELIM_STOP = "}";
32 private static final String MARKER = "$__";
33
34 /**
35 * Perform substitution on a property set
36 *
37 * @param properties the property set to perform substitution on
38 * @param callback Callback for substitution
39 */
40 public static void performSubstitution(Map<String, String> properties, Function<String, String> callback) {
41 performSubstitution(properties, callback, true, true);
42 }
43
44 /**
45 * Perform substitution on a property set
46 *
47 * @param properties the property set to perform substitution on
48 * @param callback the callback to obtain substitution values
49 * @param substituteFromConfig If substitute from configuration
50 * @param defaultsToEmptyString sets an empty string if a replacement value is not found, leaves intact otherwise
51 */
52 public static void performSubstitution(
53 Map<String, String> properties,
54 Function<String, String> callback,
55 boolean substituteFromConfig,
56 boolean defaultsToEmptyString) {
57 Map<String, String> org = new HashMap<>(properties);
58 for (String name : properties.keySet()) {
59 properties.compute(
60 name,
61 (k, value) ->
62 substVars(value, name, null, org, callback, substituteFromConfig, defaultsToEmptyString));
63 }
64 }
65
66 /**
67 * <p>
68 * This method performs property variable substitution on the
69 * specified value. If the specified value contains the syntax
70 * {@code ${<prop-name>}}, where {@code <prop-name>}
71 * refers to either a configuration property or a system property,
72 * then the corresponding property value is substituted for the variable
73 * placeholder. Multiple variable placeholders may exist in the
74 * specified value as well as nested variable placeholders, which
75 * are substituted from inner most to outer most. Configuration
76 * properties override system properties.
77 * </p>
78 *
79 * @param val The string on which to perform property substitution.
80 * @param currentKey The key of the property being evaluated used to
81 * detect cycles.
82 * @param cycleMap Map of variable references used to detect nested cycles.
83 * @param configProps Set of configuration properties.
84 * @return The value of the specified string after system property substitution.
85 * @throws IllegalArgumentException If there was a syntax error in the
86 * property placeholder syntax or a recursive variable reference.
87 **/
88 public static String substVars(
89 String val, String currentKey, Map<String, String> cycleMap, Map<String, String> configProps) {
90 return substVars(val, currentKey, cycleMap, configProps, null);
91 }
92
93 /**
94 * <p>
95 * This method performs property variable substitution on the
96 * specified value. If the specified value contains the syntax
97 * {@code ${<prop-name>}}, where {@code <prop-name>}
98 * refers to either a configuration property or a system property,
99 * then the corresponding property value is substituted for the variable
100 * placeholder. Multiple variable placeholders may exist in the
101 * specified value as well as nested variable placeholders, which
102 * are substituted from inner most to outer most. Configuration
103 * properties override system properties.
104 * </p>
105 *
106 * @param val The string on which to perform property substitution.
107 * @param currentKey The key of the property being evaluated used to
108 * detect cycles.
109 * @param cycleMap Map of variable references used to detect nested cycles.
110 * @param configProps Set of configuration properties.
111 * @param callback the callback to obtain substitution values
112 * @return The value of the specified string after system property substitution.
113 * @throws IllegalArgumentException If there was a syntax error in the
114 * property placeholder syntax or a recursive variable reference.
115 **/
116 public static String substVars(
117 String val,
118 String currentKey,
119 Map<String, String> cycleMap,
120 Map<String, String> configProps,
121 Function<String, String> callback) {
122 return substVars(val, currentKey, cycleMap, configProps, callback, true, false);
123 }
124
125 /**
126 * <p>
127 * This method performs property variable substitution on the
128 * specified value. If the specified value contains the syntax
129 * {@code ${<prop-name>}}, where {@code <prop-name>}
130 * refers to either a configuration property or a system property,
131 * then the corresponding property value is substituted for the variable
132 * placeholder. Multiple variable placeholders may exist in the
133 * specified value as well as nested variable placeholders, which
134 * are substituted from inner most to outer most. Configuration
135 * properties override system properties.
136 * </p>
137 *
138 * @param val The string on which to perform property substitution.
139 * @param currentKey The key of the property being evaluated used to
140 * detect cycles.
141 * @param cycleMap Map of variable references used to detect nested cycles.
142 * @param configProps Set of configuration properties.
143 * @param callback the callback to obtain substitution values
144 * @param substituteFromConfig If substitute from configuration
145 * @param defaultsToEmptyString sets an empty string if a replacement value is not found, leaves intact otherwise
146 * @return The value of the specified string after system property substitution.
147 * @throws IllegalArgumentException If there was a syntax error in the
148 * property placeholder syntax or a recursive variable reference.
149 **/
150 public static String substVars(
151 String val,
152 String currentKey,
153 Map<String, String> cycleMap,
154 Map<String, String> configProps,
155 Function<String, String> callback,
156 boolean substituteFromConfig,
157 boolean defaultsToEmptyString) {
158 return unescape(doSubstVars(
159 val, currentKey, cycleMap, configProps, callback, substituteFromConfig, defaultsToEmptyString));
160 }
161
162 private static String doSubstVars(
163 String val,
164 String currentKey,
165 Map<String, String> cycleMap,
166 Map<String, String> configProps,
167 Function<String, String> callback,
168 boolean substituteFromConfig,
169 boolean defaultsToEmptyString) {
170 if (cycleMap == null) {
171 cycleMap = new HashMap<>();
172 }
173
174 // Put the current key in the cycle map.
175 cycleMap.put(currentKey, currentKey);
176
177 // Assume we have a value that is something like:
178 // "leading ${foo.${bar}} middle ${baz} trailing"
179
180 // Find the first ending '}' variable delimiter, which
181 // will correspond to the first deepest nested variable
182 // placeholder.
183 int startDelim;
184 int stopDelim = -1;
185 do {
186 stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1);
187 while (stopDelim > 0 && val.charAt(stopDelim - 1) == ESCAPE_CHAR) {
188 stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1);
189 }
190
191 // Find the matching starting "${" variable delimiter
192 // by looping until we find a start delimiter that is
193 // greater than the stop delimiter we have found.
194 startDelim = val.indexOf(DELIM_START);
195 while (stopDelim >= 0) {
196 int idx = val.indexOf(DELIM_START, startDelim + DELIM_START.length());
197 if ((idx < 0) || (idx > stopDelim)) {
198 break;
199 } else if (idx < stopDelim) {
200 startDelim = idx;
201 }
202 }
203 } while (startDelim >= 0 && stopDelim >= 0 && stopDelim < startDelim + DELIM_START.length());
204
205 // If we do not have a start or stop delimiter, then just
206 // return the existing value.
207 if ((startDelim < 0) || (stopDelim < 0)) {
208 cycleMap.remove(currentKey);
209 return val;
210 }
211
212 // At this point, we have found a variable placeholder so
213 // we must perform a variable substitution on it.
214 // Using the start and stop delimiter indices, extract
215 // the first, deepest nested variable placeholder.
216 String variable = val.substring(startDelim + DELIM_START.length(), stopDelim);
217 String org = variable;
218
219 // Strip expansion modifiers
220 int idx1 = variable.lastIndexOf(":-");
221 int idx2 = variable.lastIndexOf(":+");
222 int idx = idx1 >= 0 && idx2 >= 0 ? Math.min(idx1, idx2) : idx1 >= 0 ? idx1 : idx2;
223 String op = null;
224 if (idx >= 0) {
225 op = variable.substring(idx);
226 variable = variable.substring(0, idx);
227 }
228
229 // Verify that this is not a recursive variable reference.
230 if (cycleMap.get(variable) != null) {
231 throw new IllegalArgumentException("recursive variable reference: " + variable);
232 }
233
234 String substValue = null;
235 // Get the value of the deepest nested variable placeholder.
236 // Try to configuration properties first.
237 if (substituteFromConfig && configProps != null) {
238 substValue = configProps.get(variable);
239 }
240 if (substValue == null) {
241 if (!variable.isEmpty()) {
242 if (callback != null) {
243 substValue = callback.apply(variable);
244 }
245 }
246 }
247
248 if (op != null) {
249 if (op.startsWith(":-")) {
250 if (substValue == null || substValue.isEmpty()) {
251 substValue = op.substring(":-".length());
252 }
253 } else if (op.startsWith(":+")) {
254 if (substValue != null && !substValue.isEmpty()) {
255 substValue = op.substring(":+".length());
256 }
257 } else {
258 throw new IllegalArgumentException("Bad substitution: ${" + org + "}");
259 }
260 }
261
262 if (substValue == null) {
263 if (defaultsToEmptyString) {
264 substValue = "";
265 } else {
266 // alters the original token to avoid infinite recursion
267 // altered tokens are reverted in substVarsPreserveUnresolved()
268 substValue = MARKER + "{" + variable + "}";
269 }
270 }
271
272 // Remove the found variable from the cycle map, since
273 // it may appear more than once in the value and we don't
274 // want such situations to appear as a recursive reference.
275 cycleMap.remove(variable);
276
277 // Append the leading characters, the substituted value of
278 // the variable, and the trailing characters to get the new
279 // value.
280 val = val.substring(0, startDelim) + substValue + val.substring(stopDelim + DELIM_STOP.length());
281
282 // Now perform substitution again, since there could still
283 // be substitutions to make.
284 val = doSubstVars(
285 val, currentKey, cycleMap, configProps, callback, substituteFromConfig, defaultsToEmptyString);
286
287 cycleMap.remove(currentKey);
288
289 // Return the value.
290 return val;
291 }
292
293 public static String escape(String val) {
294 return val.replace("$", MARKER);
295 }
296
297 private static String unescape(String val) {
298 val = val.replaceAll("\\" + MARKER, "\\$");
299 int escape = val.indexOf(ESCAPE_CHAR);
300 while (escape >= 0 && escape < val.length() - 1) {
301 char c = val.charAt(escape + 1);
302 if (c == '{' || c == '}' || c == ESCAPE_CHAR) {
303 val = val.substring(0, escape) + val.substring(escape + 1);
304 }
305 escape = val.indexOf(ESCAPE_CHAR, escape + 1);
306 }
307 return val;
308 }
309 }