1 package org.apache.maven.shared.filtering; 2 3 /* 4 * Licensed to the Apache Software Foundation (ASF) under one 5 * or more contributor license agreements. See the NOTICE file 6 * distributed with this work for additional information 7 * regarding copyright ownership. The ASF licenses this file 8 * to you under the Apache License, Version 2.0 (the 9 * "License"); you may not use this file except in compliance 10 * with the License. You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, 15 * software distributed under the License is distributed on an 16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 * KIND, either express or implied. See the License for the 18 * specific language governing permissions and limitations 19 * under the License. 20 */ 21 22 import java.io.File; 23 import java.io.FileNotFoundException; 24 import java.io.IOException; 25 import java.io.InputStream; 26 import java.nio.file.Files; 27 import java.util.LinkedList; 28 import java.util.List; 29 import java.util.Properties; 30 31 import org.apache.maven.shared.utils.StringUtils; 32 import org.codehaus.plexus.logging.Logger; 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 /** 41 * Private empty constructor to prevent instantiation. 42 */ 43 private PropertyUtils() 44 { 45 // prevent instantiation 46 } 47 48 /** 49 * Reads a property file, resolving all internal variables, using the supplied base properties. 50 * <p> 51 * The properties are resolved iteratively, so if the value of property A refers to property B, then after 52 * resolution the value of property B will contain the value of property B. 53 * </p> 54 * 55 * @param propFile The property file to load. 56 * @param baseProps Properties containing the initial values to substitute into the properties file. 57 * @return Properties object containing the properties in the file with their values fully resolved. 58 * @throws IOException if profile does not exist, or cannot be read. 59 */ 60 public static Properties loadPropertyFile( File propFile, Properties baseProps ) 61 throws IOException 62 { 63 return loadPropertyFile( propFile, baseProps, null ); 64 } 65 66 /** 67 * Reads a property file, resolving all internal variables, using the supplied base properties. 68 * <p> 69 * The properties are resolved iteratively, so if the value of property A refers to property B, then after 70 * resolution the value of property B will contain the value of property B. 71 * </p> 72 * 73 * @param propFile The property file to load. 74 * @param baseProps Properties containing the initial values to substitute into the properties file. 75 * @param logger Logger instance 76 * @return Properties object containing the properties in the file with their values fully resolved. 77 * @throws IOException if profile does not exist, or cannot be read. 78 * 79 * @since 3.1.2 80 */ 81 public static Properties loadPropertyFile( File propFile, Properties baseProps, Logger logger ) 82 throws IOException 83 { 84 if ( !propFile.exists() ) 85 { 86 throw new FileNotFoundException( propFile.toString() ); 87 } 88 89 final Properties fileProps = new Properties(); 90 91 try ( InputStream inStream = Files.newInputStream( propFile.toPath() ) ) 92 { 93 fileProps.load( inStream ); 94 } 95 96 final Properties combinedProps = new Properties(); 97 combinedProps.putAll( baseProps == null ? new Properties() : baseProps ); 98 combinedProps.putAll( fileProps ); 99 100 // The algorithm iterates only over the fileProps which is all that is required to resolve 101 // the properties defined within the file. This is slightly different to current, however 102 // I suspect that this was the actual original intent. 103 // 104 // The difference is that #loadPropertyFile(File, boolean, boolean) also resolves System properties 105 // whose values contain expressions. I believe this is unexpected and is not validated by the test cases, 106 // as can be verified by replacing the implementation of #loadPropertyFile(File, boolean, boolean) 107 // with the commented variant I have provided that reuses this method. 108 109 for ( Object o : fileProps.keySet() ) 110 { 111 final String k = (String) o; 112 final String propValue = getPropertyValue( k, combinedProps, logger ); 113 fileProps.setProperty( k, propValue ); 114 } 115 116 return fileProps; 117 } 118 119 /** 120 * Reads a property file, resolving all internal variables. 121 * 122 * @param propfile The property file to load 123 * @param fail whether to throw an exception when the file cannot be loaded or to return null 124 * @param useSystemProps whether to incorporate System.getProperties settings into the returned Properties object. 125 * @return the loaded and fully resolved Properties object 126 * @throws IOException if profile does not exist, or cannot be read. 127 */ 128 public static Properties loadPropertyFile( File propfile, boolean fail, boolean useSystemProps ) 129 throws IOException 130 { 131 return loadPropertyFile( propfile, fail, useSystemProps, null ); 132 } 133 134 /** 135 * Reads a property file, resolving all internal variables. 136 * 137 * @param propfile The property file to load 138 * @param fail whether to throw an exception when the file cannot be loaded or to return null 139 * @param useSystemProps whether to incorporate System.getProperties settings into the returned Properties object. 140 * @param logger Logger instance 141 * @return the loaded and fully resolved Properties object 142 * @throws IOException if profile does not exist, or cannot be read. 143 * 144 * @since 3.1.2 145 */ 146 public static Properties loadPropertyFile( File propfile, boolean fail, boolean useSystemProps, Logger logger ) 147 throws IOException 148 { 149 150 final Properties baseProps = new Properties(); 151 152 if ( useSystemProps ) 153 { 154 baseProps.putAll( System.getProperties() ); 155 } 156 157 final Properties resolvedProps = new Properties(); 158 try 159 { 160 resolvedProps.putAll( loadPropertyFile( propfile, baseProps, logger ) ); 161 } 162 catch ( FileNotFoundException e ) 163 { 164 if ( fail ) 165 { 166 throw new FileNotFoundException( propfile.toString() ); 167 } 168 } 169 170 if ( useSystemProps ) 171 { 172 resolvedProps.putAll( baseProps ); 173 } 174 175 return resolvedProps; 176 } 177 178 /** 179 * Retrieves a property value, replacing values like ${token} using the Properties to look them up. It will leave 180 * unresolved properties alone, trying for System properties, and implements reparsing (in the case that the value 181 * of a property contains a key), and will not loop endlessly on a pair like test = ${test}. 182 * 183 * @param k 184 * @param p 185 * @param logger Logger instance 186 * @return The filtered property value. 187 */ 188 private static String getPropertyValue( String k, Properties p, Logger logger ) 189 { 190 // This can also be done using InterpolationFilterReader, 191 // but it requires reparsing the file over and over until 192 // it doesn't change. 193 194 // for cycle detection 195 List<String> valueChain = new LinkedList<>(); 196 valueChain.add( k ); 197 198 String v = p.getProperty( k ); 199 String defaultValue = v; 200 String ret = ""; 201 int idx, idx2; 202 203 while ( ( idx = v.indexOf( "${" ) ) >= 0 ) 204 { 205 // append prefix to result 206 ret += v.substring( 0, idx ); 207 208 // strip prefix from original 209 v = v.substring( idx + 2 ); 210 211 // if no matching } then bail 212 idx2 = v.indexOf( '}' ); 213 if ( idx2 < 0 ) 214 { 215 break; 216 } 217 218 // strip out the key and resolve it 219 // resolve the key/value for the ${statement} 220 String nk = v.substring( 0, idx2 ); 221 v = v.substring( idx2 + 1 ); 222 String nv = p.getProperty( nk ); 223 224 if ( valueChain.contains( nk ) ) 225 { 226 if ( logger != null ) 227 { 228 logCircularDetection( valueChain, nk, logger ); 229 } 230 return defaultValue; 231 } 232 else 233 { 234 valueChain.add( nk ); 235 236 // try global environment.. 237 if ( nv == null && !StringUtils.isEmpty( nk ) ) 238 { 239 nv = System.getProperty( nk ); 240 } 241 242 // if the key cannot be resolved, 243 // leave it alone ( and don't parse again ) 244 // else prefix the original string with the 245 // resolved property ( so it can be parsed further ) 246 // taking recursion into account. 247 if ( nv == null || nv.equals( k ) || k.equals( nk ) ) 248 { 249 ret += "${" + nk + "}"; 250 } 251 else 252 { 253 v = nv + v; 254 } 255 } 256 } 257 258 return ret + v; 259 } 260 261 /** 262 * Logs the detected cycle in properties resolution 263 * @param valueChain the sequence of properties resolved so far 264 * @param nk the key the closes the cycle 265 * @param logger Logger instance 266 */ 267 private static void logCircularDetection( List<String> valueChain, String nk, Logger logger ) 268 { 269 StringBuilder sb = new StringBuilder( "Circular reference between properties detected: " ); 270 for ( String key : valueChain ) 271 { 272 sb.append( key ).append( " => " ); 273 } 274 sb.append( nk ); 275 logger.warn( sb.toString() ); 276 } 277 }