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.api.core.security.PropertyValueSecurity;
53  import de.smartics.properties.spi.config.definition.DefinitionKeyHelper;
54  import de.smartics.properties.spi.core.classpath.PropertiesFilesLoader;
55  import de.smartics.properties.spi.core.classpath.PropertySetClassesLoader;
56  import de.smartics.properties.spi.core.metadata.PropertyMetaDataParser;
57  import de.smartics.properties.spi.core.registry.InMemoryPropertyDescriptorRegistry;
58  import de.smartics.properties.spi.core.util.ClassLoaderUtils;
59  import de.smartics.util.lang.Arg;
60  import de.smartics.util.lang.NullArgumentException;
61  import de.smartics.util.lang.classpath.ClassPathContext;
62  
63  /**
64   * Loads property descriptors and properties files found on the class path.
65   *
66   * @param <T> the concrete type of the returned configuration properties.
67   */
68  @NotThreadSafe
69  public final class ClassPathLoader<T extends ConfigurationPropertiesManagement>
70  { // NOPMD
71    // ********************************* Fields *********************************
72  
73    // --- constants ------------------------------------------------------------
74  
75    /**
76     * Reference to the logger for this class.
77     */
78    private static final Logger LOG = LoggerFactory
79        .getLogger(ClassPathLoader.class);
80  
81    // --- members --------------------------------------------------------------
82  
83    /**
84     * The cache to create and cache new configuration properties instances.
85     */
86    private final FactoryCache<T> factoryCache;
87  
88    /**
89     * The class root URLs to search for property descriptors and properties
90     * files.
91     */
92    private final Collection<URL> rootUrls = new ArrayList<URL>();
93  
94    /**
95     * The root URLs to search for additional properties files. These URLs are
96     * always considered for properties files.
97     */
98    private final Collection<URL> additionalRootUrls = new ArrayList<URL>();
99  
100   /**
101    * The additional properties added by other means than loading them from the
102    * class path.
103    */
104   private final Collection<PropertyProvider> rootPropertyProviders =
105       new ArrayList<PropertyProvider>();
106 
107   /**
108    * The flag indicates that configuration problems are not signaled by
109    * exceptions.
110    */
111   private final boolean lenient;
112 
113   /**
114    * The flag indicates that loading properties form the class path is to be
115    * skipped.
116    */
117   private final boolean skipPropertyLoading;
118 
119   /**
120    * The helper to decrypt secured property values.
121    */
122   private final PropertyValueSecurity decrypter;
123 
124   // ****************************** Initializer *******************************
125 
126   // ****************************** Constructors ******************************
127 
128   /**
129    * Default constructor.
130    *
131    * @param factoryCache the cache to create and cache new configuration
132    *          properties instances.
133    * @param lenient the flag indicates that configuration problems are not
134    *          signaled by exceptions.
135    * @param skipPropertyLoading the flag indicates that loading properties from
136    *          the class path is to be skipped.
137    * @param decrypter the helper to decrypt secured property values.
138    * @throws NullArgumentException if {@code factoryCache} or {@code decrypter}
139    *           is <code>null</code>.
140    */
141   public ClassPathLoader(final FactoryCache<T> factoryCache,
142       final boolean lenient, final boolean skipPropertyLoading,
143       final PropertyValueSecurity decrypter) throws NullArgumentException
144   {
145     this.factoryCache = Arg.checkNotNull("factoryCache", factoryCache);
146     this.lenient = lenient;
147     this.skipPropertyLoading = skipPropertyLoading;
148     this.decrypter = Arg.checkNotNull("decrypter", decrypter);
149   }
150 
151   // ****************************** Inner Classes *****************************
152 
153   // ********************************* Methods ********************************
154 
155   // --- init -----------------------------------------------------------------
156 
157   // --- get&set --------------------------------------------------------------
158 
159   /**
160    * Adds the given URL to the set of class path root URLs.
161    *
162    * @param rootUrl the URL to add as a class path root URL.
163    * @throws NullArgumentException if {@code rootUrl} is <code>null</code>.
164    */
165   public void addRootUrl(final URL rootUrl) throws NullArgumentException
166   {
167     Arg.checkNotNull("rootUrl", rootUrl);
168 
169     if (!rootUrls.contains(rootUrl))
170     {
171       rootUrls.add(rootUrl);
172     }
173   }
174 
175   /**
176    * Adds the root URL of the given {@code exemplar} to the set of class path
177    * root URLs.
178    *
179    * @param exemplar a sample class to derive the root URL from.
180    * @throws NullArgumentException if {@code exemplar} is <code>null</code>.
181    */
182   public void addRootUrl(final Class<?> exemplar) throws NullArgumentException
183   {
184     Arg.checkNotNull("exemplar", exemplar);
185     addRootUrl(Thread.currentThread().getContextClassLoader());
186   }
187 
188   /**
189    * Adds the relevant root URL of the given {@code classLoader} to the set of
190    * class path root URLs.
191    * <p>
192    * Only archives that contain the
193    * <code>{@value de.smartics.properties.api.core.domain.PropertiesContext#META_INF_HOME}</code>
194    * are relevant and therefore added.
195    * </p>
196    *
197    * @param classLoader the class loader whose class roots are added.
198    * @throws NullArgumentException if {@code exemplar} is <code>null</code>.
199    */
200   public void addRootUrl(final ClassLoader classLoader)
201     throws NullArgumentException
202   {
203     Arg.checkNotNull("classLoader", classLoader);
204 
205     try
206     {
207       addResources(classLoader);
208     }
209     catch (final IOException e)
210     {
211       LOG.warn("Cannot determine class path roots for the given class loader.",
212           e);
213     }
214   }
215 
216   private void addResources(final ClassLoader classLoader) throws IOException
217   {
218     for (final URL url : Collections.list(classLoader
219         .getResources(PropertiesContext.META_INF_HOME)))
220     {
221       final URL rootUrl = truncateUrl(url);
222       addRootUrl(rootUrl);
223     }
224   }
225 
226   private static URL truncateUrl(final URL url)
227   {
228     final String urlString = url.toExternalForm();
229     final boolean isJar = "jar".equals(url.getProtocol());
230     final int last;
231     if (isJar)
232     {
233       last = urlString.indexOf('!') + 2;
234     }
235     else
236     {
237       last = urlString.length() - PropertiesContext.META_INF_HOME.length() - 1;
238     }
239 
240     final String urlStringTruncated = urlString.substring(0, last);
241     URL urlTruncated;
242     try
243     {
244       urlTruncated = new URL(urlStringTruncated);
245       return urlTruncated;
246     }
247     catch (final MalformedURLException e)
248     {
249       LOG.warn("Cannot use URL '{}' in its truncated form '{}'.", url,
250           urlStringTruncated);
251       return url;
252     }
253   }
254 
255   // --- business -------------------------------------------------------------
256 
257   /**
258    * Loads the configuration properties instance from information found on the
259    * class path.
260    *
261    * @return the loaded configuration properties instance.
262    * @throws CompoundConfigurationException if loading encountered problems.
263    */
264   public ConfigurationRepositoryManagement load()
265     throws CompoundConfigurationException
266   {
267     final Set<Class<?>> propertyDescriptorTypes = loadPropertyDescriptors();
268     final Map<Class<?>, List<PropertyDescriptor>> descriptors =
269         calcDescriptors(propertyDescriptorTypes);
270     factoryCache.registerDescriptors(descriptors);
271 
272     final MultiSourcePropertiesManager propertiesManager = loadProperties();
273     try
274     {
275       for (final MultiSourceProperties properties : propertiesManager
276           .getProperties())
277       {
278         final List<ConfigurationException> exceptions =
279             properties.getExceptions();
280         if (!exceptions.isEmpty())
281         {
282           throw new CompoundConfigurationException(
283               properties.getConfigurationKey(), exceptions);
284         }
285 
286         addProperties(descriptors, properties);
287       }
288     }
289     finally
290     {
291       propertiesManager.release();
292     }
293 
294     return factoryCache.getCache();
295   }
296 
297   private Set<Class<?>> loadPropertyDescriptors()
298   {
299     final PropertySetClassesLoader loader = new PropertySetClassesLoader();
300     final Set<Class<?>> propertyDescriptorTypes =
301         loader.getPropertySetTypes(rootUrls);
302     return propertyDescriptorTypes;
303   }
304 
305   private MultiSourcePropertiesManager loadProperties()
306   {
307     final MultiSourcePropertiesManager allPropertiesManager =
308         new MultiSourcePropertiesManager(lenient, rootPropertyProviders);
309 
310     loadProperties(allPropertiesManager);
311 
312     if (skipPropertyLoading)
313     {
314       allPropertiesManager.create();
315     }
316 
317     return allPropertiesManager;
318   }
319 
320   private void loadProperties(
321       final MultiSourcePropertiesManager allPropertiesManager)
322   {
323     final Collection<URL> propertiesUrls = createPropertiesUrls();
324     if (!propertiesUrls.isEmpty())
325     {
326       final PropertiesFilesLoader loader = new PropertiesFilesLoader();
327       LOG.debug("Loading properties/Root location URLs: {}", propertiesUrls);
328       final Set<String> propertiesFiles =
329           loader.getPropertiesFiles(propertiesUrls);
330 
331       final ClassLoader classLoader =
332           new URLClassLoader(propertiesUrls.toArray(new URL[propertiesUrls
333               .size()]), Thread.currentThread().getContextClassLoader());
334       for (final String propertiesFile : propertiesFiles)
335       {
336         if (propertiesFile.contains("META-INF"))
337         {
338           continue;
339         }
340 
341         final ClassPathContext context =
342             ClassLoaderUtils
343                 .createClassPathContext(classLoader, propertiesFile);
344 
345         if (!isDefintionsArchive(context))
346         {
347           continue;
348         }
349 
350         final DefinitionKeyHelper helper =
351             allPropertiesManager.getDefinition(context);
352         if (helper != null)
353         {
354           addProperties(allPropertiesManager, classLoader, propertiesFile,
355               helper);
356         }
357         else
358         {
359           LOG.warn("Skipping '" + propertiesFile + "' since no '"
360                    + PropertiesContext.META_INF_HOME + "' provided.");
361         }
362       }
363     }
364   }
365 
366   private static boolean isDefintionsArchive(final ClassPathContext context)
367   {
368     final URL url = context.getResource(PropertiesContext.DEFINITION_FILE);
369     return url != null;
370   }
371 
372   private Collection<URL> createPropertiesUrls()
373   {
374     if (skipPropertyLoading)
375     {
376       return this.additionalRootUrls;
377     }
378     else
379     {
380       final Collection<URL> urls =
381           new ArrayList<URL>(additionalRootUrls.size() + rootUrls.size());
382       urls.addAll(additionalRootUrls);
383       urls.addAll(rootUrls);
384       return urls;
385     }
386   }
387 
388   private void addProperties(
389       final MultiSourcePropertiesManager allPropertiesManager,
390       final ClassLoader classLoader, final String propertiesFile,
391       final DefinitionKeyHelper helper) throws IllegalArgumentException
392   {
393     final ConfigurationKey<?> key = helper.parse(propertiesFile);
394     final MultiSourceProperties allProperties =
395         allPropertiesManager.create(key);
396 
397     final PropertyLocation location =
398         new PropertyLocationHelper().createPropertyLocation(classLoader,
399             propertiesFile);
400     final Properties properties = loadProperties(classLoader, propertiesFile);
401     allProperties.add(location, properties);
402   }
403 
404   private Properties loadProperties(final ClassLoader classLoader,
405       final String propertiesFile)
406   {
407     // TODO: There is a similar method in PropertiesHelper: Refactor
408     final Properties properties = new Properties();
409     InputStream in = classLoader.getResourceAsStream(propertiesFile);
410     try
411     {
412       if (in != null)
413       {
414         in = new BufferedInputStream(in);
415         properties.load(in);
416       }
417       else
418       {
419         LOG.warn("Cannot find properties '" + propertiesFile
420                  + "' in class path.");
421       }
422     }
423     catch (final IOException e)
424     {
425       LOG.warn("Cannot load properties from '" + propertiesFile + "'.");
426     }
427     finally
428     {
429       IOUtils.closeQuietly(in);
430     }
431 
432     return properties;
433   }
434 
435   private Map<Class<?>, List<PropertyDescriptor>> calcDescriptors(
436       final Set<Class<?>> propertyDescriptorTypes)
437   {
438     final Map<Class<?>, List<PropertyDescriptor>> map =
439         new HashMap<Class<?>, List<PropertyDescriptor>>();
440 
441     for (final Class<?> type : propertyDescriptorTypes)
442     {
443       final PropertiesContext context = factoryCache.getContext(type);
444       if (context == null)
445       {
446         LOG.debug("Cannot find context for type '" + type.getName()
447                   + "'. Skipping.");
448         continue;
449       }
450       final PropertyMetaDataParser propertyDescriptorParser =
451           PropertyMetaDataParser.create(context);
452       final List<PropertyDescriptor> descriptors =
453           propertyDescriptorParser.readDescriptors(type);
454       map.put(type, descriptors);
455     }
456 
457     return map;
458   }
459 
460   private void addProperties(
461       final Map<Class<?>, List<PropertyDescriptor>> descriptorMap,
462       final MultiSourceProperties compositeProperties)
463   {
464     final Properties properties = new Properties();
465     for (final Entry<Class<?>, List<PropertyDescriptor>> entry : descriptorMap
466         .entrySet())
467     {
468       final List<PropertyDescriptor> descriptors = entry.getValue();
469 
470       for (final PropertyDescriptor descriptor : descriptors)
471       {
472         final String propertyKey = descriptor.getKey().toString();
473         final Property property =
474             compositeProperties.getValue(propertyKey, true);
475         if (property != null && property.getValue() != null)
476         {
477           properties.put(propertyKey, property);
478         }
479       }
480     }
481     final ConfigurationKey<?> key = compositeProperties.getConfigurationKey();
482     final ConfigurationPropertiesManagement configuration =
483         factoryCache.ensureManagement(key);
484     final PropertyProvider provider =
485         new PropertiesPropertyProvider(key, new PropertyLocation(
486             "classpath-various"), properties);
487     configuration.addDefinitions(provider);
488   }
489 
490   /**
491    * Adds all class path root URLs provided by the context class loader of the
492    * current thread.
493    */
494   public void addDefaultRootUrls()
495   {
496     addBootRootUrls();
497     addRootUrl(Thread.currentThread().getContextClassLoader());
498   }
499 
500   private void addBootRootUrls()
501   {
502     final PropertyDescriptorRegistry registry =
503         new InMemoryPropertyDescriptorRegistry();
504     final BootConfigurationProperties initBootConfiguration =
505         new BootConfigurationProperties(registry, decrypter);
506     final BootLoader bootLoader =
507         new BootLoader(initBootConfiguration, Thread.currentThread()
508             .getContextClassLoader());
509     final ConfigurationPropertiesManagement bootConfiguration =
510         bootLoader.loadAndValidate();
511     final BootProperties confProperties =
512         bootConfiguration.getProperties(BootProperties.class);
513     final List<URL> urls = confProperties.additionalPropertiesLocations();
514     if (urls != null)
515     {
516       for (final URL url : urls)
517       {
518         final URL normalized = normalize(url);
519         addAdditionalRootUrl(normalized);
520       }
521     }
522   }
523 
524   private void addAdditionalRootUrl(final URL rootUrl)
525   {
526     Arg.checkNotNull("rootUrl", rootUrl);
527 
528     if (!additionalRootUrls.contains(rootUrl))
529     {
530       additionalRootUrls.add(rootUrl);
531     }
532   }
533 
534   private static URL normalize(final URL url)
535   {
536     final String urlString = url.toExternalForm();
537     if (urlString.indexOf(urlString.length() - 1) != '/')
538     {
539       try
540       {
541         return new URL(urlString + '/');
542       }
543       catch (final MalformedURLException e)
544       {
545         LOG.warn("Cannot append '/' to '" + urlString
546                  + "' for normalization. Using unnormalized URL instead.", e);
547       }
548     }
549     return url;
550   }
551 
552   /**
553    * Adds the given root URLs to the collection of class path roots managed by
554    * this class loader. The root locations are used for searching property
555    * declarations and property definitions.
556    *
557    * @param rootLocations the additional root locations to search for property
558    *          declarations and definitions.
559    */
560   public void addRootUrls(final List<URL> rootLocations)
561   {
562     this.rootUrls.addAll(rootLocations);
563   }
564 
565   /**
566    * Adds the given property providers as additional property definitions.
567    * Values provided by these instances are taken into account similar to any
568    * property definitions found on the class path.
569    * <p>
570    * Property values provided by these providers take precedence over any
571    * property values found on the class path.
572    * </p>
573    *
574    * @param rootPropertyProviders the additional property definitions to add.
575    */
576   public void addRootProperties(
577       final List<PropertyProvider> rootPropertyProviders)
578   {
579     this.rootPropertyProviders.addAll(rootPropertyProviders);
580   }
581 
582   // --- object basics --------------------------------------------------------
583 
584   /**
585    * Returns the string representation of the object.
586    *
587    * @return the string representation of the object.
588    */
589   @Override
590   public String toString()
591   {
592     final StringBuilder buffer = new StringBuilder();
593 
594     final char returnChar = '\n';
595 
596     buffer.append("== Root URLs:");
597     for (final URL url : rootUrls)
598     {
599       buffer.append(returnChar).append("  ").append(url.toExternalForm());
600     }
601 
602     buffer.append(returnChar).append("== Additional root URLs:");
603     for (final URL url : additionalRootUrls)
604     {
605       buffer.append(returnChar).append("  ").append(url.toExternalForm());
606     }
607 
608     buffer.append(returnChar).append("\n== Root Property Providers:");
609     for (final PropertyProvider provider : rootPropertyProviders)
610     {
611       buffer.append(returnChar).append("  ").append(provider);
612     }
613 
614     buffer.append(returnChar).append("== Factory Cache:\n");
615     buffer.append(factoryCache);
616 
617     buffer.append(returnChar).append("== Properties:");
618     buffer.append(returnChar).append("  Lenient validation  : ")
619         .append(lenient);
620     buffer.append(returnChar).append("  Skip propertyLoading: ")
621         .append(skipPropertyLoading);
622 
623     return buffer.toString();
624   }
625 }