Coverage Report - de.smartics.properties.spi.config.support.ClassPathLoader
 
Classes in this File Line Coverage Branch Coverage Complexity
ClassPathLoader
0%
0/137
0%
0/38
2,444
 
 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  0
   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  0
   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  0
   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  0
   {
 131  0
     Arguments.checkNotNull("factoryCache", factoryCache);
 132  
 
 133  0
     this.factoryCache = factoryCache;
 134  0
     this.lenient = lenient;
 135  0
     this.skipPropertyLoading = skipPropertyLoading;
 136  0
   }
 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  0
     Arguments.checkNotNull("rootUrl", rootUrl);
 155  
 
 156  0
     if (!rootUrls.contains(rootUrl))
 157  
     {
 158  0
       rootUrls.add(rootUrl);
 159  
     }
 160  0
   }
 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  0
     Arguments.checkNotNull("exemplar", exemplar);
 172  0
     addRootUrl(Thread.currentThread().getContextClassLoader());
 173  0
   }
 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  0
     Arguments.checkNotNull("classLoader", classLoader);
 191  
 
 192  
     try
 193  
     {
 194  0
       addResources(classLoader);
 195  
     }
 196  0
     catch (final IOException e)
 197  
     {
 198  0
       LOG.warn("Cannot determine class path roots for the given class loader.",
 199  
           e);
 200  0
     }
 201  0
   }
 202  
 
 203  
   private void addResources(final ClassLoader classLoader) throws IOException
 204  
   {
 205  0
     for (final URL url : Collections.list(classLoader
 206  
         .getResources(PropertiesContext.META_INF_HOME)))
 207  
     {
 208  0
       final URL rootUrl = truncateUrl(url);
 209  0
       addRootUrl(rootUrl);
 210  0
     }
 211  0
   }
 212  
 
 213  
   private static URL truncateUrl(final URL url)
 214  
   {
 215  0
     final String urlString = url.toExternalForm();
 216  0
     final boolean isJar = "jar".equals(url.getProtocol());
 217  
     final int last;
 218  0
     if (isJar)
 219  
     {
 220  0
       last = urlString.indexOf('!') + 2;
 221  
     }
 222  
     else
 223  
     {
 224  0
       last = urlString.length() - PropertiesContext.META_INF_HOME.length() - 1;
 225  
     }
 226  
 
 227  0
     final String urlStringTruncated = urlString.substring(0, last);
 228  
     URL urlTruncated;
 229  
     try
 230  
     {
 231  0
       urlTruncated = new URL(urlStringTruncated);
 232  0
       return urlTruncated;
 233  
     }
 234  0
     catch (final MalformedURLException e)
 235  
     {
 236  0
       LOG.warn("Cannot use URL '{}' in its truncated form '{}'.", url,
 237  
           urlStringTruncated);
 238  0
       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  0
     final Set<Class<?>> propertyDescriptorTypes = loadPropertyDescriptors();
 255  0
     final Map<Class<?>, List<PropertyDescriptor>> descriptors =
 256  
         calcDescriptors(propertyDescriptorTypes);
 257  0
     factoryCache.registerDescriptors(descriptors);
 258  
 
 259  0
     final MultiSourcePropertiesManager propertiesManager = loadProperties();
 260  
 
 261  0
     for (final MultiSourceProperties properties : propertiesManager
 262  
         .getProperties())
 263  
     {
 264  0
       final List<ConfigurationException> exceptions =
 265  
           properties.getExceptions();
 266  0
       if (!exceptions.isEmpty())
 267  
       {
 268  0
         throw new CompoundConfigurationException(
 269  
             properties.getConfigurationKey(), exceptions);
 270  
       }
 271  
 
 272  0
       addProperties(descriptors, properties);
 273  0
     }
 274  
 
 275  0
     return factoryCache.getCache();
 276  
   }
 277  
 
 278  
   private Set<Class<?>> loadPropertyDescriptors()
 279  
   {
 280  0
     final PropertySetClassesLoader loader = new PropertySetClassesLoader();
 281  0
     final Set<Class<?>> propertyDescriptorTypes =
 282  
         loader.getPropertySetTypes(rootUrls);
 283  0
     return propertyDescriptorTypes;
 284  
   }
 285  
 
 286  
   private MultiSourcePropertiesManager loadProperties()
 287  
   {
 288  0
     final MultiSourcePropertiesManager allPropertiesManager =
 289  
         new MultiSourcePropertiesManager(lenient, rootPropertyProviders);
 290  
 
 291  0
     if (!skipPropertyLoading)
 292  
     {
 293  0
       final PropertiesFilesLoader loader = new PropertiesFilesLoader();
 294  0
       LOG.debug("Loading properties/Root location URLs: {}", rootUrls);
 295  0
       final Set<String> propertiesFiles = loader.getPropertiesFiles(rootUrls);
 296  
 
 297  0
       final ClassLoader classLoader =
 298  
           new URLClassLoader(rootUrls.toArray(new URL[rootUrls.size()]), Thread
 299  
               .currentThread().getContextClassLoader());
 300  0
       for (final String propertiesFile : propertiesFiles)
 301  
       {
 302  0
         if (propertiesFile.contains("META-INF"))
 303  
         {
 304  0
           continue;
 305  
         }
 306  
 
 307  0
         final ClassPathContext context =
 308  
             ClassLoaderUtils
 309  
                 .createClassPathContext(classLoader, propertiesFile);
 310  0
         final DefinitionKeyHelper helper =
 311  
             allPropertiesManager.getDefinition(context);
 312  0
         if (helper != null)
 313  
         {
 314  0
           addProperties(allPropertiesManager, classLoader, propertiesFile,
 315  
               helper);
 316  
         }
 317  
         else
 318  
         {
 319  0
           LOG.warn("Skipping '" + propertiesFile + "' since no '"
 320  
                    + PropertiesContext.META_INF_HOME + "' provided.");
 321  
         }
 322  0
       }
 323  0
     }
 324  
     else
 325  
     {
 326  0
       allPropertiesManager.create();
 327  
     }
 328  
 
 329  0
     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  0
     final ConfigurationKey key = helper.parse(propertiesFile);
 338  0
     final MultiSourceProperties allProperties =
 339  
         allPropertiesManager.create(key);
 340  
 
 341  0
     final PropertyLocation location =
 342  
         new PropertyLocationHelper().createPropertyLocation(classLoader,
 343  
             propertiesFile);
 344  0
     final Properties properties = loadProperties(classLoader, propertiesFile);
 345  0
     allProperties.add(location, properties);
 346  0
   }
 347  
 
 348  
   private Properties loadProperties(final ClassLoader classLoader,
 349  
       final String propertiesFile)
 350  
   {
 351  
     // TODO: There is a similar method in PropertiesUtils: Refactor
 352  0
     final Properties properties = new Properties();
 353  0
     InputStream in = classLoader.getResourceAsStream(propertiesFile);
 354  
     try
 355  
     {
 356  0
       if (in != null)
 357  
       {
 358  0
         in = new BufferedInputStream(in);
 359  0
         properties.load(in);
 360  
       }
 361  
       else
 362  
       {
 363  0
         LOG.warn("Cannot find properties '" + propertiesFile
 364  
                  + "' in class path.");
 365  
       }
 366  
     }
 367  0
     catch (final IOException e)
 368  
     {
 369  0
       LOG.warn("Cannot load properties from '" + propertiesFile + "'.");
 370  
     }
 371  
     finally
 372  
     {
 373  0
       IOUtils.closeQuietly(in);
 374  0
     }
 375  
 
 376  0
     return properties;
 377  
   }
 378  
 
 379  
   private Map<Class<?>, List<PropertyDescriptor>> calcDescriptors(
 380  
       final Set<Class<?>> propertyDescriptorTypes)
 381  
   {
 382  0
     final Map<Class<?>, List<PropertyDescriptor>> map =
 383  
         new HashMap<Class<?>, List<PropertyDescriptor>>();
 384  
 
 385  0
     for (final Class<?> type : propertyDescriptorTypes)
 386  
     {
 387  0
       final PropertiesContext context = factoryCache.getContext(type);
 388  0
       if (context == null)
 389  
       {
 390  0
         LOG.debug("Cannot find context for type '" + type.getName()
 391  
                   + "'. Skipping.");
 392  0
         continue;
 393  
       }
 394  0
       final PropertyMetaDataParser propertyDescriptorParser =
 395  
           PropertyMetaDataParser.create(context);
 396  0
       final List<PropertyDescriptor> descriptors =
 397  
           propertyDescriptorParser.readDescriptors(type);
 398  0
       map.put(type, descriptors);
 399  0
     }
 400  
 
 401  0
     return map;
 402  
   }
 403  
 
 404  
   private void addProperties(
 405  
       final Map<Class<?>, List<PropertyDescriptor>> descriptorMap,
 406  
       final MultiSourceProperties compositeProperties)
 407  
   {
 408  0
     final Properties properties = new Properties();
 409  
 
 410  0
     for (final Entry<Class<?>, List<PropertyDescriptor>> entry : descriptorMap
 411  
         .entrySet())
 412  
     {
 413  0
       final List<PropertyDescriptor> descriptors = entry.getValue();
 414  
 
 415  0
       for (final PropertyDescriptor descriptor : descriptors)
 416  
       {
 417  0
         final String propertyKey = descriptor.getKey().toString();
 418  0
         final Property property = compositeProperties.getValue(propertyKey);
 419  0
         if (property != null && property.getValue() != null)
 420  
         {
 421  0
           properties.put(propertyKey, property);
 422  
         }
 423  0
       }
 424  
 
 425  0
       final ConfigurationKey key = compositeProperties.getConfigurationKey();
 426  0
       final ConfigurationPropertiesManagement configuration =
 427  
           factoryCache.ensureManagement(key);
 428  0
       configuration.addDefinitions(properties);
 429  0
     }
 430  0
   }
 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  0
     Arguments.checkNotNull("registry", registry);
 444  
 
 445  0
     addBootRootUrls(registry);
 446  0
     addRootUrl(Thread.currentThread().getContextClassLoader());
 447  0
   }
 448  
 
 449  
   private void addBootRootUrls(final PropertyDescriptorRegistry registry)
 450  
   {
 451  0
     final BootConfigurationProperties initBootConfiguration =
 452  
         new BootConfigurationProperties(registry);
 453  0
     final BootLoader bootLoader =
 454  
         new BootLoader(initBootConfiguration, Thread.currentThread()
 455  
             .getContextClassLoader());
 456  0
     final ConfigurationPropertiesManagement bootConfiguration =
 457  
         bootLoader.loadAndValidate();
 458  0
     final BootProperties confProperties =
 459  
         bootConfiguration.getProperties(BootProperties.class);
 460  0
     final List<URL> urls = confProperties.additionalPropertiesLocations();
 461  0
     if (urls != null)
 462  
     {
 463  0
       for (final URL url : urls)
 464  
       {
 465  0
         final URL normalized = normalize(url);
 466  0
         addRootUrl(normalized);
 467  0
       }
 468  
     }
 469  0
   }
 470  
 
 471  
   private static URL normalize(final URL url)
 472  
   {
 473  0
     final String urlString = url.toExternalForm();
 474  0
     if (urlString.indexOf(urlString.length() - 1) != '/')
 475  
     {
 476  
       try
 477  
       {
 478  0
         return new URL(urlString + '/');
 479  
       }
 480  0
       catch (final MalformedURLException e)
 481  
       {
 482  0
         LOG.warn("Cannot append '/' to '" + urlString
 483  
                  + "' for normalization. Using unnormalized URL instead.", e);
 484  
       }
 485  
     }
 486  0
     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  0
     this.rootUrls.addAll(rootLocations);
 500  0
   }
 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  0
     this.rootPropertyProviders.addAll(rootPropertyProviders);
 517  0
   }
 518  
 
 519  
   // --- object basics --------------------------------------------------------
 520  
 
 521  
 }