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 }