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