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.shared.filtering; 20 21 import java.io.File; 22 import java.io.FileNotFoundException; 23 import java.io.IOException; 24 import java.io.InputStream; 25 import java.nio.file.Files; 26 import java.util.LinkedList; 27 import java.util.List; 28 import java.util.Properties; 29 30 import org.slf4j.Logger; 31 32 import static org.apache.maven.shared.filtering.FilteringUtils.isEmpty; 33 34 /** 35 * @author <a href="mailto:kenney@neonics.com">Kenney Westerhof</a> 36 * @author William Ferguson 37 */ 38 public final class PropertyUtils { 39 /** 40 * Private empty constructor to prevent instantiation. 41 */ 42 private PropertyUtils() { 43 // prevent instantiation 44 } 45 46 /** 47 * Reads a property file, resolving all internal variables, using the supplied base properties. 48 * <p> 49 * The properties are resolved iteratively, so if the value of property A refers to property B, then after 50 * resolution the value of property B will contain the value of property B. 51 * </p> 52 * 53 * @param propFile The property file to load. 54 * @param baseProps Properties containing the initial values to substitute into the properties file. 55 * @return Properties object containing the properties in the file with their values fully resolved. 56 * @throws IOException if profile does not exist, or cannot be read. 57 */ 58 public static Properties loadPropertyFile(File propFile, Properties baseProps) throws IOException { 59 return loadPropertyFile(propFile, baseProps, null); 60 } 61 62 /** 63 * Reads a property file, resolving all internal variables, using the supplied base properties. 64 * <p> 65 * The properties are resolved iteratively, so if the value of property A refers to property B, then after 66 * resolution the value of property B will contain the value of property B. 67 * </p> 68 * 69 * @param propFile The property file to load. 70 * @param baseProps Properties containing the initial values to substitute into the properties file. 71 * @param logger Logger instance 72 * @return Properties object containing the properties in the file with their values fully resolved. 73 * @throws IOException if profile does not exist, or cannot be read. 74 * 75 * @since 3.1.2 76 */ 77 public static Properties loadPropertyFile(File propFile, Properties baseProps, Logger logger) throws IOException { 78 if (!propFile.exists()) { 79 throw new FileNotFoundException(propFile.toString()); 80 } 81 82 final Properties fileProps = new Properties(); 83 84 try (InputStream inStream = Files.newInputStream(propFile.toPath())) { 85 fileProps.load(inStream); 86 } 87 88 final Properties combinedProps = new Properties(); 89 combinedProps.putAll(baseProps == null ? new Properties() : baseProps); 90 combinedProps.putAll(fileProps); 91 92 // The algorithm iterates only over the fileProps which is all that is required to resolve 93 // the properties defined within the file. This is slightly different to current, however 94 // I suspect that this was the actual original intent. 95 // 96 // The difference is that #loadPropertyFile(File, boolean, boolean) also resolves System properties 97 // whose values contain expressions. I believe this is unexpected and is not validated by the test cases, 98 // as can be verified by replacing the implementation of #loadPropertyFile(File, boolean, boolean) 99 // with the commented variant I have provided that reuses this method. 100 101 for (Object o : fileProps.keySet()) { 102 final String k = (String) o; 103 final String propValue = getPropertyValue(k, combinedProps, logger); 104 fileProps.setProperty(k, propValue); 105 } 106 107 return fileProps; 108 } 109 110 /** 111 * Reads a property file, resolving all internal variables. 112 * 113 * @param propfile The property file to load 114 * @param fail whether to throw an exception when the file cannot be loaded or to return null 115 * @param useSystemProps whether to incorporate System.getProperties settings into the returned Properties object. 116 * @return the loaded and fully resolved Properties object 117 * @throws IOException if profile does not exist, or cannot be read. 118 */ 119 public static Properties loadPropertyFile(File propfile, boolean fail, boolean useSystemProps) throws IOException { 120 return loadPropertyFile(propfile, fail, useSystemProps, null); 121 } 122 123 /** 124 * Reads a property file, resolving all internal variables. 125 * 126 * @param propfile The property file to load 127 * @param fail whether to throw an exception when the file cannot be loaded or to return null 128 * @param useSystemProps whether to incorporate System.getProperties settings into the returned Properties object. 129 * @param logger Logger instance 130 * @return the loaded and fully resolved Properties object 131 * @throws IOException if profile does not exist, or cannot be read. 132 * 133 * @since 3.1.2 134 */ 135 public static Properties loadPropertyFile(File propfile, boolean fail, boolean useSystemProps, Logger logger) 136 throws IOException { 137 138 final Properties baseProps = new Properties(); 139 140 if (useSystemProps) { 141 baseProps.putAll(System.getProperties()); 142 } 143 144 final Properties resolvedProps = new Properties(); 145 try { 146 resolvedProps.putAll(loadPropertyFile(propfile, baseProps, logger)); 147 } catch (FileNotFoundException e) { 148 if (fail) { 149 throw new FileNotFoundException(propfile.toString()); 150 } 151 } 152 153 if (useSystemProps) { 154 resolvedProps.putAll(baseProps); 155 } 156 157 return resolvedProps; 158 } 159 160 /** 161 * Retrieves a property value, replacing values like ${token} using the Properties to look them up. It will leave 162 * unresolved properties alone, trying for System properties, and implements reparsing (in the case that the value 163 * of a property contains a key), and will not loop endlessly on a pair like test = ${test}. 164 * 165 * @param k 166 * @param p 167 * @param logger Logger instance 168 * @return The filtered property value. 169 */ 170 private static String getPropertyValue(String k, Properties p, Logger logger) { 171 // This can also be done using InterpolationFilterReader, 172 // but it requires reparsing the file over and over until 173 // it doesn't change. 174 175 // for cycle detection 176 List<String> valueChain = new LinkedList<>(); 177 valueChain.add(k); 178 179 String v = p.getProperty(k); 180 String defaultValue = v; 181 StringBuilder ret = new StringBuilder(); 182 int idx, idx2; 183 184 while ((idx = v.indexOf("${")) >= 0) { 185 // append prefix to result 186 ret.append(v, 0, idx); 187 188 // strip prefix from original 189 v = v.substring(idx + 2); 190 191 // if no matching } then bail 192 idx2 = v.indexOf('}'); 193 if (idx2 < 0) { 194 break; 195 } 196 197 // strip out the key and resolve it 198 // resolve the key/value for the ${statement} 199 String nk = v.substring(0, idx2); 200 v = v.substring(idx2 + 1); 201 String nv = p.getProperty(nk); 202 203 if (valueChain.contains(nk)) { 204 if (logger != null) { 205 logCircularDetection(valueChain, nk, logger); 206 } 207 return defaultValue; 208 } else { 209 valueChain.add(nk); 210 211 // try global environment.. 212 if (nv == null && !isEmpty(nk)) { 213 nv = System.getProperty(nk); 214 } 215 216 // if the key cannot be resolved, 217 // leave it alone ( and don't parse again ) 218 // else prefix the original string with the 219 // resolved property ( so it can be parsed further ) 220 // taking recursion into account. 221 if (nv == null || k.equals(nk)) { 222 ret.append("${").append(nk).append("}"); 223 } else { 224 v = nv + v.replace("${" + nk + "}", nv); 225 } 226 } 227 } 228 229 return ret + v; 230 } 231 232 /** 233 * Logs the detected cycle in properties resolution 234 * @param valueChain the sequence of properties resolved so far 235 * @param nk the key the closes the cycle 236 * @param logger Logger instance 237 */ 238 private static void logCircularDetection(List<String> valueChain, String nk, Logger logger) { 239 StringBuilder sb = new StringBuilder("Circular reference between properties detected: "); 240 for (String key : valueChain) { 241 sb.append(key).append(" => "); 242 } 243 sb.append(nk); 244 logger.warn(sb.toString()); 245 } 246 }