Coverage Report - de.smartics.properties.spi.config.support.ClassPathLoader
 
Classes in this File Line Coverage Branch Coverage Complexity
ClassPathLoader
73%
129/175
57%
31/54
2,478
 
 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  1
   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  3
   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  3
   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  3
   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  3
   {
 145  3
     this.factoryCache = Arg.checkNotNull("factoryCache", factoryCache);
 146  3
     this.lenient = lenient;
 147  3
     this.skipPropertyLoading = skipPropertyLoading;
 148  3
     this.decrypter = Arg.checkNotNull("decrypter", decrypter);
 149  3
   }
 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  5
     Arg.checkNotNull("rootUrl", rootUrl);
 168  
 
 169  5
     if (!rootUrls.contains(rootUrl))
 170  
     {
 171  5
       rootUrls.add(rootUrl);
 172  
     }
 173  5
   }
 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  1
     Arg.checkNotNull("exemplar", exemplar);
 185  1
     addRootUrl(Thread.currentThread().getContextClassLoader());
 186  1
   }
 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  2
     Arg.checkNotNull("classLoader", classLoader);
 204  
 
 205  
     try
 206  
     {
 207  2
       addResources(classLoader);
 208  
     }
 209  0
     catch (final IOException e)
 210  
     {
 211  0
       LOG.warn("Cannot determine class path roots for the given class loader.",
 212  
           e);
 213  2
     }
 214  2
   }
 215  
 
 216  
   private void addResources(final ClassLoader classLoader) throws IOException
 217  
   {
 218  2
     for (final URL url : Collections.list(classLoader
 219  
         .getResources(PropertiesContext.META_INF_HOME)))
 220  
     {
 221  4
       final URL rootUrl = truncateUrl(url);
 222  4
       addRootUrl(rootUrl);
 223  4
     }
 224  2
   }
 225  
 
 226  
   private static URL truncateUrl(final URL url)
 227  
   {
 228  4
     final String urlString = url.toExternalForm();
 229  4
     final boolean isJar = "jar".equals(url.getProtocol());
 230  
     final int last;
 231  4
     if (isJar)
 232  
     {
 233  0
       last = urlString.indexOf('!') + 2;
 234  
     }
 235  
     else
 236  
     {
 237  4
       last = urlString.length() - PropertiesContext.META_INF_HOME.length() - 1;
 238  
     }
 239  
 
 240  4
     final String urlStringTruncated = urlString.substring(0, last);
 241  
     URL urlTruncated;
 242  
     try
 243  
     {
 244  4
       urlTruncated = new URL(urlStringTruncated);
 245  4
       return urlTruncated;
 246  
     }
 247  0
     catch (final MalformedURLException e)
 248  
     {
 249  0
       LOG.warn("Cannot use URL '{}' in its truncated form '{}'.", url,
 250  
           urlStringTruncated);
 251  0
       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  3
     final Set<Class<?>> propertyDescriptorTypes = loadPropertyDescriptors();
 268  3
     final Map<Class<?>, List<PropertyDescriptor>> descriptors =
 269  
         calcDescriptors(propertyDescriptorTypes);
 270  3
     factoryCache.registerDescriptors(descriptors);
 271  
 
 272  3
     final MultiSourcePropertiesManager propertiesManager = loadProperties();
 273  
     try
 274  
     {
 275  3
       for (final MultiSourceProperties properties : propertiesManager
 276  
           .getProperties())
 277  
       {
 278  3
         final List<ConfigurationException> exceptions =
 279  
             properties.getExceptions();
 280  3
         if (!exceptions.isEmpty())
 281  
         {
 282  0
           throw new CompoundConfigurationException(
 283  
               properties.getConfigurationKey(), exceptions);
 284  
         }
 285  
 
 286  3
         addProperties(descriptors, properties);
 287  3
       }
 288  
     }
 289  
     finally
 290  
     {
 291  3
       propertiesManager.release();
 292  3
     }
 293  
 
 294  3
     return factoryCache.getCache();
 295  
   }
 296  
 
 297  
   private Set<Class<?>> loadPropertyDescriptors()
 298  
   {
 299  3
     final PropertySetClassesLoader loader = new PropertySetClassesLoader();
 300  3
     final Set<Class<?>> propertyDescriptorTypes =
 301  
         loader.getPropertySetTypes(rootUrls);
 302  3
     return propertyDescriptorTypes;
 303  
   }
 304  
 
 305  
   private MultiSourcePropertiesManager loadProperties()
 306  
   {
 307  3
     final MultiSourcePropertiesManager allPropertiesManager =
 308  
         new MultiSourcePropertiesManager(lenient, rootPropertyProviders);
 309  
 
 310  3
     loadProperties(allPropertiesManager);
 311  
 
 312  3
     if (skipPropertyLoading)
 313  
     {
 314  0
       allPropertiesManager.create();
 315  
     }
 316  
 
 317  3
     return allPropertiesManager;
 318  
   }
 319  
 
 320  
   private void loadProperties(
 321  
       final MultiSourcePropertiesManager allPropertiesManager)
 322  
   {
 323  3
     final Collection<URL> propertiesUrls = createPropertiesUrls();
 324  3
     if (!propertiesUrls.isEmpty())
 325  
     {
 326  3
       final PropertiesFilesLoader loader = new PropertiesFilesLoader();
 327  3
       LOG.debug("Loading properties/Root location URLs: {}", propertiesUrls);
 328  3
       final Set<String> propertiesFiles =
 329  
           loader.getPropertiesFiles(propertiesUrls);
 330  
 
 331  3
       final ClassLoader classLoader =
 332  
           new URLClassLoader(propertiesUrls.toArray(new URL[propertiesUrls
 333  
               .size()]), Thread.currentThread().getContextClassLoader());
 334  3
       for (final String propertiesFile : propertiesFiles)
 335  
       {
 336  58
         if (propertiesFile.contains("META-INF"))
 337  
         {
 338  2
           continue;
 339  
         }
 340  
 
 341  56
         final ClassPathContext context =
 342  
             ClassLoaderUtils
 343  
                 .createClassPathContext(classLoader, propertiesFile);
 344  
 
 345  56
         if (!isDefintionsArchive(context))
 346  
         {
 347  14
           continue;
 348  
         }
 349  
 
 350  42
         final DefinitionKeyHelper helper =
 351  
             allPropertiesManager.getDefinition(context);
 352  42
         if (helper != null)
 353  
         {
 354  42
           addProperties(allPropertiesManager, classLoader, propertiesFile,
 355  
               helper);
 356  
         }
 357  
         else
 358  
         {
 359  0
           LOG.warn("Skipping '" + propertiesFile + "' since no '"
 360  
                    + PropertiesContext.META_INF_HOME + "' provided.");
 361  
         }
 362  42
       }
 363  
     }
 364  3
   }
 365  
 
 366  
   private static boolean isDefintionsArchive(final ClassPathContext context)
 367  
   {
 368  56
     final URL url = context.getResource(PropertiesContext.DEFINITION_FILE);
 369  56
     return url != null;
 370  
   }
 371  
 
 372  
   private Collection<URL> createPropertiesUrls()
 373  
   {
 374  3
     if (skipPropertyLoading)
 375  
     {
 376  0
       return this.additionalRootUrls;
 377  
     }
 378  
     else
 379  
     {
 380  3
       final Collection<URL> urls =
 381  
           new ArrayList<URL>(additionalRootUrls.size() + rootUrls.size());
 382  3
       urls.addAll(additionalRootUrls);
 383  3
       urls.addAll(rootUrls);
 384  3
       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  42
     final ConfigurationKey<?> key = helper.parse(propertiesFile);
 394  42
     final MultiSourceProperties allProperties =
 395  
         allPropertiesManager.create(key);
 396  
 
 397  42
     final PropertyLocation location =
 398  
         new PropertyLocationHelper().createPropertyLocation(classLoader,
 399  
             propertiesFile);
 400  42
     final Properties properties = loadProperties(classLoader, propertiesFile);
 401  42
     allProperties.add(location, properties);
 402  42
   }
 403  
 
 404  
   private Properties loadProperties(final ClassLoader classLoader,
 405  
       final String propertiesFile)
 406  
   {
 407  
     // TODO: There is a similar method in PropertiesHelper: Refactor
 408  42
     final Properties properties = new Properties();
 409  42
     InputStream in = classLoader.getResourceAsStream(propertiesFile);
 410  
     try
 411  
     {
 412  42
       if (in != null)
 413  
       {
 414  42
         in = new BufferedInputStream(in);
 415  42
         properties.load(in);
 416  
       }
 417  
       else
 418  
       {
 419  0
         LOG.warn("Cannot find properties '" + propertiesFile
 420  
                  + "' in class path.");
 421  
       }
 422  
     }
 423  0
     catch (final IOException e)
 424  
     {
 425  0
       LOG.warn("Cannot load properties from '" + propertiesFile + "'.");
 426  
     }
 427  
     finally
 428  
     {
 429  42
       IOUtils.closeQuietly(in);
 430  42
     }
 431  
 
 432  42
     return properties;
 433  
   }
 434  
 
 435  
   private Map<Class<?>, List<PropertyDescriptor>> calcDescriptors(
 436  
       final Set<Class<?>> propertyDescriptorTypes)
 437  
   {
 438  3
     final Map<Class<?>, List<PropertyDescriptor>> map =
 439  
         new HashMap<Class<?>, List<PropertyDescriptor>>();
 440  
 
 441  3
     for (final Class<?> type : propertyDescriptorTypes)
 442  
     {
 443  62
       final PropertiesContext context = factoryCache.getContext(type);
 444  62
       if (context == null)
 445  
       {
 446  0
         LOG.debug("Cannot find context for type '" + type.getName()
 447  
                   + "'. Skipping.");
 448  0
         continue;
 449  
       }
 450  62
       final PropertyMetaDataParser propertyDescriptorParser =
 451  
           PropertyMetaDataParser.create(context);
 452  62
       final List<PropertyDescriptor> descriptors =
 453  
           propertyDescriptorParser.readDescriptors(type);
 454  62
       map.put(type, descriptors);
 455  62
     }
 456  
 
 457  3
     return map;
 458  
   }
 459  
 
 460  
   private void addProperties(
 461  
       final Map<Class<?>, List<PropertyDescriptor>> descriptorMap,
 462  
       final MultiSourceProperties compositeProperties)
 463  
   {
 464  3
     final Properties properties = new Properties();
 465  3
     for (final Entry<Class<?>, List<PropertyDescriptor>> entry : descriptorMap
 466  
         .entrySet())
 467  
     {
 468  62
       final List<PropertyDescriptor> descriptors = entry.getValue();
 469  
 
 470  62
       for (final PropertyDescriptor descriptor : descriptors)
 471  
       {
 472  113
         final String propertyKey = descriptor.getKey().toString();
 473  113
         final Property property =
 474  
             compositeProperties.getValue(propertyKey, true);
 475  113
         if (property != null && property.getValue() != null)
 476  
         {
 477  36
           properties.put(propertyKey, property);
 478  
         }
 479  113
       }
 480  62
     }
 481  3
     final ConfigurationKey<?> key = compositeProperties.getConfigurationKey();
 482  3
     final ConfigurationPropertiesManagement configuration =
 483  
         factoryCache.ensureManagement(key);
 484  3
     final PropertyProvider provider =
 485  
         new PropertiesPropertyProvider(key, new PropertyLocation(
 486  
             "classpath-various"), properties);
 487  3
     configuration.addDefinitions(provider);
 488  3
   }
 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  1
     addBootRootUrls();
 497  1
     addRootUrl(Thread.currentThread().getContextClassLoader());
 498  1
   }
 499  
 
 500  
   private void addBootRootUrls()
 501  
   {
 502  1
     final PropertyDescriptorRegistry registry =
 503  
         new InMemoryPropertyDescriptorRegistry();
 504  1
     final BootConfigurationProperties initBootConfiguration =
 505  
         new BootConfigurationProperties(registry, decrypter);
 506  1
     final BootLoader bootLoader =
 507  
         new BootLoader(initBootConfiguration, Thread.currentThread()
 508  
             .getContextClassLoader());
 509  1
     final ConfigurationPropertiesManagement bootConfiguration =
 510  
         bootLoader.loadAndValidate();
 511  1
     final BootProperties confProperties =
 512  
         bootConfiguration.getProperties(BootProperties.class);
 513  1
     final List<URL> urls = confProperties.additionalPropertiesLocations();
 514  1
     if (urls != null)
 515  
     {
 516  0
       for (final URL url : urls)
 517  
       {
 518  0
         final URL normalized = normalize(url);
 519  0
         addAdditionalRootUrl(normalized);
 520  0
       }
 521  
     }
 522  1
   }
 523  
 
 524  
   private void addAdditionalRootUrl(final URL rootUrl)
 525  
   {
 526  0
     Arg.checkNotNull("rootUrl", rootUrl);
 527  
 
 528  0
     if (!additionalRootUrls.contains(rootUrl))
 529  
     {
 530  0
       additionalRootUrls.add(rootUrl);
 531  
     }
 532  0
   }
 533  
 
 534  
   private static URL normalize(final URL url)
 535  
   {
 536  0
     final String urlString = url.toExternalForm();
 537  0
     if (urlString.indexOf(urlString.length() - 1) != '/')
 538  
     {
 539  
       try
 540  
       {
 541  0
         return new URL(urlString + '/');
 542  
       }
 543  0
       catch (final MalformedURLException e)
 544  
       {
 545  0
         LOG.warn("Cannot append '/' to '" + urlString
 546  
                  + "' for normalization. Using unnormalized URL instead.", e);
 547  
       }
 548  
     }
 549  0
     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  1
     this.rootUrls.addAll(rootLocations);
 563  1
   }
 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  1
     this.rootPropertyProviders.addAll(rootPropertyProviders);
 580  1
   }
 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  0
     final StringBuilder buffer = new StringBuilder();
 593  
 
 594  0
     final char returnChar = '\n';
 595  
 
 596  0
     buffer.append("== Root URLs:");
 597  0
     for (final URL url : rootUrls)
 598  
     {
 599  0
       buffer.append(returnChar).append("  ").append(url.toExternalForm());
 600  
     }
 601  
 
 602  0
     buffer.append(returnChar).append("== Additional root URLs:");
 603  0
     for (final URL url : additionalRootUrls)
 604  
     {
 605  0
       buffer.append(returnChar).append("  ").append(url.toExternalForm());
 606  
     }
 607  
 
 608  0
     buffer.append(returnChar).append("\n== Root Property Providers:");
 609  0
     for (final PropertyProvider provider : rootPropertyProviders)
 610  
     {
 611  0
       buffer.append(returnChar).append("  ").append(provider);
 612  
     }
 613  
 
 614  0
     buffer.append(returnChar).append("== Factory Cache:\n");
 615  0
     buffer.append(factoryCache);
 616  
 
 617  0
     buffer.append(returnChar).append("== Properties:");
 618  0
     buffer.append(returnChar).append("  Lenient validation  : ")
 619  
         .append(lenient);
 620  0
     buffer.append(returnChar).append("  Skip propertyLoading: ")
 621  
         .append(skipPropertyLoading);
 622  
 
 623  0
     return buffer.toString();
 624  
   }
 625  
 }