View Javadoc
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 }