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 }