View Javadoc

1   /*
2    * Copyright 2012-2013 smartics, Kronseder & Reiner GmbH
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package de.smartics.properties.spi.config.support;
17  
18  import java.io.BufferedInputStream;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.net.MalformedURLException;
22  import java.net.URL;
23  import java.net.URLClassLoader;
24  import java.util.ArrayList;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Map.Entry;
31  import java.util.Properties;
32  import java.util.Set;
33  
34  import javax.annotation.concurrent.NotThreadSafe;
35  
36  import org.apache.commons.io.IOUtils;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  import de.smartics.properties.api.config.app.BootProperties;
41  import de.smartics.properties.api.config.domain.CompoundConfigurationException;
42  import de.smartics.properties.api.config.domain.ConfigurationException;
43  import de.smartics.properties.api.config.domain.ConfigurationPropertiesManagement;
44  import de.smartics.properties.api.config.domain.ConfigurationRepositoryManagement;
45  import de.smartics.properties.api.config.domain.Property;
46  import de.smartics.properties.api.config.domain.PropertyLocation;
47  import de.smartics.properties.api.config.domain.PropertyProvider;
48  import de.smartics.properties.api.config.domain.key.ConfigurationKey;
49  import de.smartics.properties.api.core.domain.PropertiesContext;
50  import de.smartics.properties.api.core.domain.PropertyDescriptor;
51  import de.smartics.properties.api.core.domain.PropertyDescriptorRegistry;
52  import de.smartics.properties.spi.config.definition.DefinitionKeyHelper;
53  import de.smartics.properties.spi.core.classpath.PropertiesFilesLoader;
54  import de.smartics.properties.spi.core.classpath.PropertySetClassesLoader;
55  import de.smartics.properties.spi.core.metadata.PropertyMetaDataParser;
56  import de.smartics.properties.spi.core.util.ClassLoaderUtils;
57  import de.smartics.util.lang.Arguments;
58  import de.smartics.util.lang.ClassPathContext;
59  import de.smartics.util.lang.NullArgumentException;
60  
61  /**
62   * Loads property descriptors and properties files found on the class path.
63   *
64   * @param <T> the concrete type of the returned configuration properties.
65   */
66  @NotThreadSafe
67  public final class ClassPathLoader<T extends ConfigurationPropertiesManagement>
68  { // NOPMD
69    // ********************************* Fields *********************************
70  
71    // --- constants ------------------------------------------------------------
72  
73    /**
74     * Reference to the logger for this class.
75     */
76    private static final Logger LOG = LoggerFactory
77        .getLogger(ClassPathLoader.class);
78  
79    // --- members --------------------------------------------------------------
80  
81    /**
82     * The cache to create and cache new configuration properties instances.
83     */
84    private final FactoryCache<T> factoryCache;
85  
86    /**
87     * The class root URLs to search for property descriptors and properties
88     * files.
89     */
90    private final Collection<URL> rootUrls = new ArrayList<URL>();
91  
92    /**
93     * The additional properties added by other means than loading them from the
94     * class path.
95     */
96    private final Collection<PropertyProvider> rootPropertyProviders =
97        new ArrayList<PropertyProvider>();
98  
99    /**
100    * The flag indicates that configuration problems are not signaled by
101    * exceptions.
102    */
103   private final boolean lenient;
104 
105   /**
106    * The flag indicates that loading properties form the class path is to be
107    * skipped.
108    */
109   private final boolean skipPropertyLoading;
110 
111   // ****************************** Initializer *******************************
112 
113   // ****************************** Constructors ******************************
114 
115   /**
116    * Default constructor.
117    *
118    * @param factoryCache the cache to create and cache new configuration
119    *          properties instances.
120    * @param lenient the flag indicates that configuration problems are not
121    *          signaled by exceptions.
122    * @param skipPropertyLoading the flag indicates that loading properties from
123    *          the class path is to be skipped.
124    * @throws NullArgumentException if {@code factoryCache} is <code>null</code>
125    *           .
126    */
127   public ClassPathLoader(final FactoryCache<T> factoryCache,
128       final boolean lenient, final boolean skipPropertyLoading)
129     throws NullArgumentException
130   {
131     Arguments.checkNotNull("factoryCache", factoryCache);
132 
133     this.factoryCache = factoryCache;
134     this.lenient = lenient;
135     this.skipPropertyLoading = skipPropertyLoading;
136   }
137 
138   // ****************************** Inner Classes *****************************
139 
140   // ********************************* Methods ********************************
141 
142   // --- init -----------------------------------------------------------------
143 
144   // --- get&set --------------------------------------------------------------
145 
146   /**
147    * Adds the given URL to the set of class path root URLs.
148    *
149    * @param rootUrl the URL to add as a class path root URL.
150    * @throws NullArgumentException if {@code rootUrl} is <code>null</code>.
151    */
152   public void addRootUrl(final URL rootUrl) throws NullArgumentException
153   {
154     Arguments.checkNotNull("rootUrl", rootUrl);
155 
156     if (!rootUrls.contains(rootUrl))
157     {
158       rootUrls.add(rootUrl);
159     }
160   }
161 
162   /**
163    * Adds the root URL of the given {@code exemplar} to the set of class path
164    * root URLs.
165    *
166    * @param exemplar a sample class to derive the root URL from.
167    * @throws NullArgumentException if {@code exemplar} is <code>null</code>.
168    */
169   public void addRootUrl(final Class<?> exemplar) throws NullArgumentException
170   {
171     Arguments.checkNotNull("exemplar", exemplar);
172     addRootUrl(Thread.currentThread().getContextClassLoader());
173   }
174 
175   /**
176    * Adds the relevant root URL of the given {@code classLoader} to the set of
177    * class path root URLs.
178    * <p>
179    * Only archives that contain the
180    * <code>{@value de.smartics.properties.api.core.domain.PropertiesContext#META_INF_HOME}</code>
181    * are relevant and therefore added.
182    * </p>
183    *
184    * @param classLoader the class loader whose class roots are added.
185    * @throws NullArgumentException if {@code exemplar} is <code>null</code>.
186    */
187   public void addRootUrl(final ClassLoader classLoader)
188     throws NullArgumentException
189   {
190     Arguments.checkNotNull("classLoader", classLoader);
191 
192     try
193     {
194       addResources(classLoader);
195     }
196     catch (final IOException e)
197     {
198       LOG.warn("Cannot determine class path roots for the given class loader.",
199           e);
200     }
201   }
202 
203   private void addResources(final ClassLoader classLoader) throws IOException
204   {
205     for (final URL url : Collections.list(classLoader
206         .getResources(PropertiesContext.META_INF_HOME)))
207     {
208       final URL rootUrl = truncateUrl(url);
209       addRootUrl(rootUrl);
210     }
211   }
212 
213   private static URL truncateUrl(final URL url)
214   {
215     final String urlString = url.toExternalForm();
216     final boolean isJar = "jar".equals(url.getProtocol());
217     final int last;
218     if (isJar)
219     {
220       last = urlString.indexOf('!') + 2;
221     }
222     else
223     {
224       last = urlString.length() - PropertiesContext.META_INF_HOME.length() - 1;
225     }
226 
227     final String urlStringTruncated = urlString.substring(0, last);
228     URL urlTruncated;
229     try
230     {
231       urlTruncated = new URL(urlStringTruncated);
232       return urlTruncated;
233     }
234     catch (final MalformedURLException e)
235     {
236       LOG.warn("Cannot use URL '{}' in its truncated form '{}'.", url,
237           urlStringTruncated);
238       return url;
239     }
240   }
241 
242   // --- business -------------------------------------------------------------
243 
244   /**
245    * Loads the configuration properties instance from information found on the
246    * class path.
247    *
248    * @return the loaded configuration properties instance.
249    * @throws CompoundConfigurationException if loading encountered problems.
250    */
251   public ConfigurationRepositoryManagement load()
252     throws CompoundConfigurationException
253   {
254     final Set<Class<?>> propertyDescriptorTypes = loadPropertyDescriptors();
255     final Map<Class<?>, List<PropertyDescriptor>> descriptors =
256         calcDescriptors(propertyDescriptorTypes);
257     factoryCache.registerDescriptors(descriptors);
258 
259     final MultiSourcePropertiesManager propertiesManager = loadProperties();
260 
261     for (final MultiSourceProperties properties : propertiesManager
262         .getProperties())
263     {
264       final List<ConfigurationException> exceptions =
265           properties.getExceptions();
266       if (!exceptions.isEmpty())
267       {
268         throw new CompoundConfigurationException(
269             properties.getConfigurationKey(), exceptions);
270       }
271 
272       addProperties(descriptors, properties);
273     }
274 
275     return factoryCache.getCache();
276   }
277 
278   private Set<Class<?>> loadPropertyDescriptors()
279   {
280     final PropertySetClassesLoader loader = new PropertySetClassesLoader();
281     final Set<Class<?>> propertyDescriptorTypes =
282         loader.getPropertySetTypes(rootUrls);
283     return propertyDescriptorTypes;
284   }
285 
286   private MultiSourcePropertiesManager loadProperties()
287   {
288     final MultiSourcePropertiesManager allPropertiesManager =
289         new MultiSourcePropertiesManager(lenient, rootPropertyProviders);
290 
291     if (!skipPropertyLoading)
292     {
293       final PropertiesFilesLoader loader = new PropertiesFilesLoader();
294       LOG.debug("Loading properties/Root location URLs: {}", rootUrls);
295       final Set<String> propertiesFiles = loader.getPropertiesFiles(rootUrls);
296 
297       final ClassLoader classLoader =
298           new URLClassLoader(rootUrls.toArray(new URL[rootUrls.size()]), Thread
299               .currentThread().getContextClassLoader());
300       for (final String propertiesFile : propertiesFiles)
301       {
302         if (propertiesFile.contains("META-INF"))
303         {
304           continue;
305         }
306 
307         final ClassPathContext context =
308             ClassLoaderUtils
309                 .createClassPathContext(classLoader, propertiesFile);
310         final DefinitionKeyHelper helper =
311             allPropertiesManager.getDefinition(context);
312         if (helper != null)
313         {
314           addProperties(allPropertiesManager, classLoader, propertiesFile,
315               helper);
316         }
317         else
318         {
319           LOG.warn("Skipping '" + propertiesFile + "' since no '"
320                    + PropertiesContext.META_INF_HOME + "' provided.");
321         }
322       }
323     }
324     else
325     {
326       allPropertiesManager.create();
327     }
328 
329     return allPropertiesManager;
330   }
331 
332   private void addProperties(
333       final MultiSourcePropertiesManager allPropertiesManager,
334       final ClassLoader classLoader, final String propertiesFile,
335       final DefinitionKeyHelper helper) throws IllegalArgumentException
336   {
337     final ConfigurationKey key = helper.parse(propertiesFile);
338     final MultiSourceProperties allProperties =
339         allPropertiesManager.create(key);
340 
341     final PropertyLocation location =
342         new PropertyLocationHelper().createPropertyLocation(classLoader,
343             propertiesFile);
344     final Properties properties = loadProperties(classLoader, propertiesFile);
345     allProperties.add(location, properties);
346   }
347 
348   private Properties loadProperties(final ClassLoader classLoader,
349       final String propertiesFile)
350   {
351     // TODO: There is a similar method in PropertiesUtils: Refactor
352     final Properties properties = new Properties();
353     InputStream in = classLoader.getResourceAsStream(propertiesFile);
354     try
355     {
356       if (in != null)
357       {
358         in = new BufferedInputStream(in);
359         properties.load(in);
360       }
361       else
362       {
363         LOG.warn("Cannot find properties '" + propertiesFile
364                  + "' in class path.");
365       }
366     }
367     catch (final IOException e)
368     {
369       LOG.warn("Cannot load properties from '" + propertiesFile + "'.");
370     }
371     finally
372     {
373       IOUtils.closeQuietly(in);
374     }
375 
376     return properties;
377   }
378 
379   private Map<Class<?>, List<PropertyDescriptor>> calcDescriptors(
380       final Set<Class<?>> propertyDescriptorTypes)
381   {
382     final Map<Class<?>, List<PropertyDescriptor>> map =
383         new HashMap<Class<?>, List<PropertyDescriptor>>();
384 
385     for (final Class<?> type : propertyDescriptorTypes)
386     {
387       final PropertiesContext context = factoryCache.getContext(type);
388       if (context == null)
389       {
390         LOG.debug("Cannot find context for type '" + type.getName()
391                   + "'. Skipping.");
392         continue;
393       }
394       final PropertyMetaDataParser propertyDescriptorParser =
395           PropertyMetaDataParser.create(context);
396       final List<PropertyDescriptor> descriptors =
397           propertyDescriptorParser.readDescriptors(type);
398       map.put(type, descriptors);
399     }
400 
401     return map;
402   }
403 
404   private void addProperties(
405       final Map<Class<?>, List<PropertyDescriptor>> descriptorMap,
406       final MultiSourceProperties compositeProperties)
407   {
408     final Properties properties = new Properties();
409 
410     for (final Entry<Class<?>, List<PropertyDescriptor>> entry : descriptorMap
411         .entrySet())
412     {
413       final List<PropertyDescriptor> descriptors = entry.getValue();
414 
415       for (final PropertyDescriptor descriptor : descriptors)
416       {
417         final String propertyKey = descriptor.getKey().toString();
418         final Property property = compositeProperties.getValue(propertyKey);
419         if (property != null && property.getValue() != null)
420         {
421           properties.put(propertyKey, property);
422         }
423       }
424 
425       final ConfigurationKey key = compositeProperties.getConfigurationKey();
426       final ConfigurationPropertiesManagement configuration =
427           factoryCache.ensureManagement(key);
428       configuration.addDefinitions(properties);
429     }
430   }
431 
432   /**
433    * Adds all class path root URLs provided by the context class loader of the
434    * current thread.
435    *
436    * @param registry the registry to resolve property descriptors.
437    * @throws NullPointerException of either {@code key} or {@code registry} is
438    *           <code>null</code>.
439    */
440   public void addDefaultRootUrls(final PropertyDescriptorRegistry registry)
441     throws NullPointerException
442   {
443     Arguments.checkNotNull("registry", registry);
444 
445     addBootRootUrls(registry);
446     addRootUrl(Thread.currentThread().getContextClassLoader());
447   }
448 
449   private void addBootRootUrls(final PropertyDescriptorRegistry registry)
450   {
451     final BootConfigurationProperties initBootConfiguration =
452         new BootConfigurationProperties(registry);
453     final BootLoader bootLoader =
454         new BootLoader(initBootConfiguration, Thread.currentThread()
455             .getContextClassLoader());
456     final ConfigurationPropertiesManagement bootConfiguration =
457         bootLoader.loadAndValidate();
458     final BootProperties confProperties =
459         bootConfiguration.getProperties(BootProperties.class);
460     final List<URL> urls = confProperties.additionalPropertiesLocations();
461     if (urls != null)
462     {
463       for (final URL url : urls)
464       {
465         final URL normalized = normalize(url);
466         addRootUrl(normalized);
467       }
468     }
469   }
470 
471   private static URL normalize(final URL url)
472   {
473     final String urlString = url.toExternalForm();
474     if (urlString.indexOf(urlString.length() - 1) != '/')
475     {
476       try
477       {
478         return new URL(urlString + '/');
479       }
480       catch (final MalformedURLException e)
481       {
482         LOG.warn("Cannot append '/' to '" + urlString
483                  + "' for normalization. Using unnormalized URL instead.", e);
484       }
485     }
486     return url;
487   }
488 
489   /**
490    * Adds the given root URLs to the collection of class path roots managed by
491    * this class loader. The root locations are used for searching property
492    * declarations and property definitions.
493    *
494    * @param rootLocations the additional root locations to search for property
495    *          declarations and definitions.
496    */
497   public void addRootUrls(final List<URL> rootLocations)
498   {
499     this.rootUrls.addAll(rootLocations);
500   }
501 
502   /**
503    * Adds the given property providers as additional property definitions.
504    * Values provided by these instances are taken into account similar to any
505    * property definitions found on the class path.
506    * <p>
507    * Property values provided by these providers take precedence over any
508    * property values found on the class path.
509    * </p>
510    *
511    * @param rootPropertyProviders the additional property definitions to add.
512    */
513   public void addRootProperties(
514       final List<PropertyProvider> rootPropertyProviders)
515   {
516     this.rootPropertyProviders.addAll(rootPropertyProviders);
517   }
518 
519   // --- object basics --------------------------------------------------------
520 
521 }