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(UrlUtil.getResourceUrlsFromFolder(
219         classLoader, PropertiesContext.META_INF_HOME)))
220     {
221       addRootUrl(url);
222     }
223   }
224 
225   // --- business -------------------------------------------------------------
226 
227   /**
228    * Loads the configuration properties instance from information found on the
229    * class path.
230    *
231    * @return the loaded configuration properties instance.
232    * @throws CompoundConfigurationException if loading encountered problems.
233    */
234   public ConfigurationRepositoryManagement load()
235     throws CompoundConfigurationException
236   {
237     final Set<Class<?>> propertyDescriptorTypes = loadPropertyDescriptors();
238     final Map<Class<?>, List<PropertyDescriptor>> descriptors =
239         calcDescriptors(propertyDescriptorTypes);
240     factoryCache.registerDescriptors(descriptors);
241 
242     final MultiSourcePropertiesManager propertiesManager = loadProperties();
243     try
244     {
245       for (final MultiSourceProperties properties : propertiesManager
246           .getProperties())
247       {
248         final List<ConfigurationException> exceptions =
249             properties.getExceptions();
250         if (!exceptions.isEmpty())
251         {
252           throw new CompoundConfigurationException(
253               properties.getConfigurationKey(), exceptions);
254         }
255 
256         addProperties(descriptors, properties);
257       }
258     }
259     finally
260     {
261       propertiesManager.release();
262     }
263 
264     return factoryCache.getCache();
265   }
266 
267   private Set<Class<?>> loadPropertyDescriptors()
268   {
269     final PropertySetClassesLoader loader = new PropertySetClassesLoader();
270     final Set<Class<?>> propertyDescriptorTypes =
271         loader.getPropertySetTypes(rootUrls);
272     return propertyDescriptorTypes;
273   }
274 
275   private MultiSourcePropertiesManager loadProperties()
276   {
277     final MultiSourcePropertiesManager allPropertiesManager =
278         new MultiSourcePropertiesManager(lenient, rootPropertyProviders);
279 
280     loadProperties(allPropertiesManager);
281 
282     if (skipPropertyLoading)
283     {
284       allPropertiesManager.create();
285     }
286 
287     return allPropertiesManager;
288   }
289 
290   private void loadProperties(
291       final MultiSourcePropertiesManager allPropertiesManager)
292   {
293     final Collection<URL> propertiesUrls = createPropertiesUrls();
294     if (!propertiesUrls.isEmpty())
295     {
296       final PropertiesFilesLoader loader = new PropertiesFilesLoader();
297       LOG.debug("Loading properties/Root location URLs: {}", propertiesUrls);
298       final Set<String> propertiesFiles =
299           loader.getPropertiesFiles(propertiesUrls);
300 
301       final ClassLoader classLoader =
302           new URLClassLoader(propertiesUrls.toArray(new URL[propertiesUrls
303               .size()]), Thread.currentThread().getContextClassLoader());
304       for (final String propertiesFile : propertiesFiles)
305       {
306         if (propertiesFile.contains("META-INF"))
307         {
308           continue;
309         }
310 
311         final ClassPathContext context =
312             ClassLoaderUtils
313                 .createClassPathContext(classLoader, propertiesFile);
314 
315         if (!isDefintionsArchive(context))
316         {
317           continue;
318         }
319 
320         final DefinitionKeyHelper helper =
321             allPropertiesManager.getDefinition(context);
322         if (helper != null)
323         {
324           addProperties(allPropertiesManager, classLoader, propertiesFile,
325               helper);
326         }
327         else
328         {
329           LOG.warn("Skipping '" + propertiesFile + "' since no '"
330                    + PropertiesContext.META_INF_HOME + "' provided.");
331         }
332       }
333     }
334   }
335 
336   private static boolean isDefintionsArchive(final ClassPathContext context)
337   {
338     final URL url = context.getResource(PropertiesContext.DEFINITION_FILE);
339     return url != null;
340   }
341 
342   private Collection<URL> createPropertiesUrls()
343   {
344     if (skipPropertyLoading)
345     {
346       return this.additionalRootUrls;
347     }
348     else
349     {
350       final Collection<URL> urls =
351           new ArrayList<URL>(additionalRootUrls.size() + rootUrls.size());
352       urls.addAll(additionalRootUrls);
353       urls.addAll(rootUrls);
354       return urls;
355     }
356   }
357 
358   private void addProperties(
359       final MultiSourcePropertiesManager allPropertiesManager,
360       final ClassLoader classLoader, final String propertiesFile,
361       final DefinitionKeyHelper helper) throws IllegalArgumentException
362   {
363     final ConfigurationKey<?> key = helper.parse(propertiesFile);
364     final MultiSourceProperties allProperties =
365         allPropertiesManager.create(key);
366 
367     final PropertyLocation location =
368         new PropertyLocationHelper().createPropertyLocation(classLoader,
369             propertiesFile);
370     final Properties properties = loadProperties(classLoader, propertiesFile);
371     allProperties.add(location, properties);
372   }
373 
374   private Properties loadProperties(final ClassLoader classLoader,
375       final String propertiesFile)
376   {
377     // TODO: There is a similar method in PropertiesHelper: Refactor
378     final Properties properties = new Properties();
379     InputStream in = classLoader.getResourceAsStream(propertiesFile);
380     try
381     {
382       if (in != null)
383       {
384         in = new BufferedInputStream(in);
385         properties.load(in);
386       }
387       else
388       {
389         LOG.warn("Cannot find properties '" + propertiesFile
390                  + "' in class path.");
391       }
392     }
393     catch (final IOException e)
394     {
395       LOG.warn("Cannot load properties from '" + propertiesFile + "'.");
396     }
397     finally
398     {
399       IOUtils.closeQuietly(in);
400     }
401 
402     return properties;
403   }
404 
405   private Map<Class<?>, List<PropertyDescriptor>> calcDescriptors(
406       final Set<Class<?>> propertyDescriptorTypes)
407   {
408     final Map<Class<?>, List<PropertyDescriptor>> map =
409         new HashMap<Class<?>, List<PropertyDescriptor>>();
410 
411     for (final Class<?> type : propertyDescriptorTypes)
412     {
413       final PropertiesContext context = factoryCache.getContext(type);
414       if (context == null)
415       {
416         LOG.debug("Cannot find context for type '" + type.getName()
417                   + "'. Skipping.");
418         continue;
419       }
420       final PropertyMetaDataParser propertyDescriptorParser =
421           PropertyMetaDataParser.create(context);
422       final List<PropertyDescriptor> descriptors =
423           propertyDescriptorParser.readDescriptors(type);
424       map.put(type, descriptors);
425     }
426 
427     return map;
428   }
429 
430   private void addProperties(
431       final Map<Class<?>, List<PropertyDescriptor>> descriptorMap,
432       final MultiSourceProperties compositeProperties)
433   {
434     final Properties properties = new Properties();
435     for (final Entry<Class<?>, List<PropertyDescriptor>> entry : descriptorMap
436         .entrySet())
437     {
438       final List<PropertyDescriptor> descriptors = entry.getValue();
439 
440       for (final PropertyDescriptor descriptor : descriptors)
441       {
442         final String propertyKey = descriptor.getKey().toString();
443         final Property property =
444             compositeProperties.getValue(propertyKey, true);
445         if (property != null && property.getValue() != null)
446         {
447           properties.put(propertyKey, property);
448         }
449       }
450     }
451     final ConfigurationKey<?> key = compositeProperties.getConfigurationKey();
452     final ConfigurationPropertiesManagement configuration =
453         factoryCache.ensureManagement(key);
454     final PropertyProvider provider =
455         new PropertiesPropertyProvider(key, new PropertyLocation(
456             "classpath-various"), properties);
457     configuration.addDefinitions(provider);
458   }
459 
460   /**
461    * Adds all class path root URLs provided by the context class loader of the
462    * current thread.
463    */
464   public void addDefaultRootUrls()
465   {
466     addBootRootUrls();
467     addRootUrl(Thread.currentThread().getContextClassLoader());
468   }
469 
470   private void addBootRootUrls()
471   {
472     final PropertyDescriptorRegistry registry =
473         new InMemoryPropertyDescriptorRegistry();
474     final BootConfigurationProperties initBootConfiguration =
475         new BootConfigurationProperties(registry, decrypter);
476     final BootLoader bootLoader =
477         new BootLoader(initBootConfiguration, Thread.currentThread()
478             .getContextClassLoader());
479     final ConfigurationPropertiesManagement bootConfiguration =
480         bootLoader.loadAndValidate();
481     final BootProperties confProperties =
482         bootConfiguration.getProperties(BootProperties.class);
483     final List<URL> urls = confProperties.additionalPropertiesLocations();
484     if (urls != null)
485     {
486       for (final URL url : urls)
487       {
488         final URL normalized = normalize(url);
489         addAdditionalRootUrl(normalized);
490       }
491     }
492   }
493 
494   private void addAdditionalRootUrl(final URL rootUrl)
495   {
496     Arg.checkNotNull("rootUrl", rootUrl);
497 
498     if (!additionalRootUrls.contains(rootUrl))
499     {
500       additionalRootUrls.add(rootUrl);
501     }
502   }
503 
504   private static URL normalize(final URL url)
505   {
506     final String urlString = url.toExternalForm();
507     if (urlString.indexOf(urlString.length() - 1) != '/')
508     {
509       try
510       {
511         return new URL(urlString + '/');
512       }
513       catch (final MalformedURLException e)
514       {
515         LOG.warn("Cannot append '/' to '" + urlString
516                  + "' for normalization. Using unnormalized URL instead.", e);
517       }
518     }
519     return url;
520   }
521 
522   /**
523    * Adds the given root URLs to the collection of class path roots managed by
524    * this class loader. The root locations are used for searching property
525    * declarations and property definitions.
526    *
527    * @param rootLocations the additional root locations to search for property
528    *          declarations and definitions.
529    */
530   public void addRootUrls(final List<URL> rootLocations)
531   {
532     this.rootUrls.addAll(rootLocations);
533   }
534 
535   /**
536    * Adds the given property providers as additional property definitions.
537    * Values provided by these instances are taken into account similar to any
538    * property definitions found on the class path.
539    * <p>
540    * Property values provided by these providers take precedence over any
541    * property values found on the class path.
542    * </p>
543    *
544    * @param rootPropertyProviders the additional property definitions to add.
545    */
546   public void addRootProperties(
547       final List<PropertyProvider> rootPropertyProviders)
548   {
549     this.rootPropertyProviders.addAll(rootPropertyProviders);
550   }
551 
552   // --- object basics --------------------------------------------------------
553 
554   /**
555    * Returns the string representation of the object.
556    *
557    * @return the string representation of the object.
558    */
559   @Override
560   public String toString()
561   {
562     final StringBuilder buffer = new StringBuilder();
563 
564     final char returnChar = '\n';
565 
566     buffer.append("== Root URLs:");
567     for (final URL url : rootUrls)
568     {
569       buffer.append(returnChar).append("  ").append(url.toExternalForm());
570     }
571 
572     buffer.append(returnChar).append("== Additional root URLs:");
573     for (final URL url : additionalRootUrls)
574     {
575       buffer.append(returnChar).append("  ").append(url.toExternalForm());
576     }
577 
578     buffer.append(returnChar).append("\n== Root Property Providers:");
579     for (final PropertyProvider provider : rootPropertyProviders)
580     {
581       buffer.append(returnChar).append("  ").append(provider);
582     }
583 
584     buffer.append(returnChar).append("== Factory Cache:\n");
585     buffer.append(factoryCache);
586 
587     buffer.append(returnChar).append("== Properties:");
588     buffer.append(returnChar).append("  Lenient validation  : ")
589         .append(lenient);
590     buffer.append(returnChar).append("  Skip propertyLoading: ")
591         .append(skipPropertyLoading);
592 
593     return buffer.toString();
594   }
595 }