View Javadoc
1   /**
2    * This file Copyright (c) 2011-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.init;
35  
36  import static info.magnolia.objectfactory.Components.getComponent;
37  import static org.apache.commons.collections4.CollectionUtils.isEmpty;
38  import static org.apache.commons.lang3.StringUtils.substringAfter;
39  
40  import info.magnolia.init.properties.EnvironmentPropertySource;
41  import info.magnolia.init.properties.FileSystemPropertySource;
42  import info.magnolia.init.properties.ServletContextPropertySource;
43  import info.magnolia.init.properties.SystemPropertySource;
44  
45  import java.io.FileNotFoundException;
46  import java.io.IOException;
47  import java.nio.file.Paths;
48  import java.util.ArrayList;
49  import java.util.Arrays;
50  import java.util.List;
51  import java.util.Optional;
52  import java.util.function.Supplier;
53  import java.util.stream.Collectors;
54  import java.util.stream.Stream;
55  
56  import javax.inject.Inject;
57  import javax.inject.Singleton;
58  import javax.servlet.ServletContext;
59  
60  import org.apache.commons.lang3.StringUtils;
61  import org.apache.commons.text.StringSubstitutor;
62  import org.apache.commons.text.lookup.StringLookup;
63  
64  import com.google.common.base.Suppliers;
65  
66  /**
67   * Resolves the paths of the properties files to load. The name of the file can be defined as a context parameter in
68   * web.xml. Multiple paths, comma separated, are supported (the first existing file in the list will be used), and the
69   * following variables will be used:
70   *
71   * <ul>
72   * <li>{@code ${servername}}: name of the server where the webapp is running, lowercase</li>
73   * <li>{@code ${webapp}}: the last token in the webapp path (e.g. {@code magnoliaPublic} for a webapp deployed at
74   * {@code tomcat/webapps/magnoliaPublic})</li>
75   * <li>{@code ${contextPath}}: the context path of the web application</li>
76   * <li>{@code ${systemProperty/*}} is replaced with the corresponding system property</li>
77   * <li>{@code ${env/*}} is replaced with the corresponding environment property}</li>
78   * <li>{@code ${contextAttribute/*}} is replaced with the corresponding attribute from servlet context</li>
79   * <li>{@code ${contextParam/*}} is replaced with the corresponding parameters from servlet context</li>
80   * <li>{@code ${*}} is replaced with the corresponding value looked up from the system properties, the environment
81   * variables, the context attributes and the context parameters in that order.</li>
82   * </ul>
83   *
84   * <p>
85   * If no {@code magnolia.initialization.file} context parameter is set a default is assumed, which depends
86   * on the presence of a {@link DefaultMagnoliaPropertiesResolver#MAGNOLIA_PROFILE_ENV MAGNOLIA_PROFILE} environment
87   * variable or system property:
88   * </p>
89   *
90   * If {@code MAGNOLIA_PROFILE} is not set {@link DefaultMagnoliaPropertiesResolver#DEFAULT_INITIALIZATION_PARAMETER}
91   * is used for a default:
92   * <pre>
93   * &lt;context-param>
94   *   &lt;param-name>magnolia.initialization.file&lt;/param-name>
95   *   &lt;param-value>
96   *      WEB-INF/config/${servername}/${contextPath}/magnolia.properties,
97   *      WEB-INF/config/${servername}/${webapp}/magnolia.properties,
98   *      WEB-INF/config/${servername}/magnolia.properties,
99   *      WEB-INF/config/${contextPath}/magnolia.properties,
100  *      WEB-INF/config/${webapp}/magnolia.properties,
101  *      WEB-INF/config/default/magnolia.properties,
102  *      WEB-INF/config/magnolia.properties"
103  *   &lt;/param-value>
104  * &lt;/context-param>
105  * </pre>
106  *
107  * If {@code MAGNOLIA_PROFILE} is set {@link DefaultMagnoliaPropertiesResolver#PROFILE_INITIALIZATION_PARAMETER}
108  * is used for a default:
109  * <pre>
110  * &lt;context-param>
111  *   &lt;param-name>magnolia.initialization.file&lt;/param-name>
112  *   &lt;param-value>
113  *      WEB-INF/config/${MAGNOLIA_PROFILE}/magnolia_${MAGNOLIA_INSTANCE_TYPE}_${MAGNOLIA_STAGE}.properties,"
114  *      WEB-INF/config/${MAGNOLIA_PROFILE}/magnolia_${MAGNOLIA_INSTANCE_TYPE}.properties,"
115  *      WEB-INF/config/${MAGNOLIA_PROFILE}/magnolia.properties,"
116  *      WEB-INF/config/shared/magnolia_${MAGNOLIA_INSTANCE_TYPE}_${MAGNOLIA_STAGE}.properties,"
117  *      WEB-INF/config/shared/magnolia_${MAGNOLIA_INSTANCE_TYPE}.properties,"
118  *      WEB-INF/config/shared/magnolia.properties,"
119  *      WEB-INF/config/default/magnolia_${MAGNOLIA_INSTANCE_TYPE}_${MAGNOLIA_STAGE}.properties,"
120  *      WEB-INF/config/default/magnolia_${MAGNOLIA_INSTANCE_TYPE}.properties,"
121  *      WEB-INF/config/default/magnolia.properties,"
122  *   &lt;/param-value>
123  * &lt;/context-param>
124  * </pre>
125  *
126  * <h3>Advanced usage: deployment service</h3>
127  *
128  * <p>
129  * Using the {@code ${servername}} and {@code ${webapp}} properties you can easily bundle in the same webapp
130  * different set of configurations which are automatically applied depending on the server name (useful for switching
131  * between development, test and production instances where the repository configuration need to be different) or the
132  * webapp name (useful to bundle both the public and admin log4j/jndi/bootstrap configuration in the same war).
133  * The values for the placeholders are provided by {@link MagnoliaInitPaths}. By default the initializer will try to
134  * search for the file in different location with different combination of {@code ${servername}}, {@code ${contextPath}}
135  * and {@code ${webapp}} in the following order of precedence:
136  * <ol>
137  * <li>{@code WEB-INF/config/${servername}/${contextPath}/magnolia.properties}</li>
138  * <li>{@code WEB-INF/config/${servername}/${webapp}/magnolia.properties}</li>
139  * <li>{@code WEB-INF/config/${servername}/magnolia.properties}</li>
140  * <li>{@code WEB-INF/config/${contextPath}/magnolia.properties}</li>
141  * <li>{@code WEB-INF/config/${webapp}/magnolia.properties}</li>
142  * <li>{@code WEB-INF/config/default/magnolia.properties}</li>
143  * <li>{@code WEB-INF/config/magnolia.properties}</li>
144  * </ol>
145  * </p>
146  * <p>
147  * <em><b>Deprecation note:</b> this configuration mode is deprecated and will be replaced by profile directories in
148  * future versions. See the following section.</em>
149  * </p>
150  * <br>
151  *
152  * <h3>Advanced usage: configuration profiles</h3>
153  * <p>
154  * Alternatively, when the {@code MAGNOLIA_PROFILE} system property or environment variable is set, its value is used to
155  * locate a directory at code {@code WEB_INF/config/${MAGNOLIA_PROFILE}}. Property files in that directory, the
156  * {@code WEB-INF/config/shared} directory and the {@code WEB-INF/config/default} directory are resolved in the
157  * following order of precedence:
158  * <ol>
159  * <li>{@code WEB-INF/config/${MAGNOLIA_PROFILE}/magnolia_${MAGNOLIA_INSTANCE_TYPE}_${MAGNOLIA_STAGE}.properties}</li>
160  * <li>{@code WEB-INF/config/${MAGNOLIA_PROFILE}/magnolia_${MAGNOLIA_INSTANCE_TYPE}.properties}</li>
161  * <li>{@code WEB-INF/config/${MAGNOLIA_PROFILE}/magnolia.properties}</li>
162  * <li>{@code WEB-INF/config/shared/magnolia_${MAGNOLIA_INSTANCE_TYPE}_${MAGNOLIA_STAGE}.properties}</li>
163  * <li>{@code WEB-INF/config/shared/magnolia_${MAGNOLIA_INSTANCE_TYPE}.properties}</li>
164  * <li>{@code WEB-INF/config/shared/magnolia.properties}</li>
165  * <li>{@code WEB-INF/config/default/magnolia_${MAGNOLIA_INSTANCE_TYPE}_${MAGNOLIA_STAGE}.properties}</li>
166  * <li>{@code WEB-INF/config/default/magnolia_${MAGNOLIA_INSTANCE_TYPE}.properties}</li>
167  * <li>{@code WEB-INF/config/default/magnolia.properties}</li>
168  * </ol>
169  * </p>
170  *
171  */
172 @Singleton
173 public class DefaultMagnoliaPropertiesResolver implements MagnoliaPropertiesResolver {
174     private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultMagnoliaPropertiesResolver.class);
175 
176     /**
177      * Context attribute prefix, to obtain a property definition like ${contextAttribute/property}, that can refer to
178      * any context attribute.
179      */
180     public static final String CONTEXT_ATTRIBUTE_PLACEHOLDER_PREFIX = "contextAttribute/";
181 
182     /**
183      * Context parameter prefix, to obtain a property definition like ${contextParam/property}, that can refer to any
184      * context parameter.
185      */
186     public static final String CONTEXT_PARAM_PLACEHOLDER_PREFIX = "contextParam/";
187 
188     /**
189      * System property prefix, to obtain a property definition like ${systemProperty/property}, that can refer to any
190      * System property.
191      */
192     public static final String SYSTEM_PROPERTY_PLACEHOLDER_PREFIX = "systemProperty/";
193 
194     /**
195      * System property prefix, to obtain a property definition like ${systemProperty/property}, that can refer to any
196      * System property.
197      */
198     public static final String ENV_PROPERTY_PLACEHOLDER_PREFIX = "env/";
199 
200     /**
201      * Context parameter name. Value should be a comma-separated list of paths to look for properties files.
202      * Defaults to {@value #DEFAULT_INITIALIZATION_PARAMETER}
203      */
204     protected static final String MAGNOLIA_INITIALIZATION_FILE = "magnolia.initialization.file";
205 
206     /**
207      * Default value for the MAGNOLIA_INITIALIZATION_FILE parameter,
208      * unless {@link #MAGNOLIA_PROFILE_ENV MAGNOLIA_PROFILE} is set.
209      *
210      * @deprecated Use {@link #PROFILE_INITIALIZATION_PARAMETER} instead
211      */
212     @Deprecated
213     protected static final String DEFAULT_INITIALIZATION_PARAMETER =
214             "WEB-INF/config/${servername}/${contextPath}/magnolia.properties,"
215                     + "WEB-INF/config/${servername}/${webapp}/magnolia.properties,"
216                     + "WEB-INF/config/${servername}/magnolia.properties,"
217                     + "WEB-INF/config/${contextPath}/magnolia.properties,"
218                     + "WEB-INF/config/${webapp}/magnolia.properties,"
219                     + "WEB-INF/config/default/magnolia.properties,"
220                     + "WEB-INF/config/magnolia.properties";
221 
222     /**
223      * Default value for the MAGNOLIA_INITIALIZATION_FILE parameter in profile based configuration.
224      * if {@link #MAGNOLIA_PROFILE_ENV MAGNOLIA_PROFILE} is set.
225      */
226     protected static final String PROFILE_INITIALIZATION_PARAMETER =
227             "WEB-INF/config/${MAGNOLIA_PROFILE}/magnolia_${MAGNOLIA_INSTANCE_TYPE}_${MAGNOLIA_STAGE}.properties," +
228             "WEB-INF/config/${MAGNOLIA_PROFILE}/magnolia_${MAGNOLIA_INSTANCE_TYPE}.properties," +
229             "WEB-INF/config/${MAGNOLIA_PROFILE}/magnolia.properties," +
230             "WEB-INF/config/shared/magnolia_${MAGNOLIA_INSTANCE_TYPE}_${MAGNOLIA_STAGE}.properties," +
231             "WEB-INF/config/shared/magnolia_${MAGNOLIA_INSTANCE_TYPE}.properties," +
232             "WEB-INF/config/shared/magnolia.properties," +
233             "WEB-INF/config/default/magnolia_${MAGNOLIA_INSTANCE_TYPE}_${MAGNOLIA_STAGE}.properties," +
234             "WEB-INF/config/default/magnolia_${MAGNOLIA_INSTANCE_TYPE}.properties," +
235             "WEB-INF/config/default/magnolia.properties,";
236 
237     /**
238      * Enable profile based configuration by setting {@code MAGNOLIA_PROFILE}.
239      */
240     protected static final String MAGNOLIA_PROFILE_ENV = "MAGNOLIA_PROFILE";
241 
242     private final ServletContext context;
243     private final MagnoliaInitPaths initPaths;
244     private final SystemPropertySource systemPropertySource;
245     private final EnvironmentPropertySource environmentPropertySource;
246     private final Supplier<List<String>> locations = Suppliers.memoize(this::resolveLocations);
247 
248     @Inject
249     public DefaultMagnoliaPropertiesResolver(ServletContext context, MagnoliaInitPaths initPaths, SystemPropertySource systemPropertySource, EnvironmentPropertySource environmentPropertySource) {
250         this.context = context;
251         this.initPaths = initPaths;
252         this.systemPropertySource = systemPropertySource;
253         this.environmentPropertySource = environmentPropertySource;
254     }
255 
256     /**
257      * @deprecated  use {@link #DefaultMagnoliaPropertiesResolver(ServletContext, MagnoliaInitPaths, SystemPropertySource, EnvironmentPropertySource)}  } instead.
258      */
259     @Deprecated
260     public DefaultMagnoliaPropertiesResolver(ServletContext context, MagnoliaInitPaths initPaths) {
261         this(context, initPaths, getComponent(SystemPropertySource.class), getComponent(EnvironmentPropertySource.class));
262     }
263 
264     protected String getConfigurationProfile() {
265         String profile = systemPropertySource.getProperty(MAGNOLIA_PROFILE_ENV);
266         return profile != null ? profile : environmentPropertySource.getProperty(MAGNOLIA_PROFILE_ENV);
267     }
268 
269     protected String getInitParameter(ServletContext ctx, String name, String defaultValue) {
270         final String propertiesFilesString = ctx.getInitParameter(name);
271         if (StringUtils.isEmpty(propertiesFilesString)) {
272             log.debug("{} value in web.xml is undefined, falling back to default: {}", name, defaultValue);
273             String profile = getConfigurationProfile();
274             if (profile == null) {
275                 log.info("Profile based configuration is disabled, falling back to configuration based on servername, webapp and contextPath");
276                 log.warn("This configuration mode is deprecated and will not be supported in future versions. " +
277                         "Consider migrating to profile directory based configuration.");
278             } else {
279                 String profileDir = "/WEB-INF/config/" + profile;
280                 if (isEmpty(ctx.getResourcePaths(profileDir))) {
281                     throw new RuntimeException("Invalid profile " + profile + ". Directory " + profileDir + " is empty or does not exist");
282                 } else {
283                     log.info("Profile based configuration is enabled. Profile is set to {}", profile);
284                 }
285             }
286             return defaultValue;
287         }
288         log.info("Found custom value of the {} context attribute in web.xml. " +
289                 "Resolving Magnolia property files from that location: {}", name, propertiesFilesString);
290         return propertiesFilesString;
291     }
292 
293     /**
294      * Used in tests, potentially in subclasses.
295      */
296     protected List<String> getLocations() {
297         return locations.get();
298     }
299 
300     private List<String> resolveLocations() {
301         String propertiesFilesString = getInitParameter(context, MAGNOLIA_INITIALIZATION_FILE,
302                 getConfigurationProfile() == null ? DEFAULT_INITIALIZATION_PARAMETER : PROFILE_INITIALIZATION_PARAMETER);
303 
304         return Arrays.stream(propertiesFilesString.trim().split("[,]+", 0))
305                 .map(String::trim)
306                 .map(this::interpolate)
307                 .filter(Optional::isPresent)  // Use Optional::stream once we bump above Java 8
308                 .map(Optional::get)
309                 .collect(Collectors.toList());
310     }
311 
312     @Override
313     public List<PropertySource> getSources() {
314         final List<PropertySource> sources = new ArrayList<PropertySource>();
315         boolean foundFiles = false;
316         for (String location : getLocations()) {
317             try {
318                 if (Paths.get(location).isAbsolute()) {
319                     sources.add(new FileSystemPropertySource(location));
320                 } else {
321                     sources.add(new ServletContextPropertySource(context, location));
322                 }
323                 foundFiles = true;
324             } catch (FileNotFoundException e) {
325                 log.debug("Configuration file not found with path [{}]", location);
326             } catch (IOException e) {
327                 throw new RuntimeException("Error while reading from the configuration file " + location, e);
328             }
329         }
330         if (!foundFiles) {
331             log.warn("No configuration files found using location list {}.", getLocations());
332         }
333 
334         return sources;
335     }
336 
337     /**
338      * Replace placeholders in configuration location strings with values looked up from
339      * {@link MagnoliaInitPaths init paths}, the {@link ServletContext servlet context}, system properties and
340      * environment variables.
341      * <p>
342      * <ul>
343      * <li>{@code ${servername}} is replaced with the host name</li>
344      * <li>{@code ${webapp}} is replaced with name of the web application</li>
345      * <li>{@code ${contextPath} is replace with the context path of the web application</li>
346      * <li>{@code ${systemProperty/*}} is replaced with the corresponding system property</li>
347      * <li>{@code ${env/*}} is replaced with the corresponding environment property}</li>
348      * <li>{@code ${contextAttribute/*}} is replaced with the corresponding attribute from servlet context</li>
349      * <li>{@code ${contextParam/*}} is replaced with the corresponding parameters from servlet context</li>
350      * <li>{@code ${*}} is replaced with the corresponding value looked up from the system properties, the environment
351      * variables, the context attributes and the context parameters in that order.</li>
352      * </ul>
353 
354      * @param source  configuration location to interpolate
355      * @return  the interpolated configuration location or {@code empty} if not all placeholders in {@code source}
356      *          could be resolved.
357      */
358     private Optional<String> interpolate(String source) {
359         Resolver resolver = new Resolver();
360         String interpolation = new StringSubstitutor(resolver).replace(source);
361         if (resolver.getUnresolvedKeys().isEmpty()) {
362             log.info("Resolved Magnolia property file location {} to {}", source, interpolation);
363             return Optional.of(interpolation);
364         } else {
365             log.debug("Could not resolve Magnolia property file locations {}. Missing keys: {}", source, resolver.getUnresolvedKeys());
366             return Optional.empty();
367         }
368     }
369 
370     private class Resolver implements StringLookup {
371             private List<String> unresolvedKeys = new ArrayList<>();
372 
373             Optional<String> getBasicProperty(String key) {
374                 switch (key) {
375                     case "servername":
376                         return Optional.ofNullable(initPaths.getServerName());
377                     case "webapp":
378                         return Optional.ofNullable(initPaths.getWebappFolderName());
379                     case "contextPath":
380                         // Use ROOT for the default (root) context, otherwise trim the leading slash to prevent double slashes
381                         String contextPath = initPaths.getContextPath();
382                         if (contextPath.length() == 0) {
383                             return Optional.of("ROOT");
384                         } else {
385                             return Optional.of(contextPath.substring(1));
386                         }
387                     default:
388                         return Optional.empty();
389                     }
390             }
391 
392             private boolean qualified(String key) {
393                 return key.contains("/");
394             }
395 
396             private Optional<String> stripPrefix(String key, String prefix) {
397                 if (key.startsWith(prefix)) {
398                     return Optional.of(substringAfter(key, prefix));
399                 } else if (!qualified(key)) {
400                     return Optional.of(key);
401                 } else {
402                     return Optional.empty();
403                 }
404             }
405 
406             private Optional<String> getContextAttribute(String key) {
407                 return stripPrefix(key, CONTEXT_ATTRIBUTE_PLACEHOLDER_PREFIX)
408                         .map(context::getAttribute)
409                         .map(Object::toString);
410             }
411 
412             private Optional<String> getContextParameter(String key) {
413                 return stripPrefix(key, CONTEXT_PARAM_PLACEHOLDER_PREFIX)
414                         .map(context::getInitParameter);
415             }
416 
417             private Optional<String> getSystemProperty(String key) {
418                 return stripPrefix(key, SYSTEM_PROPERTY_PLACEHOLDER_PREFIX)
419                         .filter(StringUtils::isNoneBlank)
420                         .map(systemPropertySource::getProperty);
421             }
422 
423             private Optional<String> getEnvironmentVariable(String key) {
424                 return stripPrefix(key, ENV_PROPERTY_PLACEHOLDER_PREFIX)
425                         .filter(StringUtils::isNoneBlank)
426                         .map(environmentPropertySource::getProperty);
427             }
428 
429             @Override
430             public String lookup(String key) {
431                 Optional<String> value = Stream.<Supplier<Optional<String>>>of(
432                         () -> getBasicProperty(key),
433                         () -> getContextAttribute(key),
434                         () -> getContextParameter(key),
435                         () -> getSystemProperty(key),
436                         () -> getEnvironmentVariable(key)
437                 )
438                         .map(Supplier::get)
439                         .filter(Optional::isPresent)  // Use Optional::stream once we bump above Java 8
440                         .map(Optional::get)
441                         .findFirst();
442 
443                 if (value.isPresent()) {
444                     return value.get();
445                 } else {
446                     unresolvedKeys.add(key);
447                     return null;
448                 }
449             }
450 
451             List<String> getUnresolvedKeys() {
452                 return unresolvedKeys;
453             }
454         }
455 
456 }