[commons-configuration] 01/02: Imported Upstream version 1.10
Tony Mancill
tmancill at moszumanska.debian.org
Thu Sep 3 04:48:34 UTC 2015
This is an automated email from the git hooks/post-receive script.
tmancill pushed a commit to branch master
in repository commons-configuration.
commit 59b61032b0392e3a2449290a30718349672474bb
Author: tony mancill <tmancill at debian.org>
Date: Wed Sep 2 21:47:14 2015 -0700
Imported Upstream version 1.10
---
LICENSE.txt | 202 ++
NOTICE.txt | 5 +
RELEASE-NOTES.txt | 100 +
conf/CommonsConfiguration.xsd | 129 ++
conf/HEADER.txt | 16 +
conf/README | 4 +
conf/checkstyle-suppressions.xml | 33 +
conf/checkstyle.xml | 188 ++
conf/findbugs-exclude-filter.xml | 31 +
pom.xml | 681 ++++++
src/changes/changes.xml | 1805 +++++++++++++++
src/main/assembly/bin.xml | 45 +
src/main/assembly/src.xml | 40 +
.../configuration/AbstractConfiguration.java | 1309 +++++++++++
.../configuration/AbstractFileConfiguration.java | 1092 +++++++++
.../AbstractHierarchicalFileConfiguration.java | 579 +++++
.../commons/configuration/BaseConfiguration.java | 198 ++
.../configuration/BaseConfigurationXMLReader.java | 137 ++
.../configuration/CombinedConfiguration.java | 976 ++++++++
.../configuration/CompositeConfiguration.java | 578 +++++
.../commons/configuration/Configuration.java | 598 +++++
.../configuration/ConfigurationBuilder.java | 46 +
.../configuration/ConfigurationComparator.java | 40 +
.../configuration/ConfigurationConverter.java | 133 ++
.../configuration/ConfigurationException.java | 78 +
.../configuration/ConfigurationFactory.java | 903 ++++++++
.../commons/configuration/ConfigurationKey.java | 687 ++++++
.../commons/configuration/ConfigurationMap.java | 210 ++
.../ConfigurationRuntimeException.java | 79 +
.../commons/configuration/ConfigurationUtils.java | 759 +++++++
.../configuration/ConfigurationXMLReader.java | 364 +++
.../commons/configuration/ConversionException.java | 77 +
.../commons/configuration/DataConfiguration.java | 1979 ++++++++++++++++
.../configuration/DatabaseConfiguration.java | 709 ++++++
.../configuration/DefaultConfigurationBuilder.java | 1800 +++++++++++++++
.../commons/configuration/DefaultFileSystem.java | 373 +++
.../DynamicCombinedConfiguration.java | 935 ++++++++
.../configuration/EnvironmentConfiguration.java | 91 +
.../commons/configuration/FileConfiguration.java | 293 +++
.../commons/configuration/FileOptionsProvider.java | 75 +
.../apache/commons/configuration/FileSystem.java | 181 ++
.../commons/configuration/FileSystemBased.java | 32 +
.../configuration/HierarchicalConfiguration.java | 1769 ++++++++++++++
.../HierarchicalConfigurationConverter.java | 203 ++
.../HierarchicalConfigurationXMLReader.java | 211 ++
.../HierarchicalINIConfiguration.java | 884 +++++++
.../HierarchicalReloadableConfiguration.java | 81 +
.../HierarchicalXMLConfiguration.java | 42 +
.../commons/configuration/INIConfiguration.java | 493 ++++
.../commons/configuration/JNDIConfiguration.java | 482 ++++
.../org/apache/commons/configuration/Lock.java | 84 +
.../commons/configuration/MapConfiguration.java | 295 +++
.../MultiFileHierarchicalConfiguration.java | 871 +++++++
.../PatternSubtreeConfigurationWrapper.java | 541 +++++
.../configuration/PrefixedKeysIterator.java | 125 +
.../configuration/PropertiesConfiguration.java | 1516 ++++++++++++
.../PropertiesConfigurationLayout.java | 1064 +++++++++
.../commons/configuration/PropertyConverter.java | 1062 +++++++++
.../StrictConfigurationComparator.java | 82 +
.../configuration/SubnodeConfiguration.java | 347 +++
.../commons/configuration/SubsetConfiguration.java | 386 ++++
.../commons/configuration/SystemConfiguration.java | 97 +
.../commons/configuration/VFSFileSystem.java | 409 ++++
.../configuration/VerifiableOutputStream.java | 31 +
.../commons/configuration/XMLConfiguration.java | 1650 ++++++++++++++
.../configuration/XMLPropertiesConfiguration.java | 425 ++++
.../configuration/beanutils/BeanDeclaration.java | 102 +
.../configuration/beanutils/BeanFactory.java | 78 +
.../configuration/beanutils/BeanHelper.java | 521 +++++
.../beanutils/ConfigurationDynaBean.java | 244 ++
.../beanutils/ConfigurationDynaClass.java | 156 ++
.../beanutils/DefaultBeanFactory.java | 110 +
.../beanutils/XMLBeanDeclaration.java | 428 ++++
.../commons/configuration/beanutils/package.html | 35 +
.../event/ConfigurationErrorEvent.java | 94 +
.../event/ConfigurationErrorListener.java | 51 +
.../configuration/event/ConfigurationEvent.java | 159 ++
.../configuration/event/ConfigurationListener.java | 45 +
.../commons/configuration/event/EventSource.java | 364 +++
.../commons/configuration/event/package.html | 32 +
.../interpol/ConfigurationInterpolator.java | 389 ++++
.../configuration/interpol/ConstantLookup.java | 172 ++
.../configuration/interpol/EnvironmentLookup.java | 65 +
.../commons/configuration/interpol/ExprLookup.java | 334 +++
.../commons/configuration/interpol/package.html | 31 +
.../org/apache/commons/configuration/package.html | 33 +
.../plist/PropertyListConfiguration.java | 713 ++++++
.../plist/XMLPropertyListConfiguration.java | 804 +++++++
.../commons/configuration/plist/package.html | 31 +
.../reloading/FileChangedReloadingStrategy.java | 226 ++
.../reloading/InvariantReloadingStrategy.java | 47 +
.../reloading/ManagedReloadingStrategy.java | 94 +
.../reloading/ManagedReloadingStrategyMBean.java | 32 +
.../configuration/reloading/Reloadable.java | 31 +
.../configuration/reloading/ReloadingStrategy.java | 55 +
.../reloading/VFSFileChangedReloadingStrategy.java | 208 ++
.../commons/configuration/reloading/package.html | 34 +
.../configuration/resolver/CatalogResolver.java | 521 +++++
.../resolver/DefaultEntityResolver.java | 130 ++
.../configuration/resolver/EntityRegistry.java | 63 +
.../resolver/EntityResolverSupport.java | 41 +
.../commons/configuration/resolver/package.html | 31 +
.../configuration/tree/ConfigurationNode.java | 269 +++
.../tree/ConfigurationNodeVisitor.java | 67 +
.../tree/ConfigurationNodeVisitorAdapter.java | 65 +
.../tree/DefaultConfigurationKey.java | 876 +++++++
.../tree/DefaultConfigurationNode.java | 717 ++++++
.../tree/DefaultExpressionEngine.java | 545 +++++
.../configuration/tree/ExpressionEngine.java | 92 +
.../commons/configuration/tree/MergeCombiner.java | 164 ++
.../commons/configuration/tree/NodeAddData.java | 196 ++
.../commons/configuration/tree/NodeCombiner.java | 124 +
.../configuration/tree/OverrideCombiner.java | 153 ++
.../commons/configuration/tree/TreeUtils.java | 82 +
.../commons/configuration/tree/UnionCombiner.java | 211 ++
.../commons/configuration/tree/ViewNode.java | 119 +
.../apache/commons/configuration/tree/package.html | 31 +
.../xpath/ConfigurationNodeIteratorAttribute.java | 80 +
.../tree/xpath/ConfigurationNodeIteratorBase.java | 197 ++
.../xpath/ConfigurationNodeIteratorChildren.java | 144 ++
.../tree/xpath/ConfigurationNodePointer.java | 270 +++
.../xpath/ConfigurationNodePointerFactory.java | 89 +
.../tree/xpath/XPathExpressionEngine.java | 426 ++++
.../commons/configuration/tree/xpath/package.html | 32 +
.../configuration/web/AppletConfiguration.java | 65 +
.../configuration/web/BaseWebConfiguration.java | 113 +
.../configuration/web/ServletConfiguration.java | 75 +
.../web/ServletContextConfiguration.java | 76 +
.../web/ServletFilterConfiguration.java | 63 +
.../web/ServletRequestConfiguration.java | 91 +
.../apache/commons/configuration/web/package.html | 32 +
src/main/javacc/PropertyListParser.jj | 291 +++
src/main/resources/PropertyList-1.0.dtd | 35 +
src/main/resources/digesterRules.xml | 42 +
src/main/resources/properties.dtd | 29 +
src/site/resources/images/logo.png | Bin 0 -> 15567 bytes
src/site/resources/images/logo.xcf | Bin 0 -> 27319 bytes
src/site/resources/profile.cobertura | 0
src/site/site.xml | 42 +
src/site/xdoc/building.xml | 120 +
src/site/xdoc/dependencies.xml | 173 ++
src/site/xdoc/download_configuration.xml | 138 ++
src/site/xdoc/index.xml | 101 +
src/site/xdoc/issue-tracking.xml | 102 +
src/site/xdoc/javadocarchive.xml | 46 +
src/site/xdoc/mail-lists.xml | 202 ++
src/site/xdoc/userguide/howto_basicfeatures.xml | 361 +++
src/site/xdoc/userguide/howto_beans.xml | 358 +++
.../xdoc/userguide/howto_combinedconfiguration.xml | 758 ++++++
.../userguide/howto_compositeconfiguration.xml | 101 +
.../xdoc/userguide/howto_configurationbuilder.xml | 894 ++++++++
src/site/xdoc/userguide/howto_events.xml | 240 ++
src/site/xdoc/userguide/howto_filebased.xml | 244 ++
src/site/xdoc/userguide/howto_filesystems.xml | 131 ++
src/site/xdoc/userguide/howto_multitenant.xml | 171 ++
src/site/xdoc/userguide/howto_properties.xml | 445 ++++
src/site/xdoc/userguide/howto_utilities.xml | 279 +++
src/site/xdoc/userguide/howto_xml.xml | 1163 ++++++++++
src/site/xdoc/userguide/overview.xml | 213 ++
src/site/xdoc/userguide/user_guide.xml | 148 ++
.../configuration/BaseNonStringProperties.java | 191 ++
.../commons/configuration/ConfigurationAssert.java | 136 ++
.../ConfigurationErrorListenerImpl.java | 104 +
.../DatabaseConfigurationTestHelper.java | 230 ++
.../configuration/FileURLStreamHandler.java | 64 +
.../configuration/InterpolationTestHelper.java | 257 +++
.../org/apache/commons/configuration/Logging.java | 271 +++
.../configuration/MockInitialContextFactory.java | 241 ++
.../configuration/NonCloneableConfiguration.java | 70 +
.../commons/configuration/NonStringTestHolder.java | 160 ++
.../configuration/TestAbstractConfiguration.java | 195 ++
.../TestAbstractConfigurationBasicFeatures.java | 529 +++++
.../configuration/TestBaseConfiguration.java | 733 ++++++
.../TestBaseConfigurationXMLReader.java | 161 ++
.../configuration/TestBaseNullConfiguration.java | 434 ++++
.../commons/configuration/TestCatalogResolver.java | 106 +
.../configuration/TestCombinedConfiguration.java | 999 ++++++++
.../configuration/TestCompositeConfiguration.java | 931 ++++++++
...tCompositeConfigurationNonStringProperties.java | 41 +
.../commons/configuration/TestConfiguration.java | 46 +
.../configuration/TestConfigurationConverter.java | 141 ++
.../configuration/TestConfigurationFactory.java | 407 ++++
.../configuration/TestConfigurationKey.java | 262 +++
.../configuration/TestConfigurationMap.java | 92 +
.../configuration/TestConfigurationSet.java | 104 +
.../configuration/TestConfigurationUtils.java | 459 ++++
.../configuration/TestDataConfiguration.java | 2402 ++++++++++++++++++++
.../configuration/TestDatabaseConfiguration.java | 551 +++++
.../TestDefaultConfigurationBuilder.java | 1279 +++++++++++
.../TestDynamicCombinedConfiguration.java | 371 +++
.../TestEnvironmentConfiguration.java | 103 +
.../commons/configuration/TestEqualBehaviour.java | 258 +++
.../commons/configuration/TestEqualsProperty.java | 42 +
.../configuration/TestFileConfiguration.java | 652 ++++++
.../TestHierarchicalConfiguration.java | 1252 ++++++++++
.../TestHierarchicalConfigurationXMLReader.java | 77 +
.../TestHierarchicalINIConfiguration.java | 971 ++++++++
.../TestHierarchicalXMLConfiguration.java | 296 +++
.../configuration/TestINIConfiguration.java | 256 +++
.../configuration/TestJNDIConfiguration.java | 375 +++
.../configuration/TestJNDIEnvironmentValues.java | 152 ++
.../configuration/TestMapConfiguration.java | 159 ++
.../TestMapConfigurationRegression.java | 39 +
.../TestMultiFileHierarchicalConfiguration.java | 316 +++
.../configuration/TestNonStringProperties.java | 38 +
.../TestNullCompositeConfiguration.java | 451 ++++
.../TestNullJNDIEnvironmentValues.java | 151 ++
.../TestPatternSubtreeConfiguration.java | 71 +
.../configuration/TestPropertiesConfiguration.java | 1359 +++++++++++
.../TestPropertiesConfigurationLayout.java | 845 +++++++
.../configuration/TestPropertiesSequence.java | 146 ++
.../configuration/TestPropertyConverter.java | 390 ++++
.../TestStrictConfigurationComparator.java | 95 +
.../configuration/TestSubnodeConfiguration.java | 625 +++++
.../configuration/TestSubsetConfiguration.java | 352 +++
.../configuration/TestSystemConfiguration.java | 52 +
.../TestSystemConfigurationRegression.java | 35 +
.../configuration/TestThreesomeConfiguration.java | 67 +
.../configuration/TestVFSConfigurationBuilder.java | 1200 ++++++++++
.../TestWebdavConfigurationBuilder.java | 1059 +++++++++
.../configuration/TestXMLConfiguration.java | 1948 ++++++++++++++++
.../TestXMLPropertiesConfiguration.java | 146 ++
.../beanutils/BeanCreationTestBean.java | 61 +
.../BeanCreationTestBeanWithListChild.java | 66 +
.../configuration/beanutils/TestBeanHelper.java | 613 +++++
.../beanutils/TestConfigurationDynaBean.java | 734 ++++++
.../TestConfigurationDynaBeanXMLConfig.java | 45 +
.../beanutils/TestDefaultBeanFactory.java | 108 +
.../beanutils/TestXMLBeanDeclaration.java | 425 ++++
.../event/AbstractTestConfigurationEvents.java | 189 ++
.../event/AbstractTestFileConfigurationEvents.java | 148 ++
.../event/ConfigurationListenerTestImpl.java | 131 ++
.../event/TestDatabaseConfigurationEvents.java | 56 +
.../configuration/event/TestEventSource.java | 394 ++++
.../event/TestHierarchicalConfigurationEvents.java | 133 ++
.../event/TestMapConfigurationEvents.java | 36 +
.../event/TestPropertiesConfigurationEvents.java | 50 +
.../event/TestSubsetConfigurationEvents.java | 37 +
.../event/TestXMLConfigurationEvents.java | 48 +
.../interpol/TestConfigurationInterpolator.java | 351 +++
.../configuration/interpol/TestConstantLookup.java | 151 ++
.../interpol/TestEnvironmentLookup.java | 71 +
.../configuration/interpol/TestExprLookup.java | 92 +
.../plist/AbstractTestPListEvents.java | 67 +
.../plist/TestPropertyListConfiguration.java | 414 ++++
.../plist/TestPropertyListConfigurationEvents.java | 49 +
.../plist/TestPropertyListParser.java | 81 +
.../plist/TestXMLPropertyListConfiguration.java | 409 ++++
.../TestXMLPropertyListConfigurationEvents.java | 48 +
.../reloading/FileAlwaysReloadingStrategy.java | 51 +
.../reloading/FileRandomReloadingStrategy.java | 86 +
.../TestFileChangedReloadingStrategy.java | 228 ++
.../reloading/TestManagedReloadingStrategy.java | 68 +
.../TestVFSFileChangedReloadingStrategy.java | 159 ++
.../apache/commons/configuration/test/HsqlDB.java | 123 +
.../configuration/tree/AbstractCombinerTest.java | 95 +
.../tree/TestDefaultConfigurationKey.java | 516 +++++
.../tree/TestDefaultConfigurationNode.java | 507 +++++
.../tree/TestDefaultExpressionEngine.java | 539 +++++
.../configuration/tree/TestMergeCombiner.java | 184 ++
.../configuration/tree/TestNodeAddData.java | 107 +
.../configuration/tree/TestOverrideCombiner.java | 178 ++
.../configuration/tree/TestUnionCombiner.java | 136 ++
.../commons/configuration/tree/TestViewNode.java | 144 ++
.../tree/xpath/AbstractXPathTest.java | 156 ++
.../xpath/TestConfigurationIteratorAttributes.java | 103 +
.../TestConfigurationNodeIteratorChildren.java | 247 ++
.../tree/xpath/TestConfigurationNodePointer.java | 191 ++
.../xpath/TestConfigurationNodePointerFactory.java | 178 ++
.../tree/xpath/TestXPathExpressionEngine.java | 449 ++++
.../xpath/TestXPathExpressionEngineInConfig.java | 118 +
.../configuration/web/TestAppletConfiguration.java | 157 ++
.../web/TestServletConfiguration.java | 82 +
.../web/TestServletContextConfiguration.java | 119 +
.../web/TestServletFilterConfiguration.java | 99 +
.../web/TestServletRequestConfiguration.java | 146 ++
.../resources/01/testMultiConfiguration_1001.xml | 39 +
src/test/resources/catalog.xml | 25 +
src/test/resources/catalog2.xml | 26 +
.../resources/config/deep/deepinclude.properties | 15 +
src/test/resources/config/deep/deeptest.properties | 17 +
.../config/deep/deeptestinvalid.properties | 19 +
src/test/resources/config/deep/test.properties | 15 +
.../resources/config/deep/testEqualDeep.properties | 26 +
.../config/deep/testFileFromClasspath.xml | 23 +
src/test/resources/config/test.properties | 25 +
src/test/resources/configA.xml | 19 +
src/test/resources/configB.xml | 19 +
src/test/resources/dataset.xml | 76 +
src/test/resources/include-interpol.properties | 18 +
src/test/resources/include.properties | 19 +
src/test/resources/jndi.properties | 17 +
src/test/resources/log4j-test.xml | 33 +
src/test/resources/resolver.dtd | 19 +
src/test/resources/sample.xml | 28 +
src/test/resources/sample.xsd | 38 +
src/test/resources/sample_1001.xml | 24 +
src/test/resources/test.ini | 17 +
src/test/resources/test.plist | 60 +
src/test/resources/test.plist.xml | 103 +
src/test/resources/test.properties | 126 +
src/test/resources/test.properties.xml | 24 +
src/test/resources/test.xml | 119 +
src/test/resources/test2.plist.xml | 29 +
src/test/resources/test2.properties | 59 +
src/test/resources/testClasspath.properties | 67 +
src/test/resources/testComplexInitialization.xml | 50 +
.../testConfigurationInterpolatorUpdate.xml | 54 +
src/test/resources/testConfigurationProvider.xml | 53 +
.../resources/testConfigurationXMLDocument.xml | 26 +
src/test/resources/testDigesterBadXML.xml | 26 +
src/test/resources/testDigesterConfiguration.xml | 23 +
src/test/resources/testDigesterConfiguration2.xml | 31 +
src/test/resources/testDigesterConfiguration3.xml | 30 +
.../testDigesterConfigurationBasePath.xml | 27 +
.../testDigesterConfigurationInclude1.xml | 53 +
.../testDigesterConfigurationInclude2.properties | 22 +
.../testDigesterConfigurationNamespaceAware.xml | 23 +
.../testDigesterConfigurationOverwrite.properties | 21 +
.../testDigesterConfigurationReverseOrder.xml | 22 +
.../testDigesterConfigurationSysProps.xml | 23 +
.../testDigesterConfigurationWithProps.xml | 21 +
src/test/resources/testDigesterCreateObject.xml | 26 +
.../testDigesterOptionalConfiguration.xml | 33 +
.../testDigesterOptionalConfigurationEx.xml | 26 +
src/test/resources/testDtd.xml | 23 +
src/test/resources/testEncoding.xml | Bin 0 -> 1818 bytes
src/test/resources/testEqual.properties | 26 +
src/test/resources/testEqualDigester.xml | 21 +
src/test/resources/testExpression.xml | 56 +
src/test/resources/testExtendedClass.xml | 50 +
.../testExtendedXMLConfigurationProvider.xml | 54 +
.../resources/testFactoryPropertiesInclude.xml | 24 +
.../testFileReloadConfigurationBuilder.xml | 51 +
.../testFileReloadConfigurationBuilder2.xml | 51 +
src/test/resources/testFileSystem.xml | 54 +
src/test/resources/testGlobalLookup.xml | 53 +
.../resources/testHierarchicalXMLConfiguration.xml | 72 +
.../testHierarchicalXMLConfiguration2.xml | 47 +
src/test/resources/testInterpolation.properties | 22 +
src/test/resources/testInterpolation.xml | 31 +
src/test/resources/testInterpolationBuilder.xml | 31 +
src/test/resources/testMultiConfiguration.xsd | 113 +
src/test/resources/testMultiConfiguration_1001.xml | 39 +
src/test/resources/testMultiConfiguration_1002.xml | 31 +
src/test/resources/testMultiConfiguration_1003.xml | 31 +
src/test/resources/testMultiConfiguration_1004.xml | 26 +
src/test/resources/testMultiConfiguration_2001.xml | 28 +
src/test/resources/testMultiConfiguration_2002.xml | 33 +
src/test/resources/testMultiConfiguration_3001.xml | 40 +
src/test/resources/testMultiConfiguration_3002.xml | 32 +
.../resources/testMultiConfiguration_default.xml | 49 +
src/test/resources/testMultiDynamic_default.xml | 56 +
src/test/resources/testMultiDynamic_default2.xml | 57 +
.../testMultiTenentConfigurationBuilder.xml | 46 +
.../testMultiTenentConfigurationBuilder2.xml | 51 +
.../testMultiTenentConfigurationBuilder3.xml | 51 +
.../testMultiTenentConfigurationBuilder4.xml | 55 +
.../testMultiTenentConfigurationBuilder5.xml | 51 +
src/test/resources/testPatternSubtreeConfig.xml | 61 +
src/test/resources/testResolver.xml | 22 +
src/test/resources/testSequence.properties | 23 +
src/test/resources/testSequenceDigester.xml | 21 +
src/test/resources/testSystemProperties.xml | 32 +
.../testVFSMultiTenentConfigurationBuilder1.xml | 52 +
.../testVFSMultiTenentConfigurationBuilder2.xml | 52 +
src/test/resources/testValidateInvalid.xml | 48 +
src/test/resources/testValidateValid.xml | 46 +
src/test/resources/testValidation.xml | 32 +
src/test/resources/testValidation2.xml | 32 +
src/test/resources/testValidation3.xml | 41 +
src/test/resources/test_invalid_date.plist.xml | 32 +
src/test/resources/testcombine1.xml | 79 +
src/test/resources/testcombine2.xml | 78 +
src/test/resources/testdb.script | 95 +
src/test/resources/threesome.properties | 22 +
376 files changed, 90610 insertions(+)
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/NOTICE.txt b/NOTICE.txt
new file mode 100644
index 0000000..2f528f9
--- /dev/null
+++ b/NOTICE.txt
@@ -0,0 +1,5 @@
+Apache Commons Configuration
+Copyright 2001-2013 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
new file mode 100644
index 0000000..d728742
--- /dev/null
+++ b/RELEASE-NOTES.txt
@@ -0,0 +1,100 @@
+$Id: RELEASE-NOTES.txt 1534410 2013-10-21 23:13:34Z henning $
+
+ Commons Configuration Package
+ Version 1.10
+ Release Notes
+
+
+INTRODUCTION
+============
+
+This document contains the release notes for this version of the Commons
+Configuration component. It describes the changes since the previous version.
+The Commons Configuration software library provides a generic configuration
+interface which enables an application to read configuration data from a variety
+of sources.
+
+The 1.10 release contains a couple of minor bug fixes and improvements. There
+are no important new features. The idea is to release the current changes which
+have been applied to the 1.x branch, so that we can start with new development
+on an improved (and partly binary incompatible) 2.0 version.
+
+As there are small changes only, Commons Configuration 1.10 is fully binary
+compatible to the previous version. The minimum required Java version is 1.5.
+
+Please note that this release was compiled with the Java 1.6 compiler in 1.5
+mode. There is a very small chance that this introduced incompatibilities with
+the Java 1.5 runtime. Java 1.5 was EOLed in October 2009.
+
+Following is a complete list of all changes in the new 1.10 release:
+
+BUG FIXES IN 1.10
+=================
+
+* [CONFIGURATION-500] Attributes in xml config should apply to all entries of a list
+
+ XMLConfiguration now adds attributes of elements defining a list to
+ all list nodes.
+
+* [CONFIGURATION-546] ClassCastException in BeanHelper constructing beans with a list
+ of child beans.
+
+ BeanHelper can now process BeanDefinitions initializing properties of
+ collection types of their target beans.
+
+* [CONFIGURATION-555] XMLConfiguration doesn't seem to be preserving whitespace
+ for the current node where xml:space="preserve" is set.
+
+ Fixed a bug in the handling of the xml:space attribute in XMLConfiguration.
+ The attribute is now also applied to the current element, not only to sub elements.
+
+* [CONFIGURATION-556] Regression with SystemProperties in 1.8 and 1.9
+
+ In 1.7 and before, any change to the system properties was immediately reflected in a
+ SystemConfiguration object. This behaviour broke in 1.8 and 1.9. This has been fixed
+ for 1.10.
+
+* [CONFIGURATION-557] Regression: MapConfiguration no longer accepts a Map<String, String>
+
+ In 1.7 and before, it was possible to pass an arbitrary Map into the constructor of
+ MapConfiguration. With the generification in 1.8, this actually broke and it was no longer
+ possible to pass in e.g. a Map<String, String> because the signature now required a
+ Map<String, Object>. Changing the constructor to accept a Map<String, ?> restores this.
+
+ All of this is purely a compiler issue, the runtime itself does not see any of the generics
+ due to the Java type erasure.
+
+* [CONFIGURATION-558] Configuration no longer accepts List<String> as default for getList()
+
+ Similar to CONFIGURATION-557, the getList(String, List) method was generified to be
+ getList(String, List<Object>) but needs to be getList(String, List<?>) so that code that
+ used a more specific list (such as a List<String>) still compiles against the new API.
+
+
+IMPROVEMENTS AND NEW FEATURES IN 1.10
+=====================================
+
+* [CONFIGURATION-525] PropertiesConfigurationLayout does not preserve comments at bottom of a file
+
+ PropertiesConfiguration now keeps a comment at the bottom of a
+ properties file. A new footer property was added for reading and
+ writing this footer comment.
+
+* [CONFIGURATION-526] Support loading from and saving to DOM nodes
+
+ XMLPropertiesConfiguration now supports loading from and saving to DOM
+ nodes.
+
+* [CONFIGURATION-534] PropertyConfiguration's handling of includes depends on the
+ existence of a base path
+
+ The includesAllowed property of PropertyConfiguration is now independent
+ from the existence of a base path.
+
+* [CONFIGURATION-550] Missing conversion to char
+
+ Conversion to Character is now supported.
+
+
+OTHER CHANGES
+=============
diff --git a/conf/CommonsConfiguration.xsd b/conf/CommonsConfiguration.xsd
new file mode 100644
index 0000000..1b88151
--- /dev/null
+++ b/conf/CommonsConfiguration.xsd
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!--
+ Apache Commons Configuration XML Schema for configuration.xml
+ version: 0.1
+ author : Borut Bolčina
+ date : January 2nd, 2006
+-->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" attributeFormDefault="unqualified">
+ <xs:element name="configuration">
+ <xs:annotation>
+ <xs:documentation>Commons Configuration v0.1</xs:documentation>
+ </xs:annotation>
+ <xs:complexType>
+ <xs:complexContent>
+ <xs:extension base="sources">
+ <xs:sequence minOccurs="0">
+ <xs:element name="override" type="sources" minOccurs="0"/>
+ <xs:element name="additional" type="sources" minOccurs="0"/>
+ </xs:sequence>
+ </xs:extension>
+ </xs:complexContent>
+ </xs:complexType>
+ </xs:element>
+ <xs:complexType name="abstractConfiguration">
+ <xs:attribute name="delimiter" type="xs:string" use="optional"/>
+ <xs:attribute name="throwExceptionOnMissing" type="xs:boolean" use="optional"/>
+ </xs:complexType>
+ <xs:complexType name="hierarhicalConfiguration">
+ <xs:complexContent>
+ <xs:extension base="abstractConfiguration">
+ <xs:attribute name="root" type="xs:string" use="optional"/>
+ </xs:extension>
+ </xs:complexContent>
+ </xs:complexType>
+ <xs:complexType name="abstractHierarchicalFileConfiguration">
+ <xs:complexContent>
+ <xs:extension base="hierarhicalConfiguration">
+ <xs:attribute name="fileName" use="required">
+ <xs:simpleType>
+ <xs:restriction base="xs:string">
+ <xs:minLength value="1"/>
+ <xs:pattern value=".+\.properties"/>
+ <xs:pattern value=".+\.xml"/>
+ <xs:pattern value=".+\.plist"/>
+ </xs:restriction>
+ </xs:simpleType>
+ </xs:attribute>
+ <xs:attribute name="autosave" type="xs:boolean" use="optional"/>
+ <xs:attribute name="basePath" type="xs:string" use="optional"/>
+ <xs:attribute name="encoding" type="xs:string" use="optional"/>
+ <xs:attribute name="url" type="xs:anyURI" use="optional"/>
+ </xs:extension>
+ </xs:complexContent>
+ </xs:complexType>
+ <xs:complexType name="abstractFileConfiguration">
+ <xs:complexContent>
+ <xs:extension base="abstractConfiguration">
+ <xs:attribute name="fileName" use="required">
+ <xs:simpleType>
+ <xs:restriction base="xs:string">
+ <xs:minLength value="1"/>
+ <xs:pattern value=".+\.properties"/>
+ <xs:pattern value=".+\.xml"/>
+ </xs:restriction>
+ </xs:simpleType>
+ </xs:attribute>
+ <xs:attribute name="encoding" type="xs:string" use="optional"/>
+ <xs:attribute name="basePath" type="xs:string" use="optional"/>
+ <xs:attribute name="autosave" type="xs:boolean" use="optional"/>
+ <xs:attribute name="path" type="xs:string" use="optional"/>
+ <xs:attribute name="url" type="xs:anyURI" use="optional"/>
+ </xs:extension>
+ </xs:complexContent>
+ </xs:complexType>
+ <xs:complexType name="propertiesConfiguration">
+ <xs:complexContent>
+ <xs:extension base="abstractFileConfiguration">
+ <xs:attribute name="header" type="xs:string" use="optional"/>
+ <xs:attribute name="include" type="xs:string" use="optional"/>
+ <xs:attribute name="includesAllowed" type="xs:boolean" use="optional"/>
+ </xs:extension>
+ </xs:complexContent>
+ </xs:complexType>
+ <xs:complexType name="xmlPropertiesConfiguration">
+ <xs:complexContent>
+ <xs:extension base="propertiesConfiguration"/>
+ </xs:complexContent>
+ </xs:complexType>
+ <xs:complexType name="xmlConfiguration">
+ <xs:complexContent>
+ <xs:extension base="abstractHierarchicalFileConfiguration">
+ <xs:attribute name="rootElementName" type="xs:string" use="optional"/>
+ <xs:attribute name="validating" type="xs:boolean" use="optional"/>
+ </xs:extension>
+ </xs:complexContent>
+ </xs:complexType>
+ <xs:complexType name="jndiConfiguration">
+ <xs:complexContent>
+ <xs:extension base="abstractConfiguration">
+ <xs:attribute name="prefix" type="xs:string" use="required"/>
+ </xs:extension>
+ </xs:complexContent>
+ </xs:complexType>
+ <xs:complexType name="sources">
+ <xs:choice maxOccurs="unbounded">
+ <xs:element name="system" type="xs:string"/>
+ <xs:element name="properties" type="propertiesConfiguration"/>
+ <xs:element name="xml" type="xmlConfiguration"/>
+ <xs:element name="plist" type="abstractHierarchicalFileConfiguration"/>
+ <xs:element name="jndi" type="jndiConfiguration"/>
+ </xs:choice>
+ </xs:complexType>
+</xs:schema>
diff --git a/conf/HEADER.txt b/conf/HEADER.txt
new file mode 100644
index 0000000..2944f98
--- /dev/null
+++ b/conf/HEADER.txt
@@ -0,0 +1,16 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
diff --git a/conf/README b/conf/README
new file mode 100644
index 0000000..bd219a8
--- /dev/null
+++ b/conf/README
@@ -0,0 +1,4 @@
+DO NOT DELETE THESE FILES!
+
+They're used by the unit tests for testing various Configuration implementations.
+
diff --git a/conf/checkstyle-suppressions.xml b/conf/checkstyle-suppressions.xml
new file mode 100644
index 0000000..8436ca3
--- /dev/null
+++ b/conf/checkstyle-suppressions.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+
+<!DOCTYPE suppressions PUBLIC
+ "-//Puppy Crawl//DTD Suppressions 1.0//EN"
+ "http://www.puppycrawl.com/dtds/suppressions_1_0.dtd">
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- ===================================================================== -->
+<!-- Exceptions for Checkstyle -->
+<!-- $Id: pom.xml 1157212 2011-08-12 18:54:24Z oheger $ -->
+<!-- ===================================================================== -->
+
+<suppressions>
+
+ <suppress checks="MissingSwitchDefault" files="PropertiesConfiguration.java"/>
+
+ <suppress checks="DoubleCheckedLocking" files="DynamicCombinedConfiguration.java"
+ lines="800-900"/>
+</suppressions>
diff --git a/conf/checkstyle.xml b/conf/checkstyle.xml
new file mode 100644
index 0000000..0fdb3f0
--- /dev/null
+++ b/conf/checkstyle.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0"?>
+<!DOCTYPE module PUBLIC
+ "-//Puppy Crawl//DTD Check Configuration 1.1//EN"
+ "http://www.puppycrawl.com/dtds/configuration_1_1.dtd">
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- ===================================================================== -->
+<!-- Checkstyle configuration that checks the commons-configuration coding conventions -->
+<!-- $Id: checkstyle.xml 1361582 2012-07-14 20:35:03Z oheger $ -->
+<!-- ===================================================================== -->
+
+<module name="Checker">
+ <property name="localeLanguage" value="en"/>
+
+ <!-- Checks that a package.html file exists for each package. -->
+ <!-- See http://checkstyle.sf.net/config_javadoc.html#PackageHtml -->
+ <module name="JavadocPackage">
+ <property name="allowLegacy" value="true"/>
+ </module>
+
+ <!-- Checks whether files end with a new line. -->
+ <!-- See http://checkstyle.sf.net/config_misc.html#NewlineAtEndOfFile -->
+ <module name="NewlineAtEndOfFile"/>
+
+ <!-- Checks that property files contain the same keys. -->
+ <!-- See http://checkstyle.sf.net/config_misc.html#Translation -->
+ <module name="Translation"/>
+
+ <!-- Exceptions -->
+ <module name="SuppressionFilter">
+ <property name="file" value="${basedir}/conf/checkstyle-suppressions.xml"/>
+ </module>
+
+ <!-- Checks for Headers -->
+ <!-- See http://checkstyle.sf.net/config_header.html -->
+ <module name="Header">
+ <property name="headerFile" value="${basedir}/conf/HEADER.txt"/>
+ <property name="ignoreLines" value="2"/>
+ </module>
+
+ <module name="FileTabCharacter"/>
+ <module name="RegexpSingleline">
+ <!-- \s matches whitespace character, $ matches end of line. -->
+ <property name="format" value="\s+$"/>
+ <property name="message" value="Line has trailing spaces."/>
+ </module>
+ <module name="FileLength"/>
+
+ <module name="TreeWalker">
+
+ <property name="cacheFile" value="${checkstyle.cache.file}"/>
+
+ <!-- Checks for Javadoc comments. -->
+ <!-- See http://checkstyle.sf.net/config_javadoc.html -->
+ <module name="JavadocMethod">
+ <property name="scope" value="public"/>
+ <property name="allowUndeclaredRTE" value="true"/>
+ <property name="allowMissingJavadoc" value="true"/>
+ </module>
+ <module name="JavadocType">
+ <property name="authorFormat" value="\S"/>
+ </module>
+ <module name="JavadocVariable"/>
+
+
+ <!-- Checks for Naming Conventions. -->
+ <!-- See http://checkstyle.sf.net/config_naming.html -->
+ <module name="ConstantName"/>
+ <module name="LocalFinalVariableName"/>
+ <module name="LocalVariableName"/>
+ <module name="MemberName"/>
+ <module name="MethodName"/>
+ <module name="PackageName"/>
+ <module name="ParameterName"/>
+ <module name="StaticVariableName"/>
+ <module name="TypeName"/>
+
+
+ <!-- Following interprets the header file as regular expressions. -->
+ <!-- <module name="RegexpHeader"/> -->
+
+
+ <!-- Checks for imports -->
+ <!-- See http://checkstyle.sf.net/config_import.html -->
+ <module name="AvoidStarImport"/>
+ <module name="IllegalImport"/> <!-- defaults to sun.* packages -->
+ <module name="RedundantImport"/>
+ <module name="UnusedImports"/>
+
+
+ <!-- Checks for Size Violations. -->
+ <!-- See http://checkstyle.sf.net/config_sizes.html -->
+ <module name="LineLength">
+ <property name="max" value="120"/>
+ </module>
+ <module name="MethodLength"/>
+ <module name="ParameterNumber"/>
+
+
+ <!-- Checks for whitespace -->
+ <!-- See http://checkstyle.sf.net/config_whitespace.html -->
+ <module name="EmptyForIteratorPad"/>
+ <module name="NoWhitespaceAfter"/>
+ <module name="NoWhitespaceBefore"/>
+ <module name="OperatorWrap"/>
+ <module name="ParenPad"/>
+ <module name="WhitespaceAfter"/>
+ <module name="WhitespaceAround"/>
+
+
+ <!-- Modifier Checks -->
+ <!-- See http://checkstyle.sf.net/config_modifiers.html -->
+ <module name="ModifierOrder"/>
+ <module name="RedundantModifier"/>
+
+
+ <!-- Checks for blocks. You know, those {}'s -->
+ <!-- See http://checkstyle.sf.net/config_blocks.html -->
+ <module name="AvoidNestedBlocks"/>
+ <module name="EmptyBlock"/>
+ <module name="LeftCurly">
+ <property name="option" value="nl"/>
+ </module>
+ <module name="NeedBraces"/>
+ <module name="RightCurly">
+ <property name="option" value="alone"/>
+ </module>
+
+
+ <!-- Checks for common coding problems -->
+ <!-- See http://checkstyle.sf.net/config_coding.html -->
+ <module name="CovariantEquals"/>
+ <module name="DoubleCheckedLocking"/>
+ <module name="EqualsHashCode"/>
+ <module name="IllegalInstantiation"/>
+ <module name="InnerAssignment"/>
+ <module name="MagicNumber">
+ <property name="ignoreNumbers" value="-1,0,1,2,3"/>
+ </module>
+ <module name="RedundantThrows">
+ <property name="allowUnchecked" value="true"/>
+ </module>
+ <module name="SimplifyBooleanExpression"/>
+ <module name="SimplifyBooleanReturn"/>
+ <module name="StringLiteralEquality"/>
+ <module name="SuperClone"/>
+ <module name="SuperFinalize"/>
+ <module name="DeclarationOrder"/>
+ <module name="ExplicitInitialization"/>
+ <module name="DefaultComesLast"/>
+ <module name="FallThrough"/>
+ <module name="MultipleVariableDeclarations"/>
+ <module name="UnnecessaryParentheses"/>
+
+ <!-- Checks for class design -->
+ <!-- See http://checkstyle.sf.net/config_design.html -->
+ <module name="FinalClass"/>
+ <module name="HideUtilityClassConstructor"/>
+ <module name="InterfaceIsType"/>
+ <module name="VisibilityModifier">
+ <property name="protectedAllowed" value="true"/>
+ </module>
+
+
+
+ <!-- Miscellaneous other checks. -->
+ <!-- See http://checkstyle.sf.net/config_misc.html -->
+ <module name="ArrayTypeStyle"/>
+ <module name="TodoComment"/>
+ <module name="UpperEll"/>
+
+ </module>
+
+</module>
diff --git a/conf/findbugs-exclude-filter.xml b/conf/findbugs-exclude-filter.xml
new file mode 100644
index 0000000..ecd5027
--- /dev/null
+++ b/conf/findbugs-exclude-filter.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- ===================================================================== -->
+<!-- $Id: findbugs-exclude-filter.xml 1197763 2011-11-04 20:48:02Z oheger $ -->
+<!-- ===================================================================== -->
+<FindBugsFilter>
+ <!-- Enable only high priority warnings -->
+ <Match>
+ <Priority value="2"/>
+ </Match>
+
+ <Match>
+ <Priority value="3"/>
+ </Match>
+</FindBugsFilter>
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..0018092
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,681 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- ===================================================================== -->
+<!-- $Id: pom.xml 1535307 2013-10-24 08:18:50Z henning $ -->
+<!-- ===================================================================== -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-parent</artifactId>
+ <version>32</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>commons-configuration</groupId>
+ <artifactId>commons-configuration</artifactId>
+ <version>1.10</version>
+ <name>Apache Commons Configuration</name>
+
+ <inceptionYear>2001</inceptionYear>
+ <description>Tools to assist in the reading of configuration/preferences files in various formats.</description>
+
+ <url>http://commons.apache.org/configuration/</url>
+
+ <issueManagement>
+ <system>jira</system>
+ <url>http://issues.apache.org/jira/browse/CONFIGURATION</url>
+ </issueManagement>
+
+ <scm>
+ <connection>scm:svn:http://svn.apache.org/repos/asf/commons/proper/configuration/tags/CONFIGURATION_1_10RC2</connection>
+ <developerConnection>scm:svn:https://svn.apache.org/repos/asf/commons/proper/configuration/tags/CONFIGURATION_1_10RC2</developerConnection>
+ <url>http://svn.apache.org/viewvc/commons/proper/configuration/tags/CONFIGURATION_1_10RC2</url>
+ </scm>
+
+ <distributionManagement>
+ <site>
+ <id>apache.website</id>
+ <url>${commons.deployment.protocol}://people.apache.org/www/commons.apache.org/${commons.componentid}</url>
+ </site>
+ </distributionManagement>
+
+ <developers>
+ <developer>
+ <name>Daniel Rall</name>
+ <id>dlr</id>
+ <email>dlr at finemaltcoding.com</email>
+ <organization>CollabNet, Inc.</organization>
+ <roles>
+ <role>Java Developer</role>
+ </roles>
+ </developer>
+
+ <developer>
+ <name>Jason van Zyl</name>
+ <id>jvanzyl</id>
+ <email>jason at zenplex.com</email>
+ <organization>Zenplex</organization>
+ <roles>
+ <role>Java Developer</role>
+ </roles>
+ </developer>
+
+ <developer>
+ <name>Martin Poeschl</name>
+ <id>mpoeschl</id>
+ <email>mpoeschl at marmot.at</email>
+ <organization>tucana.at</organization>
+ <roles>
+ <role>Java Developer</role>
+ </roles>
+ </developer>
+
+ <developer>
+ <name>dIon Gillard</name>
+ <id>dion</id>
+ <email>dion at multitask.com.au</email>
+ <organization>Multitask Consulting</organization>
+ <roles>
+ <role>Java Developer</role>
+ </roles>
+ </developer>
+
+ <developer>
+ <name>Henning P. Schmiedehausen</name>
+ <id>henning</id>
+ <email>henning at schmiedehausen.org</email>
+ <timezone>-8</timezone>
+ <roles>
+ <role>Java Developer</role>
+ </roles>
+ </developer>
+
+ <developer>
+ <name>Eric Pugh</name>
+ <id>epugh</id>
+ <email>epugh at upstate.com</email>
+ <organization>upstate.com</organization>
+ <roles>
+ <role>Java Developer</role>
+ </roles>
+ </developer>
+
+ <developer>
+ <name>Brian E. Dunbar</name>
+ <id>bdunbar</id>
+ <email>bdunbar at dunbarconsulting.org</email>
+ <organization>dunbarconsulting.org</organization>
+ <roles>
+ <role>Java Developer</role>
+ </roles>
+ </developer>
+
+ <developer>
+ <name>Emmanuel Bourg</name>
+ <id>ebourg</id>
+ <email>ebourg at apache.org</email>
+ <organization>Ariane Software</organization>
+ <timezone>+1</timezone>
+ <roles>
+ <role>Java Developer</role>
+ </roles>
+ </developer>
+
+ <developer>
+ <name>Oliver Heger</name>
+ <id>oheger</id>
+ <email>oheger at apache.org</email>
+ <organization>Agfa HealthCare</organization>
+ <timezone>+1</timezone>
+ <roles>
+ <role>Java Developer</role>
+ </roles>
+ </developer>
+
+ <developer>
+ <name>Jörg Schaible</name>
+ <id>joehni</id>
+ <email>joerg.schaible at gmx.de</email>
+ <timezone>+1</timezone>
+ <roles>
+ <role>Java Developer</role>
+ </roles>
+ </developer>
+
+ <developer>
+ <name>Ralph Goers</name>
+ <id>rgoers</id>
+ <email>rgoers at apache.org</email>
+ <organization>Intuit</organization>
+ <timezone>-8</timezone>
+ <roles>
+ <role>Java Developer</role>
+ </roles>
+ </developer>
+ </developers>
+
+ <contributors>
+ <contributor>
+ <name>Konstantin Shaposhnikov</name>
+ <email>ksh at scand.com</email>
+ <organization>scand.com</organization>
+ </contributor>
+
+ <contributor>
+ <name>Jamie M. Guillemette</name>
+ <email>JMGuillemette at gmail.com</email>
+ <organization>TD Bank</organization>
+ </contributor>
+
+ <contributor>
+ <name>Jorge Ferrer</name>
+ <email>jorge.ferrer at gmail.com</email>
+ <organization />
+ </contributor>
+
+ <contributor>
+ <name>Gabriele Garuglieri</name>
+ <email>gabriele.garuglieri at infoblu.it</email>
+ <organization>Infoblu S.p.A</organization>
+ </contributor>
+
+ <contributor>
+ <name>Nicolas De Loof</name>
+ <email>nicolas.deloof at gmail.com</email>
+ <organization>Cap Gemini</organization>
+ </contributor>
+ </contributors>
+
+ <dependencies>
+ <dependency>
+ <groupId>commons-collections</groupId>
+ <artifactId>commons-collections</artifactId>
+ <version>3.2.1</version>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-lang</groupId>
+ <artifactId>commons-lang</artifactId>
+ <version>2.6</version>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-logging</groupId>
+ <artifactId>commons-logging</artifactId>
+ <version>1.1.1</version>
+ <exclusions>
+ <exclusion>
+ <groupId>logkit</groupId>
+ <artifactId>logkit</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>avalon-framework</groupId>
+ <artifactId>avalon-framework</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-digester</groupId>
+ <artifactId>commons-digester</artifactId>
+ <version>1.8.1</version>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-beanutils</groupId>
+ <artifactId>commons-beanutils</artifactId>
+ <version>1.8.3</version>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ <version>1.6</version>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-jexl</artifactId>
+ <version>2.1.1</version>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-vfs2</artifactId>
+ <version>2.0</version>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-jxpath</groupId>
+ <artifactId>commons-jxpath</artifactId>
+ <version>1.3</version>
+ <optional>true</optional>
+ <exclusions>
+ <exclusion>
+ <groupId>xerces</groupId>
+ <artifactId>xerces</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>ant</groupId>
+ <artifactId>ant-optional</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>xml-resolver</groupId>
+ <artifactId>xml-resolver</artifactId>
+ <version>1.2</version>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>servlet-api</artifactId>
+ <version>2.4</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>xerces</groupId>
+ <artifactId>xercesImpl</artifactId>
+ <version>2.6.2</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>xml-apis</groupId>
+ <artifactId>xml-apis</artifactId>
+ <version>1.0.b2</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- Needed for testing -->
+
+ <dependency>
+ <groupId>commons-dbcp</groupId>
+ <artifactId>commons-dbcp</artifactId>
+ <version>1.2.2</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-pool</groupId>
+ <artifactId>commons-pool</artifactId>
+ <version>1.4</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>hsqldb</groupId>
+ <artifactId>hsqldb</artifactId>
+ <version>1.7.2.2</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>dbunit</groupId>
+ <artifactId>dbunit</artifactId>
+ <version>2.1</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.11</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>junit-addons</groupId>
+ <artifactId>junit-addons</artifactId>
+ <version>1.4</version>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>xerces</groupId>
+ <artifactId>xmlParserAPIs</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>xerces</groupId>
+ <artifactId>xercesImpl</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>mockobjects</groupId>
+ <artifactId>mockobjects-core</artifactId>
+ <version>0.09</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>mockobjects</groupId>
+ <artifactId>mockobjects-jdk1.4-j2ee1.3</artifactId>
+ <version>0.09</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.easymock</groupId>
+ <artifactId>easymock</artifactId>
+ <version>3.2</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>javax.mail</groupId>
+ <artifactId>mail</artifactId>
+ <version>1.4</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ <version>1.2.8</version>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <version>1.5.6</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-ext</artifactId>
+ <version>1.5.6</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ <version>1.5.6</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <properties>
+ <commons.componentid>configuration</commons.componentid>
+ <commons.release.version>1.10</commons.release.version>
+ <commons.rc.version>RC1</commons.rc.version>
+ <commons.jira.id>CONFIGURATION</commons.jira.id>
+ <maven.compiler.source>1.5</maven.compiler.source>
+ <maven.compiler.target>1.5</maven.compiler.target>
+
+ <!-- Explicitly declare optional dependencies for the OSGi manifest. -->
+ <commons.osgi.import>
+ org.apache.commons.beanutils.*;resolution:=optional,
+ org.apache.commons.digester.*;resolution:=optional,
+ org.apache.commons.collections.*;resolution:=optional,
+ org.apache.commons.codec.*;resolution:=optional,
+ org.apache.commons.jxpath.*;resolution:=optional,
+ org.apache.xml.resolver.*;resolution:=optional,
+ javax.servlet.*;resolution:=optional,
+ org.apache.commons.jexl2.*;resolution:=optional,
+ org.apache.commons.vfs2.*;resolution:=optional,
+ *
+ </commons.osgi.import>
+ </properties>
+
+ <build>
+ <testResources>
+ <testResource>
+ <directory>src/test/resources</directory>
+ </testResource>
+ <testResource>
+ <directory>src/main/resources</directory>
+ <includes>
+ <include>*.dtd</include>
+ </includes>
+ </testResource>
+ <!-- hack to ensure the N&L appear in jars -->
+ <testResource>
+ <directory>${basedir}</directory>
+ <targetPath>META-INF</targetPath>
+ <includes>
+ <include>NOTICE.txt</include>
+ <include>LICENSE.txt</include>
+ </includes>
+ </testResource>
+ </testResources>
+ <plugins>
+ <plugin>
+ <!-- create the source and binary assemblies -->
+ <artifactId>maven-assembly-plugin</artifactId>
+ <configuration>
+ <descriptors>
+ <descriptor>src/main/assembly/bin.xml</descriptor>
+ <descriptor>src/main/assembly/src.xml</descriptor>
+ </descriptors>
+ <tarLongFileMode>gnu</tarLongFileMode>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <!-- Uncomment to enable profiling unit tests -->
+ <!-- <argLine>-agentpath:"${yourkit.home}/bin/mac/libyjpagent.jnilib"</argLine> -->
+ <forkMode>once</forkMode>
+ <excludes>
+ <exclude>**/TestWebdavConfigurationBuilder.java</exclude>
+ </excludes>
+ <systemPropertyVariables>
+ <java.awt.headless>true</java.awt.headless>
+ <org.apache.commons.logging.Log>org.apache.commons.configuration.Logging</org.apache.commons.logging.Log>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <phase>install</phase>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>javacc-maven-plugin</artifactId>
+ <version>2.6</version>
+ <executions>
+ <execution>
+ <id>javacc</id>
+ <goals>
+ <goal>javacc</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-scm-publish-plugin</artifactId>
+ <configuration>
+ <ignorePathsToDelete>
+ <ignorePathToDelete>javadocs**</ignorePathToDelete>
+ </ignorePathsToDelete>
+ </configuration>
+ </plugin>
+ <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.-->
+ <plugin>
+ <groupId>org.eclipse.m2e</groupId>
+ <artifactId>lifecycle-mapping</artifactId>
+ <version>1.0.0</version>
+ <configuration>
+ <lifecycleMappingMetadata>
+ <pluginExecutions>
+ <pluginExecution>
+ <pluginExecutionFilter>
+ <groupId>
+ org.apache.maven.plugins
+ </groupId>
+ <artifactId>
+ maven-antrun-plugin
+ </artifactId>
+ <versionRange>
+ [0,)
+ </versionRange>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ </pluginExecutionFilter>
+ <action>
+ <ignore />
+ </action>
+ </pluginExecution>
+ <pluginExecution>
+ <pluginExecutionFilter>
+ <groupId>
+ org.codehaus.mojo
+ </groupId>
+ <artifactId>
+ javacc-maven-plugin
+ </artifactId>
+ <versionRange>
+ [0,)
+ </versionRange>
+ <goals>
+ <goal>javacc</goal>
+ </goals>
+ </pluginExecutionFilter>
+ <action>
+ <ignore />
+ </action>
+ </pluginExecution>
+ </pluginExecutions>
+ </lifecycleMappingMetadata>
+ </configuration>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
+ <profiles>
+ <profile>
+ <id>webdav</id>
+ <activation>
+ <activeByDefault>false</activeByDefault>
+ </activation>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.jackrabbit</groupId>
+ <artifactId>jackrabbit-webdav</artifactId>
+ <version>1.5.2</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <forkMode>once</forkMode>
+ <systemPropertyVariables>
+ <java.awt.headless>true</java.awt.headless>
+ <test.webdav.base>${test.webdav.base}</test.webdav.base>
+ </systemPropertyVariables>
+ <includes>
+ <include>**/TestWebdavConfigurationBuilder.java</include>
+ </includes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+ <reporting>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-changes-plugin</artifactId>
+ <configuration>
+ <onlyCurrentVersion>true</onlyCurrentVersion>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>2.7</version>
+ <configuration>
+ <configLocation>${basedir}/conf/checkstyle.xml</configLocation>
+ <suppressionsLocation>${basedir}/conf/checkstyle-suppressions.xml</suppressionsLocation>
+ <enableRulesSummary>false</enableRulesSummary>
+ <propertyExpansion>basedir=${basedir}</propertyExpansion>
+ </configuration>
+ <reportSets>
+ <reportSet>
+ <reports>
+ <report>checkstyle</report>
+ </reports>
+ </reportSet>
+ </reportSets>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.rat</groupId>
+ <artifactId>apache-rat-plugin</artifactId>
+ <configuration>
+ <excludes combine.children="append">
+ <exclude>src/java/org/apache/commons/configuration/plist/*.java</exclude>
+ <exclude>velocity.log</exclude>
+ </excludes>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>findbugs-maven-plugin</artifactId>
+ <version>2.3.1</version>
+ <configuration>
+ <threshold>Normal</threshold>
+ <effort>Default</effort>
+ <excludeFilterFile>${basedir}/conf/findbugs-exclude-filter.xml</excludeFilterFile>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>clirr-maven-plugin</artifactId>
+ <version>${commons.clirr.version}</version>
+ <configuration>
+ <excludes>
+ <exclude>org/apache/commons/configuration/plist/PropertyListParser*</exclude>
+ </excludes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </reporting>
+</project>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
new file mode 100644
index 0000000..7af0286
--- /dev/null
+++ b/src/changes/changes.xml
@@ -0,0 +1,1805 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- ===================================================================== -->
+<!-- $Id: changes.xml 1534402 2013-10-21 22:35:52Z henning $ -->
+<!-- ===================================================================== -->
+<document>
+ <properties>
+ <title>Changes</title>
+ <author email="epugh at upstate.com">Eric Pugh</author>
+ </properties>
+
+ <body>
+ <release version="1.10" date="in SVN"
+ description="Minor bug fixes and improvements">
+ <action dev="oheger" type="update" issue="CONFIGURATION-500">
+ XMLConfiguration now adds attributes of elements defining a list to
+ all list nodes.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-525">
+ PropertiesConfiguration now keeps a comment at the bottom of a
+ properties file. A new footer property was added for reading and
+ writing this footer comment.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-526" due-to="Oliver Kopp">
+ XMLPropertiesConfiguration now supports loading from and saving to DOM
+ nodes.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-534">
+ The includesAllowed property of PropertyConfiguration is now independent
+ from the existence of a base path.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-546" due-to="Justin Couch">
+ BeanHelper can now process BeanDefinitions initializing properties of
+ collection types of their target beans.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-550">
+ Conversion to Character is now supported.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-555">
+ Fixed a bug in the handling of the xml:space attribute in
+ XMLConfiguration. The attribute is now also applied to the current
+ element, not only to sub elements.
+ </action>
+ <action dev="henning" type="fix" issue="CONFIGURATION-556">
+ In 1.7 and before, any change to the system properties was
+ immediately reflected in a SystemConfiguration object. This
+ behaviour broke in 1.8 and 1.9. This has been fixed for 1.10.
+ </action>
+ <action dev="henning" type="fix" issue="CONFIGURATION-557">
+ In 1.7 and before, it was possible to pass an arbitrary Map
+ into the constructor of MapConfiguration. With the
+ generification in 1.8, this actually broke and it was no
+ longer possible to pass in e.g. a Map<String, String>
+ because the signature now required a Map<String,
+ Object>. Changing the constructor to accept a
+ Map<String, ?> restores this.
+ </action>
+ <action dev="henning" type="fix" issue="CONFIGURATION-558">
+ Similar to CONFIGURATION-557, the getList(String, List) method
+ was generified to be getList(String, List<Object>) but
+ needs to be getList(String, List<?>) so that code that
+ used a more specific list (such as a List<String>) still
+ compiles against the new API.
+ </action>
+ </release>
+ <release version="1.9" date="2012-08-08"
+ description="Minor bug fixes and improvements">
+ <action dev="oheger" type="update" issue="CONFIGURATION-503" due-to="Tino Sino">
+ Small changes in user guide documentation.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-502" due-to="Tino Sino">
+ Improvements of basic features and AbstractConfiguration documentation.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-501">
+ XMLPropertyListConfiguration no longer swallows exception caused by
+ invalid date properties. Now a warning message is logged.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-495">
+ List properties can now be set correctly on a HierarchicalConfiguration
+ if delimiter parsing is disabled.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-488">
+ Made static DateFormat fields in XMLPropertyListConfiguration.PListNode
+ final and added a note about proper synchronization.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-487">
+ DataConfiguration.get() now also works with String properties and if no
+ data type conversion is required.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-483">
+ DatabaseConfiguration now always closes the result set.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-482" due-to="Chris Seieroe">
+ The Import-Package section in the OSGi manifest now uses the
+ resolution:=optional directive for optional dependencies.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-481">
+ Variable substitution in configuration sources declared in a definition
+ file for DefaultConfigurationBuilder now works across multiple sources.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-477">
+ PropertyListConfiguration now can deal with C-style comments in plist
+ configuration files. Both block and single-line comments are supported.
+ </action>
+ </release>
+
+ <release version="1.8" date="2012-02-04"
+ description="Support for Java 1.5">
+ <action dev="oheger" type="fix" issue="CONFIGURATION-476">
+ Fixed possible ClassCastExceptions in CompositeConfiguration related to
+ special in-memory configurations.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-475">
+ Class ConfigurationKey was deprecated in favor of DefaultConfigurationKey.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-474">
+ Implemented delimiter parsing in HierarchicalINIConfiguration to be
+ consistent with other Configuration implementations. Note that this can
+ impact existing code. To switch back to the old behavior, call
+ setDelimiterParsingDisabled(true) before loading the configuration.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-471">
+ CompositeConfiguration now provides better support for child
+ configurations that are used as in-memory configuration.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-470">
+ Classes generated by JavaCC are now created dynamically during the
+ build process.
+ </action>
+ <action dev="ebourg" type="add">
+ Commons Configuration now requires Java 5 or later.
+ </action>
+ <action dev="ebourg" type="add" issue="CONFIGURATION-466">
+ Binary literals are now supported (i.e Ob11010001).
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-465">
+ Updated the dependency to Commons Jexl to version 2.1.1. This version
+ provides correct OSGi manifest headers.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-463">
+ Improved documentation of AbstractFileConfiguration related to load()
+ methods and their impact on the base path.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-461">
+ The project now uses standard Maven directory layout.
+ </action>
+ </release>
+
+ <release version="1.7" date="2011-09-07"
+ description="Many bugfixes, some new features.">
+ <action dev="oheger" type="fix" issue="CONFIGURATION-460">
+ Reloading now also works for configuration sources declared in the
+ additional section of a configuration definition file for
+ DefaultConfigurationBuilder.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-459">
+ ConfigurationFactory has been deprecated. The user guide was updated to
+ no more mention this class.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-458">
+ HierarchicalConfiguration now provides a specific implementation of the
+ clear() method. This is more efficient and also solves some other
+ problems related to clearing a SubnodeConfiguration.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-456">
+ Improved Javadocs of getKeys(String) method for some configuration
+ classes.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-455">
+ HierarchicalINIConfiguration.getSection() now creates a section if it
+ does not exist. The SubnodeConfiguration returned by this method is now
+ always connected to the parent ini configuration.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-452">
+ XPathExpressionEngine now provides better support for the setProperty()
+ method.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-448">
+ The parsing of ini files has been improved for property definitions
+ containing multiple separator characters.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-447">
+ DefaultConfigurationBuilder now supports including environment properties
+ using the "env" tag.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-446">
+ XMLConfiguration now handles attributes correctly whose value is an
+ empty string.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-445">
+ Transforming a CombinedConfiguration with ViewNodes to an
+ XMLConfiguration could cause problems with attributes. This has been
+ fixed.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-439">
+ Child configuration builders created for a <configuration> element
+ in a configuration definition file now inherit the configuration and
+ error listeners from the original DefaultConfigurationBuilder. This
+ makes it possible to suppress log output created for optional
+ configurations.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-438" due-to="Mike Noordermeer">
+ JNDIConfiguration.getKeys() no more logs an exception if the prefix does
+ not exist.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-437">
+ Child configuration builders created for a <configuration> element
+ in a configuration definition file now inherit some of their properties
+ from the builder object which processed the file.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-436">
+ The optional dependency to Apache Ant has been changed to the new
+ groupId org.apache.ant. The version was updated to the most recent
+ version 1.8.2 (older versions should still work).
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-434">
+ HierarchicalINIConfiguration now recognizes comment characters in
+ property definitions only if they are preceded by whitespace. Thus
+ comment characters can now be part of the property value. This is for
+ instance required for the definition of file paths which use the
+ semicolon as path separator.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-433">
+ Minor improvements of the support for indexed properties in
+ ConfigurationDynaBean.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-432">
+ The methods getList() and getStringArray() of AbstractConfiguration can
+ now handle single-valued properties of primitive types.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-428">
+ XMLConfiguration no longer escapes backslashs in the values of
+ XML elements.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-424">
+ HierarchicalINIConfiguration now works correctly with configurations
+ that contain only properties in the global section.
+ </action>
+ <action dev="rgoers" type="fix" issue="CONFIGURATION-423" due-to="William Buckley">
+ testFromClassPath() can fail when it should not because of inconsistent escaping of output from
+ PropertiesConfiguration.getURL() and FileChangedReloadingStrategy.getFile().toURL().
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-418">
+ A bug related to the interpretation of escape sequences for backslashes
+ has been fixed. The user guide has also been improved in this area.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-415">
+ Files with a plus character in their names are now handled correctly.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-413" due-to="Alexander Prishchepov">
+ SubsetConfiguration now produces correct events.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-412">
+ DatabaseConfiguration can now be instructed to perform a commit after an
+ update of the managed database table. This makes it usable in
+ environments where the connections do not use auto-commit mode.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-410">
+ Added a refresh() method to AbstractFileConfiguration and
+ AbstractHierarchicalFileConfiguration.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-409">
+ HierarchicalINIConfiguration now correctly saves sections whose name
+ contains delimiter characters.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-408">
+ PropertiesConfiguration.save() escaped slashes in properties values.
+ This was caused by a bug in commons-lang 2.4. Updating to the new
+ version commons-lang 2.5 fixed this problem.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-407">
+ Fixed a potential IllegalStateException in HierarchicalINIConfiguration
+ that can be thrown when the global section is requested concurrently.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-405">
+ XMLPropertyListConfiguration no longer throws a ConfigurationException
+ if the file to be loaded does not have an outer dict element.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-404" due-to="Rob Walker">
+ The default expression engine used by hierarchical configurations used to
+ throw a NumberFormatException if invalid indices were used in property
+ keys. This has been fixed. As a side effect brackets can now be used in
+ property keys.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-403">
+ When an empty XMLConfiguration was saved and reloaded the root element
+ was assigned an empty text value. Because of this isEmpty() returned
+ false for this configuration. This has been fixed.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-399">
+ Default variable interpolation now supports the env: prefix for
+ referencing environment variables.
+ </action>
+ <action dev="rgoers" type="fix" issue="CONFIGURATION-397">
+ Schema violation exceptions are now propagated back to the caller.
+ </action>
+ <action dev="rgoers" type="fix" issue="CONFIGURATION-390">
+ XMLConfiguration and CombinedConfiguraton are now synchronized to fix problems
+ caused by reloading in a multithreaded environment.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-396">
+ HierarchicalConfiguration.NodeVisitor is now passed the correct key to
+ its visitAfterChildren() method.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-393">
+ BaseConfiguration.clone() now also clones collections stored in the
+ internal map. This causes list properties to be handled correctly.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-389">
+ DefaultConfigurationBuilder now supports defining ini files in its
+ configuration definition file.
+ </action>
+ <action dev="rgoers" type="fix" issue="CONFIGURATION-388">
+ Attribute or element values will not be escaped when attribute or element splitting are disabled.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-363">
+ When using Commons Lang 2.6 or higher as dependency nested interpolation
+ in variable names is supported.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-362">
+ Empty dictionaries in a PropertyList configuration are now preserved when the configuration is saved.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-385">
+ DatabaseConfiguration now generates correct events for the clear() and
+ clearProperty() methods.
+ </action>
+ <action dev="rgoers" type="add" issue="CONFIGURATION-380">
+ Add ExprLookup to allow expressions to be evaluated in configurations. When
+ used, this requires that Apache Commons Jexl be added as a dependency to
+ projects using Commons Configuration.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-374">
+ MapConfiguration now provides a way of controlling the trimming
+ behavior.
+ </action>
+ <action dev="rgoers" type="add" issue="CONFIGURATION-378">
+ Added MergeCombiner to allow elements in two configurations to be merged when the
+ element and attributes in the first file match those in the second file.
+ </action>
+ <action dev="rgoers" type="add" issue="CONFIGURATION-340">
+ File system access has been abstracted to a FileSystem interface. Two implementations
+ are provided, DefaultFileSystem that behaves in a backward compatible manner and
+ VFSFileSystem which uses Commons VFS to retreive and store files.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-314">
+ PropertiesConfigurationLayout now allows setting the line separator to
+ be used when writing the properties file.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-371">
+ PropertiesConfigurationLayout now also stores the property separators used for
+ the single properties. It is also possible to change them for specific
+ properties or set a global properties separator. In earlier versions
+ the separator was hard-coded to " = ".
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-370">
+ PropertiesConfiguration now defines a nested interface IOFactory. Using
+ this interface it is possible to inject custom PropertiesReader and
+ PropertiesWriter implementations.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-368">
+ SubnodeConfiguration now fires an event of type EVENT_SUBNODE_CHANGED
+ if a structural change of the parent configuration was detected. If the
+ SubnodeConfiguration is contained in a CombinedConfiguration, the
+ CombinedConfiguration receives this event and can update itself.
+ </action>
+ <action dev="rgoers" type="fix" issue="CONFIGURATION-361">
+ MultiFileHierarchicalConfiguration was not using basepath to
+ construct the file url. It also threw an exception if the
+ file pattern resolved to a non-existent file. This is now
+ configurable.
+ </action>
+ <action dev="joehni" type="update" issue="CONFIGURATION-375">
+ Align interpolation functionality of SubnodeConfiguration and
+ SubsetConfiguration. SubsetConfiguration will now also interpolate
+ keys of the parent configuration or use the local lookups of its
+ parent. SubnodeConfiguration is in turn now able to lookup local
+ keys as well.
+ </action>
+ <action dev="joehni" type="update" issue="CONFIGURATION-376">
+ Align interpolation functionality of SubnodeConfiguration and
+ SubsetConfiguration.
+ </action>
+ <action dev="joehni" type="update" issue="CONFIGURATION-377">
+ Align interpolation functionality of SubnodeConfiguration and
+ SubsetConfiguration.
+ </action>
+ <action dev="joehni" type="fix" issue="CONFIGURATION-369">
+ SubsetConfiguration did not use locally registered lookups of its
+ interpolator.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-359">
+ Fixed broken links to the API documentation in the user's guide.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-358">
+ Improvements of the user's guide for hierarchical configurations.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-357">
+ The message of the ConversionException thrown by
+ AbstractConfiguration.getBigInteger() is now correct.
+ </action>
+ <action dev="rgoers" type="add" issue="CONFIGURATION-356">
+ Added getConfigurations and getConfigurationNameList.
+ </action>
+ <action dev="rgoers" type="add" issue="CONFIGURATION-257">
+ Allow configurations to be validated using XML Schemas.
+ </action>
+ <action dev="rgoers" type="add" issue="CONFIGURATION-355">
+ Allow configurations to be validated using XML Schemas.
+ </action>
+ </release>
+
+ <release version="1.6" date="2008-12-25"
+ description="Another set of smaller bug fixes">
+ <action dev="oheger" type="update">
+ Some dependencies to other Commons components have been updated to the
+ recent versions. Affected are Commons Lang, Commons Collections,
+ Commons Logging, Commons BeanUtils, and Commons JXPath. The older
+ versions should still work.
+ </action>
+ <action dev="rgoers" type="add" issue="CONFIGURATION-353">
+ Allow system properties to be set from a configuration file.
+ </action>
+ <action dev="rgoers" type="add" issue="CONFIGURATION-351">
+ Allow variable resolvers to be defined configured in
+ DefaultConfigurationBuilder.
+ </action>
+ <action dev="rgoers" type="add" issue="CONFIGURATION-350">
+ Added MultiFileHierarchicalConfiguration, DynamicCombinedConfiguration
+ and PatternSubtreeConfigurationWrapper.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-349" due-to="Ralph Goers">
+ The visibility of DefaultConfigurationBuilder.XMLConfigurationProvider
+ was changed from package local to public. This makes it easier to
+ implement providers that create configuration classes derived from
+ XMLConfiguration.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-348">
+ AbstractHierarchicalFileConfiguration.getKeys() now also checks whether
+ a reload is required.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-347">
+ AbstractFileConfiguration.getKeys() now returns an iterator that points
+ to a snapshot of the keys of the configuration. This prevents
+ ConcurrentModificationExceptions during iteration when a reload is
+ performed.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-346">
+ ConfigurationUtils.convertToHierarchical() now creates multiple
+ configuration nodes for properties with multiple values. This
+ improves compatibility with queries.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-345">
+ PropertiesConfiguration now per default uses the encoding "ISO-8859-1"
+ for loading properties files.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-344">
+ CombinedConfiguration could cause a deadlock when it was accessed while
+ concurrently a reload of one of its child configuration happened. This
+ was fixed by reducing synchronization where it is not strictly
+ necessary.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-341">
+ The "force reload check" mechanism of CombinedConfiguration now also
+ works with sub configurations created by configurationAt().
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-339">
+ When performing interpolation the methods getList() and getStringArray()
+ of CompositeConfiguration did not take the order of child configurations
+ into account. This could lead to wrong interpolated values when the key
+ was contained in multiple child configuration. Interpolation is now
+ always done in the correct order.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-338" due-to="David Donn">
+ PropertiesConfiguration now also performs interpolation when searching
+ for include files. This means that the name of a file to include can be
+ determined by another property.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-337" due-to="Ralph Goers">
+ DefaultConfigurationBuilder now supports defining new configuration
+ providers in the configuration definition file.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-336">
+ When converting a flat configuration to a hierarchical one it is now
+ possible to specify the expression engine to be used for this purpose.
+ This may be necessary if the flat configuration contains keys with
+ special characters interpreted by the expression engine.
+ CombinedConfiguration defines the new setConversionExpressionEngine()
+ method. The expression engine passed to this method will be used when
+ converting flat child configurations to hierarchical ones.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-335">
+ XMLConfiguration now allows disabling the attribute splitting mechanism
+ introduced in the 1.5 release (as part of the fix for CONFIGURATION-268).
+ This may be necessary for correctly processing attributes containing
+ both the list delimiter and the attribute delimiter character. The new
+ property "disableAttributeSplitting" was added for this
+ purpose.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-334">
+ Made handling of parent nodes more consistent when setRoot() or
+ setRootNode() of HierarchicalConfiguration are involved.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-332">
+ Properties written through a DataConfiguration to a wrapped
+ PropertiesConfiguration got lost when the PropertiesConfiguration was
+ saved. This has been fixed.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-331">
+ XMLBeanDeclaration now defines a factory method createBeanDeclaration()
+ for creating the declarations for complex nested properties. This
+ method can be overridden by derived classes for injecting custom
+ BeanDeclaration implementations.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-328">
+ A bug in XMLConfiguration.addNodes() made it impossible to add
+ attribute nodes using this method. This has been fixed.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-327">
+ INIConfiguration misinterpreted variables in the global section with
+ a dot in their name as section names. HierarchicalINIConfiguration fixes
+ this problem.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-326">
+ INIConfiguration does not support obtaining a subset for the global
+ section. HierarchicalINIConfiguration provides the getSection() method
+ that returns the content of the global section if null is passed in as
+ section name.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-325">
+ INIConfiguration does not return the global section in its getSections()
+ method. HierarchicalINIConfiguration fixes this problem.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-324">
+ HierarchicalINIConfiguration adds support for line continuation.
+ </action>
+ <action dev="oheger" type="update">
+ INIConfiguration has been deprecated. Its functionality is now available
+ through the new HierarchicalINIConfiguration class.
+ </action>
+ <action dev="oheger" type="add">
+ With HierarchicalINIConfiguration a complete new Configuration
+ implementation for parsing Windows INI files is available. This new
+ class is a full replacement of INIConfiguration and addresses some of its
+ shortcomings. Being derived from HierarchicalConfiguration it offers
+ the enhanced functionality of hierarchical configurations.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-322">
+ ConfigurationDynaBean now works properly with indexed properties
+ stored internally in the underlying configuration as arrays.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-321">
+ The iterator returned by HierarchicalConfiguration.getKeys(String prefix)
+ now also contains the prefix if this key is contained in the
+ configuration.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-320">
+ XMLPropertyListConfiguration is no longer limited to 32 bits integers.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-318">
+ When an XMLConfiguration is created using the copy constructor, the name
+ of the root element is now preserved.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-316">
+ Changing the text of the root element of an XMLConfiguration had no
+ effect when the configuration was saved. This has been fixed.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-315">
+ CombinedConfiguration used to send two EVENT_COMBINED_INVALIDATE events
+ for each modified child configuration. Now this event is sent only
+ once after the affected child configuration was updated.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-307">
+ XMLConfiguration now supports the xml:space attribute. This attribute
+ can be used to preserve whitespace in the content of XML elements.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-306">
+ INIConfiguration now preserves whitespace in quoted values.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-302">
+ If a change has been detected by FileChangedReloadingStrategy, the
+ reloadingRequired() method will now return true until
+ reloadingPerformed() has been called.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-301">
+ Fixed a NullPointerException that could be thrown under certain
+ circumstances when saving an XMLConfiguration that was created using
+ the constructor that takes a HierarchicalConfiguration.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-309">
+ Instantiating an XMLPropertyListConfiguration no longer fails
+ if the DTD is missing from the classpath.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-300">
+ It's now possible to read a configuration file containing
+ a '#' in its name (requires Java 1.4 or above).
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-260">
+ Fixed the date format for XMLPropertyListConfiguration.
+ </action>
+ </release>
+
+ <release version="1.5" date="2007-11-24" description="Many smaller bugfixes">
+ <action dev="oheger" type="update" due-to="Jörg Schaible">
+ Some of the dependencies in the m2 pom have been updated to be more
+ consistent.
+ </action>
+ <action dev="oheger" type="update" due-to="Jörg Schaible">
+ The dependency to commons-logging was updated to the current version
+ 1.1. Older versions of commons-logging will still work.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-273">
+ A new method interpolatedConfiguration() was added to AbstractConfiguration.
+ This method returns a configuration with the same type and
+ content as the original configuration, however all variables have been
+ resolved.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-299">
+ Resolving of variables with the prefix const (constant fields) caused
+ a ClassCastException under certain circumstances if non-String fields
+ were involved. This has been fixed.
+ </action>
+ <action dev="oheger" type="update" due-to="Nicolas De Loof">
+ The dependencies to commons-codec and commons-jxpath have been marked
+ as optional. They are not required by the core classes.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-284" due-to="Nicolas De Loof">
+ There is a new configuration implementation EnvironmentConfiguration,
+ which provides access to (OS) environment variables. On Java >= 1.5
+ this class can be directly used; on earlier versions a dependency to ant
+ is required.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-296">
+ A bug in XMLConfiguration caused that attributes of the root element
+ could not be changed. This has been fixed.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-290">
+ A new method registerEntityId() was added to XMLConfiguration, which
+ allows to register URLs for entities. A new default implementation of
+ the EntityResolver interface handles these entities automatically.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-295">
+ The subset() method of HierarchicalConfiguration now takes the value of
+ the subset's root node into account if it is not ambigous.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-294">
+ Nodes added to a XMLConfiguration using the addNodes() method could
+ lose their value when the configuration was saved. This is now fixed.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-272">
+ New copy() and append() methods have been added to AbstractConfiguration.
+ They replace the methods with the same names in ConfigurationUtils,
+ which do not handle all features of AbstractConfiguration properly (e.g.
+ list delimiters in property values are incorrectly treated). To avoid
+ such problems, the new methods should be used.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-291">
+ The addNodes() method of hierarchical file-based configurations now
+ correctly triggers an auto save.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-287">
+ HierarchicalConfiguration.addNodes() now resets the reference property
+ of all nodes to be added. This fixes a problem with XMLConfiguration,
+ which now detects the added nodes as new and treats them correctly when
+ the configuration is saved.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-285">
+ DefaultConfigurationBuilder will now notify registered error listeners
+ about optional configuration sources that could not be created. Before
+ exceptions thrown by optional configurations were swallowed
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-283">
+ ConfigurationUtils.convertToHierarchical() now correctly deals with
+ property values containing escaped list delimiters. This also affects
+ CombinedConfiguration when sub configurations with such property values
+ are contained.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-275">
+ AbstractConfiguration.addProperty() now correctly deals with list and
+ array properties if delimiter parsing is disabled.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-282">
+ The default expression engine used by HierarchicalConfiguration
+ instances is now lazily initialized. This avoids NullPointerExceptions
+ in certain server environments after a redeploy.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-281">
+ Cycles in the JNDI tree no longer cause a stack overflow in
+ JNDIConfiguration.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-277">
+ The base implementation of clear() in AbstractConfiguration now checks
+ for a potential UnsupportedOperationException when iterating over the
+ existing properties.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-280"
+ due-to="Roman Kurmanowytsch">
+ Using file-based configurations in auto-save mode together with a
+ reloading strategy could cause data loss. This has been fixed.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-279">
+ A PropertiesConfiguration that was created from a non existing file
+ lost its content when it was saved. This problem has been solved.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-215">
+ A new getSource() method was added to CompositeConfiguration and
+ CombinedConfiguration, which returns the child configuration, in which
+ a given property is defined.
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-274">
+ PropertiesConfiguration now supports escaping the escape character for
+ list delimiters.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-269">
+ PropertiesConfiguration no longer escapes the list delimiter on saving
+ if the list delimiter has been disabled.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-270">
+ List properties and properties containing interpolated variables
+ are now properly saved by INIConfiguration.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-268">
+ When delimiter parsing was disabled for XMLConfiguration, saving and
+ loading the configuration accidently added escape characters to properties
+ containing the list delimiter character. This has been fixed. It is now
+ also possible to escape the escape character itself.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-253">
+ The return value of FileConfiguration.getFile() is now always
+ consistent with the result of getURL().
+ </action>
+ <action dev="ebourg" type="update">
+ INIConfiguration uses the platform's specific line separator instead
+ of the Windows line separator.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-267">
+ INIConfiguration flushes the output at the end of a save operation.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-265">
+ For hierarchical file-based configurations the auto-save mechanism is
+ now also triggered if a subnode configuration is changed. In such a case
+ the new event type EVENT_SUBNODE_CHANGED will be sent to registered
+ listeners.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-266" due-to="Tobias Noebel">
+ ConfigurationInterpolator now also invokes the default lookup object for
+ variables with a prefix that could not be resolved by their associated
+ lookup object.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-264">
+ A SubnodeConfiguration per default does not see certain changes of its
+ parent configuration (e.g. reloads). With a new boolean parameter of
+ HierarchicalConfiguration's configurationAt() method a mode can be
+ enabled, in which the subnode configuration checks for such changes and
+ reconstructs itself if necessary.
+ </action>
+ <action dev="ebourg" type="fix">
+ byte[] properties are properly saved as data fields in the plist
+ configurations (PropertyListConfiguration and XMLPropertyListConfiguration).
+ </action>
+ <action dev="ebourg" type="add">
+ DataConfiguration now supports java.net.InetAddress,
+ javax.mail.internet.InternetAddress, and Java 5 enumeration types.
+ Properties are converted to these types using the new generic getters.
+ </action>
+ <action dev="ebourg" type="fix">
+ The object getters in DataConfiguration with no default value
+ (i.e getURL(key)) now throw a NoSuchElementException if the flag
+ throwExceptionOnMissing is set.
+ </action>
+ <action dev="ebourg" type="add">
+ Generic get methods have been added to DataConfiguration (get(),
+ getArray() and getList())
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-263">
+ XMLConfiguration used to drop attributes when an element's value was a
+ list. This has been fixed.
+ </action>
+ <action dev="ebourg" type="add" issue="CONFIGURATION-249">
+ File configurations can now be saved to FTP URLs, or any other URL
+ protocol supporting data output.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-180">
+ Fixed a potential issue in DatabaseConfiguration where an error on
+ closing a statement would prevent the connection from being closed.
+ </action>
+ <action dev="ebourg" type="add" issue="CONFIGURATION-261">
+ Date objects are now supported in ASCII plist files.
+ </action>
+ <action dev="ebourg" type="update">
+ XMLPropertyListConfiguration no longer requires commons-digester and
+ commons-beanutils to work.
+ </action>
+ <action dev="ebourg" type="update">
+ Fixed INIConfiguration to handle the quoted values and the lines
+ containing a value and a comment.
+ </action>
+ </release>
+
+ <release version="1.4" date="2007-04-08" description="Improved interpolation, configuration for INI files, reloading strategy triggered with JMX, bug fixes.">
+ <action dev="oheger" type="update" issue="CONFIGURATION-256">
+ MapConfiguration and the web-based configurations now treat strings
+ that contain an escaped list delimiter correctly: The escape character
+ will be removed, so that for instance "foo\,bar" becomes "foo,bar".
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-255">
+ DatabaseConfiguration now handles list delimiters in property values
+ correctly.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-254" due-to="Carsten Kaiser">
+ After cloning a XMLConfiguration there was still a connection to the
+ original configuration. So when the clone was modified and then saved
+ the content of the original configuration was written. This has now
+ been fixed.
+ </action>
+ <action dev="oheger" type="update">
+ Class loading in BeanHelper is now done using ClassUtils of Commons
+ Lang.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-237" due-to="Nicolas de Loof">
+ With ManagedReloadingStrategy a new reloading strategy for file-based
+ configurations was added that can be triggered through JMX.
+ </action>
+ <action dev="oheger" type="update">
+ The dependencies to Commons Lang, Commons Collections, and Commons Digester
+ are updated to use the recent available version. However older versions
+ will still work.
+ </action>
+ <action dev="oheger" type="add">
+ A pom for maven 2 was added.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-252">
+ ConfigurationUtils.getFile() now always checks first whether the passed
+ in file name is absolute. If it is, this file will be returned. This
+ prevents that on Unix under certain circumstances absolute file names
+ are interpreted as relative ones.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-251">
+ The dependency to xml-apis was changed to the version 1.0.b2. The so
+ far used version 2.0.2 is reported to be bogus.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-245">
+ In addition to configuration event listeners now so-called configuration
+ error listeners are supported. These listeners are notified about
+ internal errors that had been logged and swallowed by privious versions.
+ The new enableRuntimeExceptions() method of ConfigurationUtils
+ registers a special error listener at the passed in configuration that
+ generates a runtime exception when an error event is received.
+ </action>
+ <action dev="oheger" type="add">
+ AbstractConfiguration now allows to set an instance specific logger
+ using the setLogger() method. This gives clients more control over a
+ configuration's logging behavior.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-155">
+ SubsetConfiguration and CompositeConfiguration were updated to fully
+ support an instance specific list delimiter. Concerning splitting of
+ string properties that contain a list delimiter character, these
+ classes now behave like a "normal" configuration.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-192">
+ Variable interpolation features have been improved. A variable can now
+ have the form ${prefix:variableName} where the prefix
+ defines the type of the variable. The standard types sys for
+ system properties and const for constants are supported.
+ Variables without a prefix are treated as references to other
+ configuration properties (which is compatible to earlier versions).
+ </action>
+ <action dev="oheger" type="update">
+ Commons Configuration now depends on Commons Lang 2.2. Some features
+ of Lang's new text package are used.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-244">
+ The number of dependencies needed for DefaultConfigurationBuilder was
+ reduced by letting some of the default configuration providers resolve
+ their classes per reflection.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-240">
+ File-based configurations with a reloading strategy did not work well
+ together with CombinedConfiguration because the reloading strategy is
+ only checked when its associated configuration is accessed (which does
+ not happen when only the combined configuration is queried).
+ As a workaround CombinedConfiguration now provides the boolean
+ forceReloadCheck property. If this is set to true, all contained
+ configurations will be triggered when a property is queried. This will
+ cause a reload if necessary.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-243">
+ Configuration declarations in the configuration definition file for
+ DefaultConfigurationBuilder that are marked as optional now support a
+ new attribute config-forceCreate. If this attribute is set
+ to true and the initialization of the configuration fails,
+ DefaultConfigurationBuilder tries to add an empty configuration of the
+ correct type to the resulting combined configuration. Before this
+ change optional configurations that caused errors were never added to
+ the combined configuration.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-241">
+ CompositeConfiguration.clearProperty() now generates the correct
+ update events.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-242">
+ The configuration returned by HierarchicalConfiguration.subset()
+ performed variable interpolation only in the keys that belong to the
+ subset. Now the parent configuration is searched, too, to resolve the
+ value of the referenced property. This is consistent with the way
+ SubnodeConfiguration works.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-234">
+ DefaultConfigurationBuilder now internally uses the standard expression
+ engine for hierarchical configurations. So the dependency to Commons
+ JXPath is no more needed when this class is used. Note that this change
+ has some impact on existing code that manually sets properties before
+ the combined configuration is created; this code must now be adapted to
+ the changed syntax of property keys.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-236">
+ HierarchicalConfiguration and some of its sub classes now define a
+ copy constructor.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-197" due-to="Trevor Charles Miller">
+ A new configuration class for windows ini files was added.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-229">
+ For file-based configurations loaded by ConfigurationFactory the load()
+ method was called before all of the properties specified by attributes
+ of the XML element have been initialized. Now load() is called after
+ property initialization.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-235">
+ Interpolation of non string values did not work when SubsetConfiguration
+ was involved. This has now been fixed.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-227">
+ The compatibility of ConfigurationDynaBean with other configuration types
+ than those that inherit from BaseConfiguration was improved.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-233" due-to="Rainer Jung">
+ The getList() method of CompositeConfiguration does now fully support
+ variable interpolation. So it is possible to refer to a variable in
+ one (sub) configuration that is defined in another configuration.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-230">
+ XPathExpressionEngine used to create wrong keys for attribute nodes.
+ This caused some operations on XMLConfiguration to fail when such an
+ expression engine was set (e.g. reloading). Now correct keys for
+ attributes are constructed.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-228">
+ Some of the methods of file-based hierarchical configurations (e.g.
+ subset() or configurationAt()) did not take an eventually set reloading
+ strategy into account. This is now fixed by overriding the internal
+ fetchNodeList() method in AbstractHierarchicalFileConfiguration and
+ letting it always check for a reload.
+ </action>
+ </release>
+
+ <release version="1.3" date="2006-09-24">
+ </release>
+
+ <release version="1.3-rc2" date="2006-09-03">
+ <action dev="oheger" type="update" issue="CONFIGURATION-223" due-to="Gabriele Garuglieri">
+ AbstractFileConfiguration now overrides addProperty() and setProperty()
+ instead of addPropertyDirect() to implement the auto save feature.
+ This was necessary to properly integrate PropertiesConfigurationLayout.
+ It has also the advantage that an auto save is triggered only once if
+ multi-valued properties are involved (before a save operation was
+ performed for each property value).
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-222" due-to="Gabriele Garuglieri">
+ The new PropertiesConfigurationLayout class broke the save() operation
+ of PropertiesConfiguration when an instance was newly created and
+ populated in memory. This is fixed now by ensuring that a layout object
+ is immediately created and registered as event listener at the
+ configuration.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-221" due-to="Rainer Jung">
+ ConfigurationFactory now supports variables in its configuration
+ definition files. These variables are resolved using system properties
+ and have the typical ${} syntax.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-216" due-to="Gabriele Garuglieri">
+ There were still some problems with resolving relative paths when
+ configuration files are loaded from classpath. This fix addresses these
+ issues.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-220">
+ DataConfiguration.getDateArray() used to ignore the format argument.
+ This was fixed.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-219">
+ PropertiesConfiguration now defines its own clone() method to handle
+ the associated PropertiesConfigurationLayout object correctly.
+ </action>
+ </release>
+
+ <release version="1.3-rc1" date="2006-07-30">
+ <action dev="oheger" type="update" issue="CONFIGURATION-217">
+ The dependency to servletapi was updated from version 2.3 to version
+ 2.4, but version 2.3 will still work.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-104">
+ A new class PropertiesConfigurationLayout was introduced whose task is
+ to preserve the structure (e.g. comments, blanc lines) of a file
+ loaded by PropertiesConfiguration. Each PropertiesConfiguration
+ object is now associated with such a layout object. A saved properties
+ file will look very similar to its original.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-145">
+ clone() methods have been added to BaseConfiguration, AbstractFileConfiguration,
+ MapConfiguration, CompositeConfiguration, and CombinedConfiguration.
+ So the most important Configuration implementations now support
+ cloning. To ConfigurationUtils an utility method cloneConfiguration()
+ was added that allows to conveniently clone a configuration.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-216">
+ If a configuration file was to be loaded from classpath, the
+ constructor of AbstractFileConfiguration dropped the file's path. The
+ path is now taken into account.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-214">
+ The getter methods for numeric data types in AbstractConfiguration now
+ support conversions between different Number types, e.g. you can now
+ call getLong(key) when key points to an Integer value.
+ </action>
+ <action dev="oheger" type="add">
+ The new class DefaultConfigurationBuilder was added as an alternative to
+ ConfigurationFactory. It provides some more features and creates a
+ CombinedConfiguration object
+ </action>
+ <action dev="oheger" type="add">
+ The new class CombinedConfiguration was added as a hierarchical
+ alternative to CompositeConfiguration.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-143">
+ Support for low-level configuration events was added to all classes
+ derived from AbstractConfiguration. The major part of this is handled
+ by the new super class EventSource of AbstractConfiguration.
+ </action>
+ <action dev="oheger" type="add">
+ A new method convertToHierarchical() was added to ConfigurationUtils,
+ which is able to convert an arbitrary configuration object into a
+ hierarchical configuration.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-63">
+ Loading of file-based configurations no longer throws a NullPointerException
+ in setups where the thread context class loader is not set.
+ </action>
+ <action dev="oheger" type="update">
+ The dependency to dom4j was removed; it was only used by two test classes,
+ which have been re-written.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-100">
+ XMLConfiguration used to drop the DOCTYPE declaration when saving the
+ configuration. It is now able to extract the DTD's public and system ID
+ and write them back (more complex DOCTYPE declarations are still not supported).
+ With the new methods setSystemID() and setPublicID(), the DOCTYPE
+ declaration can be configured.
+ </action>
+ <action dev="ebourg" type="add" issue="CONFIGURATION-178">
+ Added two new constructors in CompositeConfiguration accepting a
+ collection of configurations as a parameter.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-186">
+ (Basic) Support for declaring beans in configuration files was added.
+ Some new classes in the beanutils package allow to create instances from
+ these declarations.
+ </action>
+ <action dev="oheger" type="update">
+ The implementation of the interpolation features have been extracted out
+ off AbstractConfiguration and moved to PropertyConverter. The
+ interpolateHelper() method of AbstractConfiguration is now deprectated
+ and will not be called any more during interpolation.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-182">
+ A new method configurationsAt() was added to HierarchicalConfiguration
+ that provides a convenient way of iterating over complex list-like
+ structures without the need of manually constructing configuration keys
+ with indices.
+ </action>
+ <action dev="oheger" type="add">
+ A new class SubnodeConfiguration was introduced that wraps a configuration
+ node of a HierarchicalConfiguration. All operations performed on this
+ configuration use this wrapped node as root. The new configurationAt()
+ method of HierarchicalConfiguration returns such a SubnodeConfiguration
+ for a specified sub node.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-173">
+ With XPathExpressionEngine an expression engine for hierarchical
+ configurations is now available that can evaluate XPATH expressions in
+ property keys. This expression engine implementation is based on
+ Commons JXPath, which is now declared as a new dependency (but at
+ runtime it is only needed if the XPathExpressionEngine class is used).
+ </action>
+ <action dev="oheger" type="add">
+ The code for interpreting property keys was refactored out off
+ HierarchicalConfiguration. Instead this class now supports pluggable
+ expression engines (using the setExpressionEngine() method). So it is
+ possible to plug in different expression languages. A default expression
+ engine is provided that understands the native expression language used
+ by hierarchical configurations in older versions. During the process of
+ this refactoring some methods of HierarchicalConfiguration have been
+ deprecated; they will not be called any more when searching or adding
+ properties. These are the following: createAddPath(), fetchAddNode(),
+ findLastPathNode(), findPropertyNodes().
+ </action>
+ <action dev="oheger" type="update">
+ A larger refactoring was performed on the inner Node class of
+ HierarchicalConfiguration: A ConfigurationNode interface was extracted
+ for which a default implementation (DefaultConfigurationNode) is
+ provided. HierarchicalConfiguration.Node now extends this default
+ implementation. The new ConfigurationNode interface defines some more
+ methods than the Node class did originally for conveniently dealing with
+ sub nodes and attributes. HierarchicalConfiguration now uses the new
+ type ConfigurationNode whereever possible. Some methods dealing with
+ Node objects have been deprecated and replaced by versions operating on
+ ConfigurationNode objects instead.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-155" due-to="Jorge Ferrer">
+ All configuration classes derived from AbstractConfiguration now allow
+ to set an instance specific list delimiter. This can be done through
+ the new method setListDelimiter(). As before it is possible to define
+ a default list delimiter, which will be used if no instance specific
+ delimiter is set. This can be done using the new setDefaultListDelimiter()
+ method (the methods get/setDelimiter() have been deprecated). With the
+ new setDelimiterParsingDisabled() method parsing of lists can be
+ disabled at all.
+ </action>
+ </release>
+
+ <release version="1.2" date="2005-12-17">
+ </release>
+
+ <release version="1.2-rc3" date="2005-12-07">
+ <action dev="oheger" type="update">
+ Commons Configuration now declares a dependency to Xalan. As with
+ Xerces this dependency is only needed for JDK 1.3. It was introduced
+ in a process of making Configuration buildable on a JDK 1.3. Documentation
+ about the build process was also added.
+ </action>
+ <action dev="oheger" type="update">
+ The dependency to Commons Beanutils Collections was unnecessary and
+ thus removed.
+ </action>
+ <action dev="oheger" type="update">
+ Commons Configuration now depends on Commons Digester 1.6 instead of 1.5.
+ (This was done only to pick up the latest available release of digester.)
+ </action>
+ </release>
+
+ <release version="1.2-rc2" date="2005-11-23">
+ <action dev="oheger" type="update" issue="CONFIGURATION-2">
+ ConfigurationDynaBean now implements the java.util.Map interface (as
+ was stated in the javadocs). This was done by deriving the class from
+ ConfigurationMap.
+ </action>
+ </release>
+
+ <release version="1.2-rc1" date="2005-11-11">
+ <action dev="oheger" type="update" issue="CONFIGURATION-33">
+ The reload() method in AbstractFileConfiguration was updated to prevent
+ reentrant invocation, which may be caused by some methods when they
+ are called during a reloading operation.
+ </action>
+ <action dev="ebourg, oheger" type="update">
+ AbstractHierarchicalFileConfiguration, a new base class for file based
+ hierarchical configurations, was introduced. XMLConfiguration now
+ extends this class.
+ </action>
+ <action dev="oheger" type="update" due-to="Kay Doebl" issue="CONFIGURATION-41">
+ XMLConfiguration now prints the used encoding in the xml declaration of
+ generated files. In earlier versions always the default encoding was
+ written. PropertiesConfiguration now always uses the platform specific
+ line separator when saving files.
+ </action>
+ <action dev="ebourg" type="update" issue="CONFIGURATION-8">
+ PropertiesConfiguration now translates properly the escaped unicode
+ characters (like \u1234) used in the property keys. This complies with
+ the specification of java.util.Properties.
+ </action>
+ <action dev="ebourg" type="update" issue="CONFIGURATION-123">
+ ConfigurationConverter.getProperties() now uses the delimiter of the
+ specified configuration to convert the list properties into strings.
+ </action>
+ <action dev="ebourg" type="update" issue="CONFIGURATION-123">
+ The interpolation of variables (${foo}) is now performed in all property
+ getters of AbstractConfiguration and DataConfiguration. As a side effect
+ the Properties object returned by ConfigurationConverter.getProperties()
+ contains only interpolated values.
+ </action>
+ <action dev="ebourg" type="update" issue="CONFIGURATION-35">
+ PropertiesConfiguration now uses the ISO-8859-1 encoding by default
+ instead of the system encoding to comply with the specification of
+ java.util.Properties.
+ </action>
+ <action dev="ebourg" type="update" issue="CONFIGURATION-44">
+ JNDIConfiguration no longer logs an error when attempting to get
+ a property that doesn't exist in the configuration.
+ </action>
+ <action dev="ebourg" type="update" issue="CONFIGURATION-99">
+ Attempting to load a configuration from a directory instead of a file
+ will now throw a ConfigurationException.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-28">
+ If a multi-valued property was involved in an interpolation operation,
+ AbstractConfiguration created a string representation of the list of all
+ values. This was changed to only use the first value, which makes more
+ sense in this context and is consistent with other getters for single
+ valued properties.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-83">
+ If an include file with a relative path cannot be found in the base
+ path, PropertiesConfiguration now also tries to resolve it based on its
+ own location.
+ </action>
+ <action dev="ebourg" type="update" issue="CONFIGURATION-117" due-to="Steve Bate">
+ Fixed MapConfiguration to store the list of values added under a same
+ key instead of the last value added.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-80">
+ Fixed a bug in the handling of relative file names in ConfigurationFactory:
+ In most cases relative file names were not resolved relative to the
+ location of the configuration definition file as stated in the documentation.
+ This behavior was now changed to always be in sync with the documentation.
+ This may have an impact on existing code which uses workarounds for
+ the erroneous resolving mechanism.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-6">
+ Empty elements or elements whose content consists only of comments or
+ whitespace are now taken into account by XMLConfiguration. They are
+ added to the configuration; their value is an empty string.
+ </action>
+ <action dev="oheger" type="add">
+ XMLConfiguration now sets a valid system id in the InputSource used for
+ loading files. This enables XML parsers to correctly resolve relative
+ files, e.g. DTDs.
+ </action>
+ <action dev="ebourg" type="update" issue="CONFIGURATION-74">
+ getKeys() in HierarchicalConfiguration now returns the keys in the same order the properties were inserted.
+ </action>
+ <action dev="ebourg" type="update">
+ Commons Configuration now depends on Commons Collections 3.1 instead of 3.0
+ </action>
+ <action dev="ebourg" type="add" issue="CONFIGURATION-195">
+ New configurations implementing the "property list" format used in
+ NeXT/OpenStep and its XML variant used in Mac OS X.
+ (PropertyListConfiguration and XMLPropertyListConfiguration)
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-97">
+ Resolved some issues with XMLConfiguration and properties containing
+ the delimiter character. These properties are now correctly treated,
+ escaping the delimiter will work, too.
+ </action>
+ <action dev="ebourg" type="add">
+ Added support for XMLPropertiesConfiguration in ConfigurationFactory.
+ A <properties> element will generate a XMLPropertiesConfiguration
+ if the filename ends with ".xml".
+ </action>
+ <action dev="ebourg" type="add" issue="CONFIGURATION-184">
+ PropertiesConfiguration now supports escaped key/value separators in the keys
+ (i.e foo\:key = bar).
+ </action>
+ <action dev="ebourg" type="add" issue="CONFIGURATION-166">
+ PropertiesConfiguration now supports all key/value separators supported by java.util.Properties
+ ('=', ':' and white space characters).
+ </action>
+ <action dev="ebourg" type="update">
+ Commons Configuration now depends on Commons Lang 2.1 instead of 2.0
+ </action>
+ <action dev="ebourg" type="update" issue="CONFIGURATION-207">
+ Comment lines for PropertiesConfiguration can start with the '!' char (compatibility with java.util.Properties).
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-84">
+ Because ConfigurationUtils.copy() does not fully support hierarchical
+ configurations a clone() method was added to HierarchicalConfiguration
+ that can be used instead.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-206">
+ XMLConfiguration now provides some support for validating XML
+ documents. With the setValidating() method DTD validation can be
+ enabled. It is also possible to set a custom DocumentBuilder allowing
+ a caller to perform enhanced configuration of the XML loading process.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-121">
+ AbstractFileConfiguration now always sets a valid base path if the
+ configuration file could be located. This allows PropertiesConfiguration
+ to resolve include files even when loaded from class path.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-85">
+ Updated XMLConfiguration to correctly deal with properties containing
+ dots in their names. Such properties could not be accessed before.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-9">
+ PropertiesConfiguration's handling of backslash characters at the end
+ of line was incorrect when there was an even number of trailing
+ backslashes. This is now fixed.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-130">
+ Fixed a problem related to file based configurations that are loaded
+ from a URL which is application/x-www-form-urlencoded: the save() method
+ would store such files at a wrong location.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-50">
+ Updated FileChangedReloadingStrategy to use the file based configuration's
+ source URL to find the file to watch. Before that it was possible that
+ the strategy checked the wrong file. For configuration files loaded
+ from a jar FileChangedReloadingStrategy now checks the jar file itself
+ for changes. Finally a bug was fixed which caused the strategy to
+ check the watched file's last change date on every invocation of its
+ reloadingRequired() method ignoring its refresh delay. Thanks to Jorge
+ Ferrer.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-62">
+ Fixed a bug in the collaboration between XMLConfiguration and its
+ reloading strategy: The configuration did not check its reloading
+ strategy, so no reload was performed.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-119">
+ Disabled auto save mode during PropertiesConfiguration.load(). Prior
+ it was possible that the properties file to be loaded was immideately
+ overwritten.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-89">
+ Under certain circumstances it was possible that a reloading strategy
+ set for PropertiesConfiguration interferred with the save() method causing
+ the configuration file to be erased. This has now been fixed.
+ </action>
+ <action dev="oheger" type="update" due-to="Jamie M. Guillemette" issue="CONFIGURATION-94">
+ AbstractFileConfiguration now stores the URL of the config file in the
+ load() methods. This URL is reused by the save() method to ensure that
+ the same file is written.
+ </action>
+ <action dev="ebourg" type="update" due-to="Alistair Young">
+ XMLPropertiesConfiguration no longer depends on Digester to parse the
+ configuration file, it's now implemented with a pure SAX parser.
+ </action>
+ <action dev="oheger" type="update" due-to="Mi Zhang" issue="CONFIGURATION-49">
+ Fixed a bug which causes XMLConfiguration.save to lose attribute values
+ under some circumstances. The clear() method now also ensures that the
+ associated DOM document is always cleared.
+ </action>
+ <action dev="ebourg" type="update" due-to="Kunihara Tetsuya" issue="CONFIGURATION-13">
+ XMLConfiguration now parse the configuration using the encoding
+ declared in the XML header instead of the OS default encoding.
+ </action>
+ <action dev="ebourg" type="update" due-to="Zsolt Koppany">
+ XMLConfiguration now uses the delimiter set by setDelimiter(char).
+ </action>
+ </release>
+
+ <release version="1.1" date="2005-04-02">
+ <action dev="ebourg" type="update" issue="CONFIGURATION-134">
+ Fixed a ConcurrentModificationException thrown when calling clear()
+ on a SubsetConfiguration applied to a BaseConfiguration.
+ </action>
+ <action dev="ebourg" type="update" issue="CONFIGURATION-81">
+ The resolveContainerStore() method in AbstractConfiguration now works
+ properly with arrays of objects and arrays of primitives. This means
+ it is possible to store an array of value in the configuration and
+ retrieve the first element with the getString(), getInt()... methods.
+ </action>
+ </release>
+
+ <release version="1.1-rc2" date="2005-03-06">
+ <action dev="oheger" type="update" issue="CONFIGURATION-118">
+ Updated documentation for FileConfiguration's load() methods. Fixed a
+ problem in XMLConfiguration with the output of the save() method when
+ multiple files were loaded.
+ </action>
+ <action dev="ebourg" type="update">
+ Fixed a bug in FileChangedReloadingStrategy preventing the detection
+ of a file change in some cases.
+ </action>
+ <action dev="ebourg" type="update">
+ Changed getXXXArray() and getXXXList() in DataConfiguration to return
+ an empty array/list for empty values.
+ </action>
+ <action dev="ebourg" type="update" issue="CONFIGURATION-58">
+ Fixed getLongArray(), getFloatArray() and getDoubleArray() in DataConfiguration,
+ the values were cast into integers.
+ </action>
+ </release>
+
+ <release version="1.1-rc1" date="2005-02-13">
+ <action dev="oheger" type="add" issue="CONFIGURATION-88">
+ ConfigurationFactory now always configures digester to use the context
+ classloader. This avoids problems in application server environments,
+ which use their own version of digester. Thanks to Mike Colbert for the
+ patch!
+ </action>
+ <action dev="ebourg" type="add" issue="CONFIGURATION-148">
+ Added a new configuration, XMLPropertiesConfiguration, supporting the
+ new XML format for java.util.Properties introduced in Java 1.5.
+ A 1.5 runtime is not required to use this class.
+ </action>
+ <action dev="ebourg" type="add" issue="CONFIGURATION-190">
+ Added a comment header to PropertiesConfiguration. The header is not
+ parsed when the file is loaded yet.
+ </action>
+ <action dev="ebourg" type="add">
+ Added the setEncoding(String) and the getEncoding() methods to the
+ FileConfiguration interface to control the encoding of the
+ configuration file.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-210">
+ Access to the top level element of the XML document is now provided. For
+ newly created configurations this element can be changed before the
+ document is written.
+ </action>
+ <action dev="oheger" type="update" issue="CONFIGURATION-168">
+ Merged the two XML related configuration classes into one new class
+ XMLConfiguration. This new class should provide the best of its
+ ancestors.
+ </action>
+ <action dev="ebourg" type="update">
+ Replaced the PropertyTokenizer inner class in AbstractConfiguration
+ with the split method in PropertyConverter. Also moved the method
+ building an iterator on the elements of a composite value in
+ PropertyConverter as toIterator().
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-15">
+ Some cleanup of the handling of the base path in file based configurations.
+ The base path is now always taken into account.
+ </action>
+ <action dev="ebourg" type="fix">
+ Calling getProperties on a JNDIConfiguration no longer throws an
+ UnsupportedOperationException.
+ </action>
+ <action dev="ebourg" type="remove">
+ Removed the getPropertyDirect method from AbstractConfiguration,
+ concrete configurations now implement directly the getProperty method
+ from the Configuration interface.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-187">
+ Added implementation of a save() method for HierarchicalXMLConfiguration.
+ </action>
+ <action dev="ebourg" type="update">
+ Constructing a file based configuration with a File no longer throws
+ an exception when the file doesn't exist.
+ </action>
+ <action dev="ebourg" type="add">
+ Saving a configuration now creates the path to the file if it doesn't exist.
+ </action>
+ <action dev="ebourg" type="update" issue="CONFIGURATION-45">
+ AbstractFileConfiguration.save(File) no longer fails silently when
+ an error occurs, a ConfigurationException is thrown instead.
+ </action>
+ <action dev="ebourg" type="fix">
+ ConfigurationUtils.locate() now checks if the URL based resources exist.
+ This fixes a bug preventing configuration files from being found if
+ the configuration descriptor is in a JAR file (reported by Grant Ingersoll).
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-96">
+ Fixed NPE that were caused in the constructors of file based
+ configurations if an invalid file name was specified.
+ </action>
+ <action dev="oheger" type="add" issue="CONFIGURATION-162">
+ Added support for optional configuration sources in definition files for
+ ConfigurationFactory. A new optional attribute allows to specify whether a
+ configuration source is mandatory or optional.
+ </action>
+ <action dev="ebourg" type="fix">
+ JNDIConfiguration.getKeys() now returns an empty iterator instead of
+ throwing a ConfigurationRuntimeException when a NamingException occurs.
+ The NamingExceptions are now logged.
+ </action>
+ <action dev="ebourg" type="fix">
+ DatabaseConfiguration.isEmpty() now returns true if an SQLException occurs.
+ </action>
+ <action dev="ebourg" type="add">
+ Added two methods copy(Configuration, Configuration) and
+ append(Configuration, Configuration) in ConfigurationUtils to copy
+ properties between configurations.
+ </action>
+ <action dev="ebourg" type="update">
+ Moved the constructors implementations from PropertiesConfiguration and
+ XMLConfiguration to AbstractFileConfiguration.
+ </action>
+ <action dev="epugh" type="remove">
+ Remove deprecated getVector() implementations.
+ </action>
+ <action dev="ebourg" type="add" issue="CONFIGURATION-147">
+ File based configurations can now be automatically reloaded when the
+ underlying file is modified.
+ </action>
+ <action dev="ebourg" type="add" issue="CONFIGURATION-156">
+ Added a clear() method to the Configuration interface to remove
+ all properties.
+ </action>
+ <action dev="ebourg" type="add" issue="CONFIGURATION-208">
+ Added a SystemConfiguration wrapping the system properties.
+ ConfigurationFactory recognizes the corresponding <system/>
+ element.
+ </action>
+ <action dev="ebourg" type="add">
+ Added a MapConfiguration to turn any Map into a Configuration. The
+ getConfiguration() methods in ConfigurationConverter now use
+ MapConfiguration, as a result the Configuration returned is always
+ synchronized with the underlying Properties or ExtendedProperties,
+ changes made to the Configuration are available in the Properties,
+ and reciprocally.
+ </action>
+ <action dev="ebourg" type="add" issue="CONFIGURATION-146">
+ The "autoSave" feature of XMLConfiguration has been generalized
+ to all file based configurations.
+ </action>
+ <action dev="ebourg" type="add" issue="CONFIGURATION-191">
+ Numeric properties can now be specified in hexadecimal format,
+ for example "number = 0xC5F0".
+ </action>
+ <action dev="oheger" type="fix" issue="CONFIGURATION-36">
+ Fixed HierarchicalConfiguration.getKeys(String), it returned an empty
+ iterator if the prefix string contained indices.
+ </action>
+ <action dev="ebourg" type="add">
+ Added a DataConfiguration decorator providing getters for all useful
+ types found in a configuration (URL, Locale, Date, Calendar, Color,
+ lists and arrays)
+ </action>
+ <action dev="ebourg" type="add">
+ Added 5 new configurations to be used in a web environment:
+ AppletConfiguration, ServletConfiguration, ServletContextConfiguration,
+ ServletRequestConfiguration, ServletFilterConfiguration.
+ </action>
+ </release>
+
+ <release version="1.0" date="2004-10-11" description="First official release">
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-66">
+ The getStringArray() method in CompositeConfiguration now interpolates
+ the strings.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-23">
+ SubsetConfiguration now shares the "throwExceptionOnMissing" property
+ with its parent.
+ </action>
+ <action dev="ebourg" type="fix">
+ Removed "file:" at the beginning of the base path when calling
+ setFile() on a FileConfiguration. This prevented auto saving an
+ XMLConfiguration loaded from a File (issue reported by Mark Roth).
+ </action>
+ <action dev="ebourg" type="update">
+ All NamingEnumerations in JNDIConfiguraiton are now properly closed (Suggested
+ by Eric Jung).
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-90">
+ Properties added to an XMLConfiguration are no longer duplicated in the
+ resulting XML file.
+ </action>
+ </release>
+
+ <release version="1.0-rc2" date="2004-09-24">
+ <action dev="ebourg" type="update">
+ Unified the mechanisms for loading and saving file based configurations.
+ PropertiesConfiguration, XMLConfiguration and HierarchicalXMLConfiguration
+ now implement the same FileConfiguration interface. BasePathLoader,
+ BasePathConfiguration, ClassPropertiesConfiguration and
+ BasePropertiesConfiguration have been removed.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-22">
+ Replaced the calls to Boolean.booleanValue(boolean) in
+ AbstractConfiguration and ConfigurationDynaBean to be Java 1.3
+ compatible.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-112">
+ Changing the prefix of a JNDIConfiguration will now reset the base context used.
+ </action>
+ <action dev="ebourg" type="add" due-to="Eric Jung">
+ The context used by JNDIConfiguration can be specified in its
+ constructor or through the setContext() method. The context can be
+ accessed with the getContext() method which is now public.
+ </action>
+ <action dev="henning" type="add">
+ Make the behaviour on missing properties for the get methods that
+ return objects configurable. A property throwExceptionOnMissing
+ can be set and then the getters throw an NoSuchElementException.
+ The old default behaviour of returning a null value has
+ been restored.
+ </action>
+ <action dev="epugh" type="add" issue="CONFIGURATION-151">
+ Allow configurations extending AbstractConfiguration to change the
+ delimiter used from "," to something else.
+ </action>
+ <action dev="epugh" type="fix">
+ PropertiesConfiguration.save() method has issues with preserving the filename
+ </action>
+ <action dev="epugh" type="fix" issue="CONFIGURATION-132" due-to="Mark Woodman">
+ Test cases for HierarchicalConfigurationXMLReader stores comments as text nodes.
+ </action>
+ <action dev="epugh" type="fix" issue="CONFIGURATION-183" due-to="Ricardo Gladwell">
+ Clarify for ConfigurationDynaBean that the get method should throw an
+ illegalArgumentException if there is no property specified.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-25">
+ Fixed a ClassCastException when adding a non String property to an XMLConfiguration.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-138" due-to="Oliver Heger">
+ Fixed the handling of attribute properties by HierarchicalConfigurationConverter.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-125">
+ Fixed a ClassCastException thrown on adding a non string property
+ in a DatabaseConfiguration.
+ </action>
+ <action dev="henning" type="add">
+ Bring back the getVector() methods in the Configuration interface.
+ These methods are needed for "drop-on" replacement of the
+ various pre-1.0 commons-configuration snapshots and are already
+ deprecated. These methods will be removed for 1.1.
+ </action>
+ </release>
+
+ <release version="1.0-rc1" date="2004-08-14">
+ <action dev="epugh" type="add" issue="CONFIGURATION-132" due-to="Oliver Heger">
+ HierarchicalConfigurationXMLReader stores comments as text nodes.
+ </action>
+ <action dev="epugh" type="add" issue="CONFIGURATION-122" due-to="Ricardo Gladwell">
+ project.xml contains bad dependencies.
+ </action>
+ <action dev="epugh" type="add" issue="CONFIGURATION-64" due-to="Brent Worden">
+ clearXmlProperty doesn't remove list properties completely.
+ </action>
+ <action dev="epugh" type="add" issue="CONFIGURATION-183" due-to="Ricardo Gladwell">
+ new ConfigurationDynaBean.
+ </action>
+ <action dev="epugh" type="add" issue="CONFIGURATION-185" due-to="Ricardo Gladwell">
+ new ConfigurationMap and ConfigurationSet.
+ </action>
+ <action dev="epugh" type="fix" issue="CONFIGURATION-91" due-to="Ricardo Gladwell">
+ Problem adding property XMLConfiguration.
+ </action>
+ <action dev="epugh" type="remove">
+ ConfigurationXMLDocument removed until post 1.0.
+ </action>
+ <action dev="epugh" type="fix" issue="CONFIGURATION-18">
+ DatabaseConfiguration doesn't support List properties.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-137">
+ Fixed a bug related to XMLConfiguration. Can't add a new property as an
+ attribute in XMLConfiguration.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-116">
+ Fixed a bug related to XMLConfiguration. XMLConfiguration doesn't
+ support attribute names with a dot.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-4">
+ Fixed a bug related to XMLConfiguration. XMLConfiguration doesn't ignore
+ comments.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-32">
+ Fixed a bug related to XMLConfiguration. XMLConfiguration.save() doesn't
+ escape reserved characters.
+ </action>
+ <action dev="ebourg" type="add" issue="CONFIGURATION-114">
+ Added save methods in XMLConfiguration similar to PropertiesConfiguration
+ to save the configuration to another file.
+ </action>
+ <action dev="ebourg" type="update">
+ Removed the DOM4J implementations in favor of the DOM ones.
+ DOMConfiguration has been renamed to XMLConfiguration, and
+ HierarchicalDOMConfiguration to HierarchicalXMLConfiguration. The
+ elements parsed by the ConfigurationFactory have been changed
+ accordingly.
+ </action>
+ <action dev="ebourg" type="add">
+ Added a save() method to PropertiesConfiguration and save(Writer out),
+ save(OutputStream out), save(OutputStream out, String encoding) to
+ BasePropertiesConfiguration.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-98">
+ List values are now properly stored as comma separated values in the
+ Properties object returned by ConfigurationConverter.getProperties()
+ </action>
+ <action dev="ebourg" type="update">
+ Introduced a ConversionException thrown when the value of a property is
+ not compatible the type requested. It replaces the ClassCastException
+ and the NumberFormatException thrown previously.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-174">
+ Tokens like ${ref} in a PropertyConfiguration are now properly saved.
+ </action>
+ <action dev="ebourg" type="fix" issue="CONFIGURATION-127">
+ The getList() method of a CompositeConfiguration now returns the list
+ composed of the elements in the first matching configuration and the
+ additional elements found in the in memory configuration.
+ </action>
+ <action dev="epugh" type="fix">
+ SubsetConfiguration returns a List on getList(). AbstractConfiguration
+ wouldn't properly deal with a List, only with a Container for getList()!
+ Thanks to jschaible for the unit test.
+ </action>
+ <action dev="jschaible" type="add">
+ Direct support of XML via DOM. New classes DOMConfiguration and HierarchicalDOMConfiguration.
+ </action>
+ <action dev="jschaible" type="update">
+ Update build to not include test configuration files in resulting jar.
+ </action>
+ <action dev="ebourg" type="update">
+ Refactored JNDIConfiguration to use AbstractConfiguration.
+ </action>
+ <action dev="ebourg" type="update" issue="CONFIGURATION-76">
+ Fixed invalid subsets by refactoring out the subset logic into a SubsetConfiguration.
+ </action>
+ <action dev="oheger" type="fix">
+ Reapply the ConfigurationXMLDocument that went missing during migration out of sandbox.
+ </action>
+ <action dev="epugh" type="update">
+ Apply ASL 2.0 license. Thanks to Jeff Painter for scripting the conversion!
+ </action>
+ <action dev="epugh" type="add">
+ Changed CompositeConfiguration to extend from AbstractConfiuration. This means that the behavior of
+ CompositeConfiguration is much similar to others like PropertiesConfiguration in handling of missing
+ keys, interpolation, etc.. Previously CompositeConfiguration had quite a few differences.
+ </action>
+ <action dev="epugh" type="update">
+ Removed "defaults" from BaseConfiguration. Defaults are now done via using a CompositeConfiguration, either
+ directly or via a ConfigurationFactory. if you want to save changes made to a Configuration, then you use
+ a CompositeConfiguration and get back the inMemoryConfiguration that has the delta of changes. Added a
+ bit of documentation on this.
+ </action>
+ <action dev="epugh" type="update" issue="CONFIGURATION-154">
+ Enhancement: Configuration Comparator.
+ </action>
+ <action dev="epugh" type="update" issue="CONFIGURATION-54">
+ BaseConfiguration: containsKey ignores default properties.
+ I have changed it so that now the defaults are paid attention to.
+ </action>
+ <action dev="ebourg" type="add">
+ The Configuration interface now supports BigDecimal and BigInteger numbers.
+ </action>
+ <action dev="epugh" type="add">
+ ConfigurationException is now thrown by public methods instead of Exception or
+ IOException or whatnot.
+ </action>
+ <action dev="ebourg" type="add">
+ For configuration based on properties files, allow characters like \n etc
+ to be escaped and unescaped.
+ </action>
+ <action dev="ebourg" type="add">
+ New DatabaseConfiguration that uses a database to store the properties.
+ It supports 2 table structures: one table per configuration (2 colums
+ key/value), one table for multiple configurations (2 columns key/value +
+ 1 column for the name of the configuration).
+ </action>
+ <action dev="oheger" type="add">
+ ConfigurationFactory now supports the hierarchicalDom4j element in configuration
+ definition file
+ </action>
+ <action dev="ebourg" type="update">
+ Change all Vector objects to List objects.
+ </action>
+ <action dev="oheger" type="add">
+ ConfigurationFactory now supports two types of properties files, additional and
+ override. Additional properties add each other together. Override override each
+ other. This allows you to have a single property that is either aggregated from a
+ number of sources, or have a property that is overridden according to a specific
+ order of sources.
+ </action>
+ <action dev="oheger" type="update">
+ AbstractConfiguration addProperty now delegates to an abstract addPropertyDirect
+ implemented by BaseConfiguration.
+ </action>
+ <action dev="kshaposhnikov" type="update">
+ Changed getString() method to throw a NoSuchElementException instead of "" if the
+ configuration property doesn't exist.
+ </action>
+ <action dev="kshaposhnikov" type="add">
+ Added AbstractConfiguration to make it easier to create subclasses by only
+ having to implement the methods required.
+ </action>
+ <action dev="bdunbar" type="fix">
+ ClassPropertiesConfiguration Additions:
+ Use the classloader of class that is provided by the constructor.
+ Add a constructor that indicates whether to use relative or absolute.
+ Change getPropertyStream to utilize the relative or absolute flag.
+ Add a test case that checks that absolute paths work.
+ </action>
+ <action dev="epugh" type="fix">
+ JNDIConfiguration.getKeys() Addition:
+ The JNDIConfiguration.getKeys() method was returning an unsupported
+ operation error. However, this is an important method to have
+ supported.
+ </action>
+ <action dev="epugh" type="fix">
+ CompositeConfiguration.getKeys() Fix
+ The CompositeConfiguration.getKeys() method was returning an
+ unordered list of configuration values. However, many apps
+ expect the order that keys are returned to be the order they
+ are added into the properties file.
+ </action>
+ </release>
+ </body>
+</document>
diff --git a/src/main/assembly/bin.xml b/src/main/assembly/bin.xml
new file mode 100644
index 0000000..6c02436
--- /dev/null
+++ b/src/main/assembly/bin.xml
@@ -0,0 +1,45 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<assembly>
+ <id>bin</id>
+ <formats>
+ <format>tar.gz</format>
+ <format>zip</format>
+ </formats>
+ <baseDirectory>${artifactId}-${commons.release.version}</baseDirectory>
+ <fileSets>
+ <fileSet>
+ <includes>
+ <include>LICENSE*</include>
+ <include>NOTICE*</include>
+ <include>RELEASE-NOTES*</include>
+ <include>pom.xml</include>
+ </includes>
+ </fileSet>
+ <fileSet>
+ <directory>target</directory>
+ <outputDirectory></outputDirectory>
+ <includes>
+ <include>*.jar</include>
+ </includes>
+ </fileSet>
+ <fileSet>
+ <directory>target/site/apidocs</directory>
+ <outputDirectory>apidocs</outputDirectory>
+ </fileSet>
+ </fileSets>
+</assembly>
diff --git a/src/main/assembly/src.xml b/src/main/assembly/src.xml
new file mode 100644
index 0000000..c47df19
--- /dev/null
+++ b/src/main/assembly/src.xml
@@ -0,0 +1,40 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<assembly>
+ <id>src</id>
+ <formats>
+ <format>tar.gz</format>
+ <format>zip</format>
+ </formats>
+ <baseDirectory>${artifactId}-${commons.release.version}-src</baseDirectory>
+ <fileSets>
+ <fileSet>
+ <includes>
+ <include>LICENSE*</include>
+ <include>NOTICE*</include>
+ <include>RELEASE-NOTES*</include>
+ <include>pom.xml</include>
+ </includes>
+ </fileSet>
+ <fileSet>
+ <directory>conf</directory>
+ </fileSet>
+ <fileSet>
+ <directory>src</directory>
+ </fileSet>
+ </fileSets>
+</assembly>
diff --git a/src/main/java/org/apache/commons/configuration/AbstractConfiguration.java b/src/main/java/org/apache/commons/configuration/AbstractConfiguration.java
new file mode 100644
index 0000000..eb4fa8a
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/AbstractConfiguration.java
@@ -0,0 +1,1309 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.lang.reflect.Array;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+
+import org.apache.commons.configuration.event.ConfigurationErrorEvent;
+import org.apache.commons.configuration.event.ConfigurationErrorListener;
+import org.apache.commons.configuration.event.EventSource;
+import org.apache.commons.configuration.interpol.ConfigurationInterpolator;
+import org.apache.commons.lang.BooleanUtils;
+import org.apache.commons.lang.ClassUtils;
+import org.apache.commons.lang.ObjectUtils;
+import org.apache.commons.lang.text.StrLookup;
+import org.apache.commons.lang.text.StrSubstitutor;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.impl.NoOpLog;
+
+/**
+ * <p>Abstract configuration class. Provides basic functionality but does not
+ * store any data.</p>
+ * <p>If you want to write your own Configuration class then you should
+ * implement only abstract methods from this class. A lot of functionality
+ * needed by typical implementations of the {@code Configuration}
+ * interface is already provided by this base class. Following is a list of
+ * features implemented here:
+ * <ul><li>Data conversion support. The various data types required by the
+ * {@code Configuration} interface are already handled by this base class.
+ * A concrete sub class only needs to provide a generic {@code getProperty()}
+ * method.</li>
+ * <li>Support for variable interpolation. Property values containing special
+ * variable tokens (like <code>${var}</code>) will be replaced by their
+ * corresponding values.</li>
+ * <li>Support for string lists. The values of properties to be added to this
+ * configuration are checked whether they contain a list delimiter character. If
+ * this is the case and if list splitting is enabled, the string is split and
+ * multiple values are added for this property. (With the
+ * {@code setListDelimiter()} method the delimiter character can be
+ * specified; per default a comma is used. The
+ * {@code setDelimiterParsingDisabled()} method can be used to disable
+ * list splitting completely.)</li>
+ * <li>Allows to specify how missing properties are treated. Per default the
+ * get methods returning an object will return <b>null</b> if the searched
+ * property key is not found (and no default value is provided). With the
+ * {@code setThrowExceptionOnMissing()} method this behavior can be
+ * changed to throw an exception when a requested property cannot be found.</li>
+ * <li>Basic event support. Whenever this configuration is modified registered
+ * event listeners are notified. Refer to the various {@code EVENT_XXX}
+ * constants to get an impression about which event types are supported.</li>
+ * </ul></p>
+ *
+ * @author <a href="mailto:ksh at scand.com">Konstantin Shaposhnikov </a>
+ * @author <a href="mailto:hps at intermeta.de">Henning P. Schmiedehausen </a>
+ * @version $Id: AbstractConfiguration.java 1534064 2013-10-21 08:44:33Z henning $
+ */
+public abstract class AbstractConfiguration extends EventSource implements Configuration
+{
+ /**
+ * Constant for the add property event type.
+ * @since 1.3
+ */
+ public static final int EVENT_ADD_PROPERTY = 1;
+
+ /**
+ * Constant for the clear property event type.
+ * @since 1.3
+ */
+ public static final int EVENT_CLEAR_PROPERTY = 2;
+
+ /**
+ * Constant for the set property event type.
+ * @since 1.3
+ */
+ public static final int EVENT_SET_PROPERTY = 3;
+
+ /**
+ * Constant for the clear configuration event type.
+ * @since 1.3
+ */
+ public static final int EVENT_CLEAR = 4;
+
+ /**
+ * Constant for the get property event type. This event type is used for
+ * error events.
+ * @since 1.4
+ */
+ public static final int EVENT_READ_PROPERTY = 5;
+
+ /** start token */
+ protected static final String START_TOKEN = "${";
+
+ /** end token */
+ protected static final String END_TOKEN = "}";
+
+ /**
+ * Constant for the disabled list delimiter. This character is passed to the
+ * list parsing methods if delimiter parsing is disabled. So this character
+ * should not occur in string property values.
+ */
+ private static final char DISABLED_DELIMITER = '\0';
+
+ /** The default value for listDelimiter */
+ private static char defaultListDelimiter = ',';
+
+ /** Delimiter used to convert single values to lists */
+ private char listDelimiter = defaultListDelimiter;
+
+ /**
+ * When set to true the given configuration delimiter will not be used
+ * while parsing for this configuration.
+ */
+ private boolean delimiterParsingDisabled;
+
+ /**
+ * Whether the configuration should throw NoSuchElementExceptions or simply
+ * return null when a property does not exist. Defaults to return null.
+ */
+ private boolean throwExceptionOnMissing;
+
+ /** Stores a reference to the object that handles variable interpolation.*/
+ private StrSubstitutor substitutor;
+
+ /** Stores the logger.*/
+ private Log log;
+
+ /**
+ * Creates a new instance of {@code AbstractConfiguration}.
+ */
+ public AbstractConfiguration()
+ {
+ setLogger(null);
+ }
+
+ /**
+ * For configurations extending AbstractConfiguration, allow them to change
+ * the listDelimiter from the default comma (","). This value will be used
+ * only when creating new configurations. Those already created will not be
+ * affected by this change
+ *
+ * @param delimiter The new listDelimiter
+ */
+ public static void setDefaultListDelimiter(char delimiter)
+ {
+ AbstractConfiguration.defaultListDelimiter = delimiter;
+ }
+
+ /**
+ * Sets the default list delimiter.
+ *
+ * @param delimiter the delimiter character
+ * @deprecated Use AbstractConfiguration.setDefaultListDelimiter(char)
+ * instead
+ */
+ @Deprecated
+ public static void setDelimiter(char delimiter)
+ {
+ setDefaultListDelimiter(delimiter);
+ }
+
+ /**
+ * Retrieve the current delimiter. By default this is a comma (",").
+ *
+ * @return The delimiter in use
+ */
+ public static char getDefaultListDelimiter()
+ {
+ return AbstractConfiguration.defaultListDelimiter;
+ }
+
+ /**
+ * Returns the default list delimiter.
+ *
+ * @return the default list delimiter
+ * @deprecated Use AbstractConfiguration.getDefaultListDelimiter() instead
+ */
+ @Deprecated
+ public static char getDelimiter()
+ {
+ return getDefaultListDelimiter();
+ }
+
+ /**
+ * Change the list delimiter for this configuration.
+ *
+ * Note: this change will only be effective for new parsings. If you
+ * want it to take effect for all loaded properties use the no arg constructor
+ * and call this method before setting the source.
+ *
+ * @param listDelimiter The new listDelimiter
+ */
+ public void setListDelimiter(char listDelimiter)
+ {
+ this.listDelimiter = listDelimiter;
+ }
+
+ /**
+ * Retrieve the delimiter for this configuration. The default
+ * is the value of defaultListDelimiter.
+ *
+ * @return The listDelimiter in use
+ */
+ public char getListDelimiter()
+ {
+ return listDelimiter;
+ }
+
+ /**
+ * Determine if this configuration is using delimiters when parsing
+ * property values to convert them to lists of values. Defaults to false
+ * @return true if delimiters are not being used
+ */
+ public boolean isDelimiterParsingDisabled()
+ {
+ return delimiterParsingDisabled;
+ }
+
+ /**
+ * Set whether this configuration should use delimiters when parsing
+ * property values to convert them to lists of values. By default delimiter
+ * parsing is enabled
+ *
+ * Note: this change will only be effective for new parsings. If you
+ * want it to take effect for all loaded properties use the no arg constructor
+ * and call this method before setting source.
+ * @param delimiterParsingDisabled a flag whether delimiter parsing should
+ * be disabled
+ */
+ public void setDelimiterParsingDisabled(boolean delimiterParsingDisabled)
+ {
+ this.delimiterParsingDisabled = delimiterParsingDisabled;
+ }
+
+ /**
+ * Allows to set the {@code throwExceptionOnMissing} flag. This
+ * flag controls the behavior of property getter methods that return
+ * objects if the requested property is missing. If the flag is set to
+ * <b>false</b> (which is the default value), these methods will return
+ * <b>null</b>. If set to <b>true</b>, they will throw a
+ * {@code NoSuchElementException} exception. Note that getter methods
+ * for primitive data types are not affected by this flag.
+ *
+ * @param throwExceptionOnMissing The new value for the property
+ */
+ public void setThrowExceptionOnMissing(boolean throwExceptionOnMissing)
+ {
+ this.throwExceptionOnMissing = throwExceptionOnMissing;
+ }
+
+ /**
+ * Returns true if missing values throw Exceptions.
+ *
+ * @return true if missing values throw Exceptions
+ */
+ public boolean isThrowExceptionOnMissing()
+ {
+ return throwExceptionOnMissing;
+ }
+
+ /**
+ * Returns the object that is responsible for variable interpolation.
+ *
+ * @return the object responsible for variable interpolation
+ * @since 1.4
+ */
+ public synchronized StrSubstitutor getSubstitutor()
+ {
+ if (substitutor == null)
+ {
+ substitutor = new StrSubstitutor(createInterpolator());
+ }
+ return substitutor;
+ }
+
+ /**
+ * Returns the {@code ConfigurationInterpolator} object that manages
+ * the lookup objects for resolving variables. <em>Note:</em> If this
+ * object is manipulated (e.g. new lookup objects added), synchronization
+ * has to be manually ensured. Because
+ * {@code ConfigurationInterpolator} is not thread-safe concurrent
+ * access to properties of this configuration instance (which causes the
+ * interpolator to be invoked) may cause race conditions.
+ *
+ * @return the {@code ConfigurationInterpolator} associated with this
+ * configuration
+ * @since 1.4
+ */
+ public ConfigurationInterpolator getInterpolator()
+ {
+ return (ConfigurationInterpolator) getSubstitutor()
+ .getVariableResolver();
+ }
+
+ /**
+ * Creates the interpolator object that is responsible for variable
+ * interpolation. This method is invoked on first access of the
+ * interpolation features. It creates a new instance of
+ * {@code ConfigurationInterpolator} and sets the default lookup
+ * object to an implementation that queries this configuration.
+ *
+ * @return the newly created interpolator object
+ * @since 1.4
+ */
+ protected ConfigurationInterpolator createInterpolator()
+ {
+ ConfigurationInterpolator interpol = new ConfigurationInterpolator();
+ interpol.setDefaultLookup(new StrLookup()
+ {
+ @Override
+ public String lookup(String var)
+ {
+ Object prop = resolveContainerStore(var);
+ return (prop != null) ? prop.toString() : null;
+ }
+ });
+ return interpol;
+ }
+
+ /**
+ * Returns the logger used by this configuration object.
+ *
+ * @return the logger
+ * @since 1.4
+ */
+ public Log getLogger()
+ {
+ return log;
+ }
+
+ /**
+ * Allows to set the logger to be used by this configuration object. This
+ * method makes it possible for clients to exactly control logging behavior.
+ * Per default a logger is set that will ignore all log messages. Derived
+ * classes that want to enable logging should call this method during their
+ * initialization with the logger to be used.
+ *
+ * @param log the new logger
+ * @since 1.4
+ */
+ public void setLogger(Log log)
+ {
+ this.log = (log != null) ? log : new NoOpLog();
+ }
+
+ /**
+ * Adds a special
+ * {@link org.apache.commons.configuration.event.ConfigurationErrorListener}
+ * object to this configuration that will log all internal errors. This
+ * method is intended to be used by certain derived classes, for which it is
+ * known that they can fail on property access (e.g.
+ * {@code DatabaseConfiguration}).
+ *
+ * @since 1.4
+ */
+ public void addErrorLogListener()
+ {
+ addErrorListener(new ConfigurationErrorListener()
+ {
+ public void configurationError(ConfigurationErrorEvent event)
+ {
+ getLogger().warn("Internal error", event.getCause());
+ }
+ });
+ }
+
+ public void addProperty(String key, Object value)
+ {
+ fireEvent(EVENT_ADD_PROPERTY, key, value, true);
+ addPropertyValues(key, value,
+ isDelimiterParsingDisabled() ? DISABLED_DELIMITER
+ : getListDelimiter());
+ fireEvent(EVENT_ADD_PROPERTY, key, value, false);
+ }
+
+ /**
+ * Adds a key/value pair to the Configuration. Override this method to
+ * provide write access to underlying Configuration store.
+ *
+ * @param key key to use for mapping
+ * @param value object to store
+ */
+ protected abstract void addPropertyDirect(String key, Object value);
+
+ /**
+ * Adds the specified value for the given property. This method supports
+ * single values and containers (e.g. collections or arrays) as well. In the
+ * latter case, {@code addPropertyDirect()} will be called for each
+ * element.
+ *
+ * @param key the property key
+ * @param value the value object
+ * @param delimiter the list delimiter character
+ */
+ private void addPropertyValues(String key, Object value, char delimiter)
+ {
+ Iterator<?> it = PropertyConverter.toIterator(value, delimiter);
+ while (it.hasNext())
+ {
+ addPropertyDirect(key, it.next());
+ }
+ }
+
+ /**
+ * interpolate key names to handle ${key} stuff
+ *
+ * @param base string to interpolate
+ *
+ * @return returns the key name with the ${key} substituted
+ */
+ protected String interpolate(String base)
+ {
+ Object result = interpolate((Object) base);
+ return (result == null) ? null : result.toString();
+ }
+
+ /**
+ * Returns the interpolated value. Non String values are returned without change.
+ *
+ * @param value the value to interpolate
+ *
+ * @return returns the value with variables substituted
+ */
+ protected Object interpolate(Object value)
+ {
+ return PropertyConverter.interpolate(value, this);
+ }
+
+ /**
+ * Recursive handler for multple levels of interpolation.
+ *
+ * When called the first time, priorVariables should be null.
+ *
+ * @param base string with the ${key} variables
+ * @param priorVariables serves two purposes: to allow checking for loops,
+ * and creating a meaningful exception message should a loop occur. It's
+ * 0'th element will be set to the value of base from the first call. All
+ * subsequent interpolated variables are added afterward.
+ *
+ * @return the string with the interpolation taken care of
+ * @deprecated Interpolation is now handled by
+ * {@link PropertyConverter}; this method will no longer be
+ * called
+ */
+ @Deprecated
+ protected String interpolateHelper(String base, List<?> priorVariables)
+ {
+ return base; // just a dummy implementation
+ }
+
+ public Configuration subset(String prefix)
+ {
+ return new SubsetConfiguration(this, prefix, ".");
+ }
+
+ public void setProperty(String key, Object value)
+ {
+ fireEvent(EVENT_SET_PROPERTY, key, value, true);
+ setDetailEvents(false);
+ try
+ {
+ clearProperty(key);
+ addProperty(key, value);
+ }
+ finally
+ {
+ setDetailEvents(true);
+ }
+ fireEvent(EVENT_SET_PROPERTY, key, value, false);
+ }
+
+ /**
+ * Removes the specified property from this configuration. This
+ * implementation performs some preparations and then delegates to
+ * {@code clearPropertyDirect()}, which will do the real work.
+ *
+ * @param key the key to be removed
+ */
+ public void clearProperty(String key)
+ {
+ fireEvent(EVENT_CLEAR_PROPERTY, key, null, true);
+ clearPropertyDirect(key);
+ fireEvent(EVENT_CLEAR_PROPERTY, key, null, false);
+ }
+
+ /**
+ * Removes the specified property from this configuration. This method is
+ * called by {@code clearProperty()} after it has done some
+ * preparations. It should be overridden in sub classes. This base
+ * implementation is just left empty.
+ *
+ * @param key the key to be removed
+ */
+ protected void clearPropertyDirect(String key)
+ {
+ // override in sub classes
+ }
+
+ public void clear()
+ {
+ fireEvent(EVENT_CLEAR, null, null, true);
+ setDetailEvents(false);
+ boolean useIterator = true;
+ try
+ {
+ Iterator<String> it = getKeys();
+ while (it.hasNext())
+ {
+ String key = it.next();
+ if (useIterator)
+ {
+ try
+ {
+ it.remove();
+ }
+ catch (UnsupportedOperationException usoex)
+ {
+ useIterator = false;
+ }
+ }
+
+ if (useIterator && containsKey(key))
+ {
+ useIterator = false;
+ }
+
+ if (!useIterator)
+ {
+ // workaround for Iterators that do not remove the property
+ // on calling remove() or do not support remove() at all
+ clearProperty(key);
+ }
+ }
+ }
+ finally
+ {
+ setDetailEvents(true);
+ }
+ fireEvent(EVENT_CLEAR, null, null, false);
+ }
+
+ /**
+ * {@inheritDoc} This implementation returns keys that either match the
+ * prefix or start with the prefix followed by a dot ('.'). So the call
+ * {@code getKeys("db");} will find the keys {@code db},
+ * {@code db.user}, or {@code db.password}, but not the key
+ * {@code dbdriver}.
+ */
+ public Iterator<String> getKeys(String prefix)
+ {
+ return new PrefixedKeysIterator(getKeys(), prefix);
+ }
+
+ public Properties getProperties(String key)
+ {
+ return getProperties(key, null);
+ }
+
+ /**
+ * Get a list of properties associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @param defaults Any default values for the returned
+ * {@code Properties} object. Ignored if {@code null}.
+ *
+ * @return The associated properties if key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an object that
+ * is not a String/List of Strings.
+ *
+ * @throws IllegalArgumentException if one of the tokens is malformed (does
+ * not contain an equals sign).
+ */
+ public Properties getProperties(String key, Properties defaults)
+ {
+ /*
+ * Grab an array of the tokens for this key.
+ */
+ String[] tokens = getStringArray(key);
+
+ /*
+ * Each token is of the form 'key=value'.
+ */
+ Properties props = defaults == null ? new Properties() : new Properties(defaults);
+ for (String token : tokens)
+ {
+ int equalSign = token.indexOf('=');
+ if (equalSign > 0)
+ {
+ String pkey = token.substring(0, equalSign).trim();
+ String pvalue = token.substring(equalSign + 1).trim();
+ props.put(pkey, pvalue);
+ }
+ else if (tokens.length == 1 && "".equals(token))
+ {
+ // Semantically equivalent to an empty Properties
+ // object.
+ break;
+ }
+ else
+ {
+ throw new IllegalArgumentException('\'' + token + "' does not contain an equals sign");
+ }
+ }
+ return props;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see PropertyConverter#toBoolean(Object)
+ */
+ public boolean getBoolean(String key)
+ {
+ Boolean b = getBoolean(key, null);
+ if (b != null)
+ {
+ return b.booleanValue();
+ }
+ else
+ {
+ throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see PropertyConverter#toBoolean(Object)
+ */
+ public boolean getBoolean(String key, boolean defaultValue)
+ {
+ return getBoolean(key, BooleanUtils.toBooleanObject(defaultValue)).booleanValue();
+ }
+
+ /**
+ * Obtains the value of the specified key and tries to convert it into a
+ * {@code Boolean} object. If the property has no value, the passed
+ * in default value will be used.
+ *
+ * @param key the key of the property
+ * @param defaultValue the default value
+ * @return the value of this key converted to a {@code Boolean}
+ * @throws ConversionException if the value cannot be converted to a
+ * {@code Boolean}
+ * @see PropertyConverter#toBoolean(Object)
+ */
+ public Boolean getBoolean(String key, Boolean defaultValue)
+ {
+ Object value = resolveContainerStore(key);
+
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ else
+ {
+ try
+ {
+ return PropertyConverter.toBoolean(interpolate(value));
+ }
+ catch (ConversionException e)
+ {
+ throw new ConversionException('\'' + key + "' doesn't map to a Boolean object", e);
+ }
+ }
+ }
+
+ public byte getByte(String key)
+ {
+ Byte b = getByte(key, null);
+ if (b != null)
+ {
+ return b.byteValue();
+ }
+ else
+ {
+ throw new NoSuchElementException('\'' + key + " doesn't map to an existing object");
+ }
+ }
+
+ public byte getByte(String key, byte defaultValue)
+ {
+ return getByte(key, new Byte(defaultValue)).byteValue();
+ }
+
+ public Byte getByte(String key, Byte defaultValue)
+ {
+ Object value = resolveContainerStore(key);
+
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ else
+ {
+ try
+ {
+ return PropertyConverter.toByte(interpolate(value));
+ }
+ catch (ConversionException e)
+ {
+ throw new ConversionException('\'' + key + "' doesn't map to a Byte object", e);
+ }
+ }
+ }
+
+ public double getDouble(String key)
+ {
+ Double d = getDouble(key, null);
+ if (d != null)
+ {
+ return d.doubleValue();
+ }
+ else
+ {
+ throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
+ }
+ }
+
+ public double getDouble(String key, double defaultValue)
+ {
+ return getDouble(key, new Double(defaultValue)).doubleValue();
+ }
+
+ public Double getDouble(String key, Double defaultValue)
+ {
+ Object value = resolveContainerStore(key);
+
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ else
+ {
+ try
+ {
+ return PropertyConverter.toDouble(interpolate(value));
+ }
+ catch (ConversionException e)
+ {
+ throw new ConversionException('\'' + key + "' doesn't map to a Double object", e);
+ }
+ }
+ }
+
+ public float getFloat(String key)
+ {
+ Float f = getFloat(key, null);
+ if (f != null)
+ {
+ return f.floatValue();
+ }
+ else
+ {
+ throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
+ }
+ }
+
+ public float getFloat(String key, float defaultValue)
+ {
+ return getFloat(key, new Float(defaultValue)).floatValue();
+ }
+
+ public Float getFloat(String key, Float defaultValue)
+ {
+ Object value = resolveContainerStore(key);
+
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ else
+ {
+ try
+ {
+ return PropertyConverter.toFloat(interpolate(value));
+ }
+ catch (ConversionException e)
+ {
+ throw new ConversionException('\'' + key + "' doesn't map to a Float object", e);
+ }
+ }
+ }
+
+ public int getInt(String key)
+ {
+ Integer i = getInteger(key, null);
+ if (i != null)
+ {
+ return i.intValue();
+ }
+ else
+ {
+ throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
+ }
+ }
+
+ public int getInt(String key, int defaultValue)
+ {
+ Integer i = getInteger(key, null);
+
+ if (i == null)
+ {
+ return defaultValue;
+ }
+
+ return i.intValue();
+ }
+
+ public Integer getInteger(String key, Integer defaultValue)
+ {
+ Object value = resolveContainerStore(key);
+
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ else
+ {
+ try
+ {
+ return PropertyConverter.toInteger(interpolate(value));
+ }
+ catch (ConversionException e)
+ {
+ throw new ConversionException('\'' + key + "' doesn't map to an Integer object", e);
+ }
+ }
+ }
+
+ public long getLong(String key)
+ {
+ Long l = getLong(key, null);
+ if (l != null)
+ {
+ return l.longValue();
+ }
+ else
+ {
+ throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
+ }
+ }
+
+ public long getLong(String key, long defaultValue)
+ {
+ return getLong(key, new Long(defaultValue)).longValue();
+ }
+
+ public Long getLong(String key, Long defaultValue)
+ {
+ Object value = resolveContainerStore(key);
+
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ else
+ {
+ try
+ {
+ return PropertyConverter.toLong(interpolate(value));
+ }
+ catch (ConversionException e)
+ {
+ throw new ConversionException('\'' + key + "' doesn't map to a Long object", e);
+ }
+ }
+ }
+
+ public short getShort(String key)
+ {
+ Short s = getShort(key, null);
+ if (s != null)
+ {
+ return s.shortValue();
+ }
+ else
+ {
+ throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
+ }
+ }
+
+ public short getShort(String key, short defaultValue)
+ {
+ return getShort(key, new Short(defaultValue)).shortValue();
+ }
+
+ public Short getShort(String key, Short defaultValue)
+ {
+ Object value = resolveContainerStore(key);
+
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ else
+ {
+ try
+ {
+ return PropertyConverter.toShort(interpolate(value));
+ }
+ catch (ConversionException e)
+ {
+ throw new ConversionException('\'' + key + "' doesn't map to a Short object", e);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see #setThrowExceptionOnMissing(boolean)
+ */
+ public BigDecimal getBigDecimal(String key)
+ {
+ BigDecimal number = getBigDecimal(key, null);
+ if (number != null)
+ {
+ return number;
+ }
+ else if (isThrowExceptionOnMissing())
+ {
+ throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ public BigDecimal getBigDecimal(String key, BigDecimal defaultValue)
+ {
+ Object value = resolveContainerStore(key);
+
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ else
+ {
+ try
+ {
+ return PropertyConverter.toBigDecimal(interpolate(value));
+ }
+ catch (ConversionException e)
+ {
+ throw new ConversionException('\'' + key + "' doesn't map to a BigDecimal object", e);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see #setThrowExceptionOnMissing(boolean)
+ */
+ public BigInteger getBigInteger(String key)
+ {
+ BigInteger number = getBigInteger(key, null);
+ if (number != null)
+ {
+ return number;
+ }
+ else if (isThrowExceptionOnMissing())
+ {
+ throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ public BigInteger getBigInteger(String key, BigInteger defaultValue)
+ {
+ Object value = resolveContainerStore(key);
+
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ else
+ {
+ try
+ {
+ return PropertyConverter.toBigInteger(interpolate(value));
+ }
+ catch (ConversionException e)
+ {
+ throw new ConversionException('\'' + key + "' doesn't map to a BigInteger object", e);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see #setThrowExceptionOnMissing(boolean)
+ */
+ public String getString(String key)
+ {
+ String s = getString(key, null);
+ if (s != null)
+ {
+ return s;
+ }
+ else if (isThrowExceptionOnMissing())
+ {
+ throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ public String getString(String key, String defaultValue)
+ {
+ Object value = resolveContainerStore(key);
+
+ if (value instanceof String)
+ {
+ return interpolate((String) value);
+ }
+ else if (value == null)
+ {
+ return interpolate(defaultValue);
+ }
+ else
+ {
+ throw new ConversionException('\'' + key + "' doesn't map to a String object");
+ }
+ }
+
+ /**
+ * Get an array of strings associated with the given configuration key.
+ * If the key doesn't map to an existing object, an empty array is returned.
+ * If a property is added to a configuration, it is checked whether it
+ * contains multiple values. This is obvious if the added object is a list
+ * or an array. For strings it is checked whether the string contains the
+ * list delimiter character that can be specified using the
+ * {@code setListDelimiter()} method. If this is the case, the string
+ * is split at these positions resulting in a property with multiple
+ * values.
+ *
+ * @param key The configuration key.
+ * @return The associated string array if key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a String/List of Strings.
+ * @see #setListDelimiter(char)
+ * @see #setDelimiterParsingDisabled(boolean)
+ */
+ public String[] getStringArray(String key)
+ {
+ Object value = getProperty(key);
+
+ String[] array;
+
+ if (value instanceof String)
+ {
+ array = new String[1];
+
+ array[0] = interpolate((String) value);
+ }
+ else if (value instanceof List)
+ {
+ List<?> list = (List<?>) value;
+ array = new String[list.size()];
+
+ for (int i = 0; i < array.length; i++)
+ {
+ array[i] = interpolate(ObjectUtils.toString(list.get(i), null));
+ }
+ }
+ else if (value == null)
+ {
+ array = new String[0];
+ }
+ else if (isScalarValue(value))
+ {
+ array = new String[1];
+ array[0] = value.toString();
+ }
+ else
+ {
+ throw new ConversionException('\'' + key + "' doesn't map to a String/List object");
+ }
+ return array;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see #getStringArray(String)
+ */
+ public List<Object> getList(String key)
+ {
+ return getList(key, new ArrayList<Object>());
+ }
+
+ public List<Object> getList(String key, List<?> defaultValue)
+ {
+ Object value = getProperty(key);
+ List<Object> list;
+
+ if (value instanceof String)
+ {
+ list = new ArrayList<Object>(1);
+ list.add(interpolate((String) value));
+ }
+ else if (value instanceof List)
+ {
+ list = new ArrayList<Object>();
+ List<?> l = (List<?>) value;
+
+ // add the interpolated elements in the new list
+ for (Object elem : l)
+ {
+ list.add(interpolate(elem));
+ }
+ }
+ else if (value == null)
+ {
+ list = (List<Object>) defaultValue;
+ }
+ else if (value.getClass().isArray())
+ {
+ return Arrays.asList((Object[]) value);
+ }
+ else if (isScalarValue(value))
+ {
+ return Collections.singletonList((Object) value.toString());
+ }
+ else
+ {
+ throw new ConversionException('\'' + key + "' doesn't map to a List object: " + value + ", a "
+ + value.getClass().getName());
+ }
+ return list;
+ }
+
+ /**
+ * Returns an object from the store described by the key. If the value is a
+ * Collection object, replace it with the first object in the collection.
+ *
+ * @param key The property key.
+ *
+ * @return value Value, transparently resolving a possible collection dependency.
+ */
+ protected Object resolveContainerStore(String key)
+ {
+ Object value = getProperty(key);
+ if (value != null)
+ {
+ if (value instanceof Collection)
+ {
+ Collection<?> collection = (Collection<?>) value;
+ value = collection.isEmpty() ? null : collection.iterator().next();
+ }
+ else if (value.getClass().isArray() && Array.getLength(value) > 0)
+ {
+ value = Array.get(value, 0);
+ }
+ }
+
+ return value;
+ }
+
+ /**
+ * Checks whether the specified object is a scalar value. This method is
+ * called by {@code getList()} and {@code getStringArray()} if the
+ * property requested is not a string, a list, or an array. If it returns
+ * <b>true</b>, the calling method transforms the value to a string and
+ * returns a list or an array with this single element. This implementation
+ * returns <b>true</b> if the value is of a wrapper type for a primitive
+ * type.
+ *
+ * @param value the value to be checked
+ * @return a flag whether the value is a scalar
+ * @since 1.7
+ */
+ protected boolean isScalarValue(Object value)
+ {
+ return ClassUtils.wrapperToPrimitive(value.getClass()) != null;
+ }
+
+ /**
+ * Copies the content of the specified configuration into this
+ * configuration. If the specified configuration contains a key that is also
+ * present in this configuration, the value of this key will be replaced by
+ * the new value. <em>Note:</em> This method won't work well when copying
+ * hierarchical configurations because it is not able to copy information
+ * about the properties' structure (i.e. the parent-child-relationships will
+ * get lost). So when dealing with hierarchical configuration objects their
+ * {@link HierarchicalConfiguration#clone() clone()} methods
+ * should be used.
+ *
+ * @param c the configuration to copy (can be <b>null</b>, then this
+ * operation will have no effect)
+ * @since 1.5
+ */
+ public void copy(Configuration c)
+ {
+ if (c != null)
+ {
+ for (Iterator<String> it = c.getKeys(); it.hasNext();)
+ {
+ String key = it.next();
+ Object value = c.getProperty(key);
+ fireEvent(EVENT_SET_PROPERTY, key, value, true);
+ setDetailEvents(false);
+ try
+ {
+ clearProperty(key);
+ addPropertyValues(key, value, DISABLED_DELIMITER);
+ }
+ finally
+ {
+ setDetailEvents(true);
+ }
+ fireEvent(EVENT_SET_PROPERTY, key, value, false);
+ }
+ }
+ }
+
+ /**
+ * Appends the content of the specified configuration to this configuration.
+ * The values of all properties contained in the specified configuration
+ * will be appended to this configuration. So if a property is already
+ * present in this configuration, its new value will be a union of the
+ * values in both configurations. <em>Note:</em> This method won't work
+ * well when appending hierarchical configurations because it is not able to
+ * copy information about the properties' structure (i.e. the
+ * parent-child-relationships will get lost). So when dealing with
+ * hierarchical configuration objects their
+ * {@link HierarchicalConfiguration#clone() clone()} methods
+ * should be used.
+ *
+ * @param c the configuration to be appended (can be <b>null</b>, then this
+ * operation will have no effect)
+ * @since 1.5
+ */
+ public void append(Configuration c)
+ {
+ if (c != null)
+ {
+ for (Iterator<String> it = c.getKeys(); it.hasNext();)
+ {
+ String key = it.next();
+ Object value = c.getProperty(key);
+ fireEvent(EVENT_ADD_PROPERTY, key, value, true);
+ addPropertyValues(key, value, DISABLED_DELIMITER);
+ fireEvent(EVENT_ADD_PROPERTY, key, value, false);
+ }
+ }
+ }
+
+ /**
+ * Returns a configuration with the same content as this configuration, but
+ * with all variables replaced by their actual values. This method tries to
+ * clone the configuration and then perform interpolation on all properties.
+ * So property values of the form <code>${var}</code> will be resolved as
+ * far as possible (if a variable cannot be resolved, it remains unchanged).
+ * This operation is useful if the content of a configuration is to be
+ * exported or processed by an external component that does not support
+ * variable interpolation.
+ *
+ * @return a configuration with all variables interpolated
+ * @throws ConfigurationRuntimeException if this configuration cannot be
+ * cloned
+ * @since 1.5
+ */
+ public Configuration interpolatedConfiguration()
+ {
+ // first clone this configuration
+ AbstractConfiguration c = (AbstractConfiguration) ConfigurationUtils
+ .cloneConfiguration(this);
+
+ // now perform interpolation
+ c.setDelimiterParsingDisabled(true);
+ for (Iterator<String> it = getKeys(); it.hasNext();)
+ {
+ String key = it.next();
+ c.setProperty(key, getList(key));
+ }
+
+ c.setDelimiterParsingDisabled(isDelimiterParsingDisabled());
+ return c;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/AbstractFileConfiguration.java b/src/main/java/org/apache/commons/configuration/AbstractFileConfiguration.java
new file mode 100644
index 0000000..67d0578
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/AbstractFileConfiguration.java
@@ -0,0 +1,1092 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.net.URL;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.commons.configuration.reloading.InvariantReloadingStrategy;
+import org.apache.commons.configuration.reloading.ReloadingStrategy;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <p>Partial implementation of the {@code FileConfiguration} interface.
+ * Developers of file based configuration may want to extend this class,
+ * the two methods left to implement are {@link FileConfiguration#load(Reader)}
+ * and {@link FileConfiguration#save(Writer)}.</p>
+ * <p>This base class already implements a couple of ways to specify the location
+ * of the file this configuration is based on. The following possibilities
+ * exist:
+ * <ul><li>URLs: With the method {@code setURL()} a full URL to the
+ * configuration source can be specified. This is the most flexible way. Note
+ * that the {@code save()} methods support only <em>file:</em> URLs.</li>
+ * <li>Files: The {@code setFile()} method allows to specify the
+ * configuration source as a file. This can be either a relative or an
+ * absolute file. In the former case the file is resolved based on the current
+ * directory.</li>
+ * <li>As file paths in string form: With the {@code setPath()} method a
+ * full path to a configuration file can be provided as a string.</li>
+ * <li>Separated as base path and file name: This is the native form in which
+ * the location is stored. The base path is a string defining either a local
+ * directory or a URL. It can be set using the {@code setBasePath()}
+ * method. The file name, non surprisingly, defines the name of the configuration
+ * file.</li></ul></p>
+ * <p>The configuration source to be loaded can be specified using one of the
+ * methods described above. Then the parameterless {@code load()} method can be
+ * called. Alternatively, one of the {@code load()} methods can be used which is
+ * passed the source directly. These methods typically do not change the
+ * internally stored file; however, if the configuration is not yet associated
+ * with a configuration source, the first call to one of the {@code load()}
+ * methods sets the base path and the source URL. This fact has to be taken
+ * into account when calling {@code load()} multiple times with different file
+ * paths.</p>
+ * <p>Note that the {@code load()} methods do not wipe out the configuration's
+ * content before the new configuration file is loaded. Thus it is very easy to
+ * construct a union configuration by simply loading multiple configuration
+ * files, e.g.</p>
+ * <p><pre>
+ * config.load(configFile1);
+ * config.load(configFile2);
+ * </pre></p>
+ * <p>After executing this code fragment, the resulting configuration will
+ * contain both the properties of configFile1 and configFile2. On the other
+ * hand, if the current configuration file is to be reloaded, {@code clear()}
+ * should be called first. Otherwise the properties are doubled. This behavior
+ * is analogous to the behavior of the {@code load(InputStream)} method
+ * in {@code java.util.Properties}.</p>
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: AbstractFileConfiguration.java 1234118 2012-01-20 20:36:04Z oheger $
+ * @since 1.0-rc2
+ */
+public abstract class AbstractFileConfiguration
+extends BaseConfiguration
+implements FileConfiguration, FileSystemBased
+{
+ /** Constant for the configuration reload event.*/
+ public static final int EVENT_RELOAD = 20;
+
+ /** Constant fro the configuration changed event. */
+ public static final int EVENT_CONFIG_CHANGED = 21;
+
+ /** The root of the file scheme */
+ private static final String FILE_SCHEME = "file:";
+
+ /** Stores the file name.*/
+ protected String fileName;
+
+ /** Stores the base path.*/
+ protected String basePath;
+
+ /** The auto save flag.*/
+ protected boolean autoSave;
+
+ /** Holds a reference to the reloading strategy.*/
+ protected ReloadingStrategy strategy;
+
+ /** A lock object for protecting reload operations.*/
+ protected Object reloadLock = new Lock("AbstractFileConfiguration");
+
+ /** Stores the encoding of the configuration file.*/
+ private String encoding;
+
+ /** Stores the URL from which the configuration file was loaded.*/
+ private URL sourceURL;
+
+ /** A counter that prohibits reloading.*/
+ private int noReload;
+
+ /** The FileSystem being used for this Configuration */
+ private FileSystem fileSystem = FileSystem.getDefaultFileSystem();
+
+ /**
+ * Default constructor
+ *
+ * @since 1.1
+ */
+ public AbstractFileConfiguration()
+ {
+ initReloadingStrategy();
+ setLogger(LogFactory.getLog(getClass()));
+ addErrorLogListener();
+ }
+
+ /**
+ * Creates and loads the configuration from the specified file. The passed
+ * in string must be a valid file name, either absolute or relativ.
+ *
+ * @param fileName The name of the file to load.
+ *
+ * @throws ConfigurationException Error while loading the file
+ * @since 1.1
+ */
+ public AbstractFileConfiguration(String fileName) throws ConfigurationException
+ {
+ this();
+
+ // store the file name
+ setFileName(fileName);
+
+ // load the file
+ load();
+ }
+
+ /**
+ * Creates and loads the configuration from the specified file.
+ *
+ * @param file The file to load.
+ * @throws ConfigurationException Error while loading the file
+ * @since 1.1
+ */
+ public AbstractFileConfiguration(File file) throws ConfigurationException
+ {
+ this();
+
+ // set the file and update the url, the base path and the file name
+ setFile(file);
+
+ // load the file
+ if (file.exists())
+ {
+ load();
+ }
+ }
+
+ /**
+ * Creates and loads the configuration from the specified URL.
+ *
+ * @param url The location of the file to load.
+ * @throws ConfigurationException Error while loading the file
+ * @since 1.1
+ */
+ public AbstractFileConfiguration(URL url) throws ConfigurationException
+ {
+ this();
+
+ // set the URL and update the base path and the file name
+ setURL(url);
+
+ // load the file
+ load();
+ }
+
+ public void setFileSystem(FileSystem fileSystem)
+ {
+ if (fileSystem == null)
+ {
+ throw new NullPointerException("A valid FileSystem must be specified");
+ }
+ this.fileSystem = fileSystem;
+ }
+
+ public void resetFileSystem()
+ {
+ this.fileSystem = FileSystem.getDefaultFileSystem();
+ }
+
+ public FileSystem getFileSystem()
+ {
+ return this.fileSystem;
+ }
+
+ public Object getReloadLock()
+ {
+ return reloadLock;
+ }
+
+
+ /**
+ * Load the configuration from the underlying location.
+ *
+ * @throws ConfigurationException if loading of the configuration fails
+ */
+ public void load() throws ConfigurationException
+ {
+ if (sourceURL != null)
+ {
+ load(sourceURL);
+ }
+ else
+ {
+ load(getFileName());
+ }
+ }
+
+ /**
+ * Locate the specified file and load the configuration. If the configuration is
+ * already associated with a source, the current source is not changed.
+ * Otherwise (i.e. this is the first load operation), the source URL and
+ * the base path are set now based on the source to be loaded.
+ *
+ * @param fileName the name of the file to be loaded
+ * @throws ConfigurationException if an error occurs
+ */
+ public void load(String fileName) throws ConfigurationException
+ {
+ try
+ {
+ URL url = ConfigurationUtils.locate(this.fileSystem, basePath, fileName);
+
+ if (url == null)
+ {
+ throw new ConfigurationException("Cannot locate configuration source " + fileName);
+ }
+ load(url);
+ }
+ catch (ConfigurationException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new ConfigurationException("Unable to load the configuration file " + fileName, e);
+ }
+ }
+
+ /**
+ * Load the configuration from the specified file. If the configuration is
+ * already associated with a source, the current source is not changed.
+ * Otherwise (i.e. this is the first load operation), the source URL and
+ * the base path are set now based on the source to be loaded.
+ *
+ * @param file the file to load
+ * @throws ConfigurationException if an error occurs
+ */
+ public void load(File file) throws ConfigurationException
+ {
+ try
+ {
+ load(ConfigurationUtils.toURL(file));
+ }
+ catch (ConfigurationException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new ConfigurationException("Unable to load the configuration file " + file, e);
+ }
+ }
+
+ /**
+ * Load the configuration from the specified URL. If the configuration is
+ * already associated with a source, the current source is not changed.
+ * Otherwise (i.e. this is the first load operation), the source URL and
+ * the base path are set now based on the source to be loaded.
+ *
+ * @param url the URL of the file to be loaded
+ * @throws ConfigurationException if an error occurs
+ */
+ public void load(URL url) throws ConfigurationException
+ {
+ if (sourceURL == null)
+ {
+ if (StringUtils.isEmpty(getBasePath()))
+ {
+ // ensure that we have a valid base path
+ setBasePath(url.toString());
+ }
+ sourceURL = url;
+ }
+
+ InputStream in = null;
+
+ try
+ {
+ in = fileSystem.getInputStream(url);
+ load(in);
+ }
+ catch (ConfigurationException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new ConfigurationException("Unable to load the configuration from the URL " + url, e);
+ }
+ finally
+ {
+ // close the input stream
+ try
+ {
+ if (in != null)
+ {
+ in.close();
+ }
+ }
+ catch (IOException e)
+ {
+ getLogger().warn("Could not close input stream", e);
+ }
+ }
+ }
+
+ /**
+ * Load the configuration from the specified stream, using the encoding
+ * returned by {@link #getEncoding()}.
+ *
+ * @param in the input stream
+ *
+ * @throws ConfigurationException if an error occurs during the load operation
+ */
+ public void load(InputStream in) throws ConfigurationException
+ {
+ load(in, getEncoding());
+ }
+
+ /**
+ * Load the configuration from the specified stream, using the specified
+ * encoding. If the encoding is null the default encoding is used.
+ *
+ * @param in the input stream
+ * @param encoding the encoding used. {@code null} to use the default encoding
+ *
+ * @throws ConfigurationException if an error occurs during the load operation
+ */
+ public void load(InputStream in, String encoding) throws ConfigurationException
+ {
+ Reader reader = null;
+
+ if (encoding != null)
+ {
+ try
+ {
+ reader = new InputStreamReader(in, encoding);
+ }
+ catch (UnsupportedEncodingException e)
+ {
+ throw new ConfigurationException(
+ "The requested encoding is not supported, try the default encoding.", e);
+ }
+ }
+
+ if (reader == null)
+ {
+ reader = new InputStreamReader(in);
+ }
+
+ load(reader);
+ }
+
+ /**
+ * Save the configuration. Before this method can be called a valid file
+ * name must have been set.
+ *
+ * @throws ConfigurationException if an error occurs or no file name has
+ * been set yet
+ */
+ public void save() throws ConfigurationException
+ {
+ if (getFileName() == null)
+ {
+ throw new ConfigurationException("No file name has been set!");
+ }
+
+ if (sourceURL != null)
+ {
+ save(sourceURL);
+ }
+ else
+ {
+ save(fileName);
+ }
+ strategy.init();
+ }
+
+ /**
+ * Save the configuration to the specified file. This doesn't change the
+ * source of the configuration, use setFileName() if you need it.
+ *
+ * @param fileName the file name
+ *
+ * @throws ConfigurationException if an error occurs during the save operation
+ */
+ public void save(String fileName) throws ConfigurationException
+ {
+ try
+ {
+ URL url = this.fileSystem.getURL(basePath, fileName);
+
+ if (url == null)
+ {
+ throw new ConfigurationException("Cannot locate configuration source " + fileName);
+ }
+ save(url);
+ /*File file = ConfigurationUtils.getFile(basePath, fileName);
+ if (file == null)
+ {
+ throw new ConfigurationException("Invalid file name for save: " + fileName);
+ }
+ save(file); */
+ }
+ catch (ConfigurationException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new ConfigurationException("Unable to save the configuration to the file " + fileName, e);
+ }
+ }
+
+ /**
+ * Save the configuration to the specified URL.
+ * This doesn't change the source of the configuration, use setURL()
+ * if you need it.
+ *
+ * @param url the URL
+ *
+ * @throws ConfigurationException if an error occurs during the save operation
+ */
+ public void save(URL url) throws ConfigurationException
+ {
+ OutputStream out = null;
+ try
+ {
+ out = fileSystem.getOutputStream(url);
+ save(out);
+ if (out instanceof VerifiableOutputStream)
+ {
+ ((VerifiableOutputStream) out).verify();
+ }
+ }
+ catch (IOException e)
+ {
+ throw new ConfigurationException("Could not save to URL " + url, e);
+ }
+ finally
+ {
+ closeSilent(out);
+ }
+ }
+
+ /**
+ * Save the configuration to the specified file. The file is created
+ * automatically if it doesn't exist. This doesn't change the source
+ * of the configuration, use {@link #setFile} if you need it.
+ *
+ * @param file the target file
+ *
+ * @throws ConfigurationException if an error occurs during the save operation
+ */
+ public void save(File file) throws ConfigurationException
+ {
+ OutputStream out = null;
+
+ try
+ {
+ out = fileSystem.getOutputStream(file);
+ save(out);
+ }
+ finally
+ {
+ closeSilent(out);
+ }
+ }
+
+ /**
+ * Save the configuration to the specified stream, using the encoding
+ * returned by {@link #getEncoding()}.
+ *
+ * @param out the output stream
+ *
+ * @throws ConfigurationException if an error occurs during the save operation
+ */
+ public void save(OutputStream out) throws ConfigurationException
+ {
+ save(out, getEncoding());
+ }
+
+ /**
+ * Save the configuration to the specified stream, using the specified
+ * encoding. If the encoding is null the default encoding is used.
+ *
+ * @param out the output stream
+ * @param encoding the encoding to use
+ * @throws ConfigurationException if an error occurs during the save operation
+ */
+ public void save(OutputStream out, String encoding) throws ConfigurationException
+ {
+ Writer writer = null;
+
+ if (encoding != null)
+ {
+ try
+ {
+ writer = new OutputStreamWriter(out, encoding);
+ }
+ catch (UnsupportedEncodingException e)
+ {
+ throw new ConfigurationException(
+ "The requested encoding is not supported, try the default encoding.", e);
+ }
+ }
+
+ if (writer == null)
+ {
+ writer = new OutputStreamWriter(out);
+ }
+
+ save(writer);
+ }
+
+ /**
+ * Return the name of the file.
+ *
+ * @return the file name
+ */
+ public String getFileName()
+ {
+ return fileName;
+ }
+
+ /**
+ * Set the name of the file. The passed in file name can contain a
+ * relative path.
+ * It must be used when referring files with relative paths from classpath.
+ * Use {@link AbstractFileConfiguration#setPath(String)
+ * setPath()} to set a full qualified file name.
+ *
+ * @param fileName the name of the file
+ */
+ public void setFileName(String fileName)
+ {
+ if (fileName != null && fileName.startsWith(FILE_SCHEME) && !fileName.startsWith("file://"))
+ {
+ fileName = "file://" + fileName.substring(FILE_SCHEME.length());
+ }
+
+ sourceURL = null;
+ this.fileName = fileName;
+ getLogger().debug("FileName set to " + fileName);
+ }
+
+ /**
+ * Return the base path.
+ *
+ * @return the base path
+ * @see FileConfiguration#getBasePath()
+ */
+ public String getBasePath()
+ {
+ return basePath;
+ }
+
+ /**
+ * Sets the base path. The base path is typically either a path to a
+ * directory or a URL. Together with the value passed to the
+ * {@code setFileName()} method it defines the location of the
+ * configuration file to be loaded. The strategies for locating the file are
+ * quite tolerant. For instance if the file name is already an absolute path
+ * or a fully defined URL, the base path will be ignored. The base path can
+ * also be a URL, in which case the file name is interpreted in this URL's
+ * context. Because the base path is used by some of the derived classes for
+ * resolving relative file names it should contain a meaningful value. If
+ * other methods are used for determining the location of the configuration
+ * file (e.g. {@code setFile()} or {@code setURL()}), the
+ * base path is automatically set.
+ *
+ * @param basePath the base path.
+ */
+ public void setBasePath(String basePath)
+ {
+ if (basePath != null && basePath.startsWith(FILE_SCHEME) && !basePath.startsWith("file://"))
+ {
+ basePath = "file://" + basePath.substring(FILE_SCHEME.length());
+ }
+ sourceURL = null;
+ this.basePath = basePath;
+ getLogger().debug("Base path set to " + basePath);
+ }
+
+ /**
+ * Return the file where the configuration is stored. If the base path is a
+ * URL with a protocol different than "file", or the configuration
+ * file is within a compressed archive, the return value
+ * will not point to a valid file object.
+ *
+ * @return the file where the configuration is stored; this can be <b>null</b>
+ */
+ public File getFile()
+ {
+ if (getFileName() == null && sourceURL == null)
+ {
+ return null;
+ }
+ else if (sourceURL != null)
+ {
+ return ConfigurationUtils.fileFromURL(sourceURL);
+ }
+ else
+ {
+ return ConfigurationUtils.getFile(getBasePath(), getFileName());
+ }
+ }
+
+ /**
+ * Set the file where the configuration is stored. The passed in file is
+ * made absolute if it is not yet. Then the file's path component becomes
+ * the base path and its name component becomes the file name.
+ *
+ * @param file the file where the configuration is stored
+ */
+ public void setFile(File file)
+ {
+ sourceURL = null;
+ setFileName(file.getName());
+ setBasePath((file.getParentFile() != null) ? file.getParentFile()
+ .getAbsolutePath() : null);
+ }
+
+ /**
+ * Returns the full path to the file this configuration is based on. The
+ * return value is a valid File path only if this configuration is based on
+ * a file on the local disk.
+ * If the configuration was loaded from a packed archive the returned value
+ * is the string form of the URL from which the configuration was loaded.
+ *
+ * @return the full path to the configuration file
+ */
+ public String getPath()
+ {
+ return fileSystem.getPath(getFile(), sourceURL, getBasePath(), getFileName());
+ }
+
+ /**
+ * Sets the location of this configuration as a full or relative path name.
+ * The passed in path should represent a valid file name on the file system.
+ * It must not be used to specify relative paths for files that exist
+ * in classpath, either plain file system or compressed archive,
+ * because this method expands any relative path to an absolute one which
+ * may end in an invalid absolute path for classpath references.
+ *
+ * @param path the full path name of the configuration file
+ */
+ public void setPath(String path)
+ {
+ setFile(new File(path));
+ }
+
+ URL getSourceURL()
+ {
+ return sourceURL;
+ }
+
+ /**
+ * Return the URL where the configuration is stored.
+ *
+ * @return the configuration's location as URL
+ */
+ public URL getURL()
+ {
+ return (sourceURL != null) ? sourceURL
+ : ConfigurationUtils.locate(this.fileSystem, getBasePath(), getFileName());
+ }
+
+ /**
+ * Set the location of this configuration as a URL. For loading this can be
+ * an arbitrary URL with a supported protocol. If the configuration is to
+ * be saved, too, a URL with the "file" protocol should be
+ * provided.
+ *
+ * @param url the location of this configuration as URL
+ */
+ public void setURL(URL url)
+ {
+ setBasePath(ConfigurationUtils.getBasePath(url));
+ setFileName(ConfigurationUtils.getFileName(url));
+ sourceURL = url;
+ getLogger().debug("URL set to " + url);
+ }
+
+ public void setAutoSave(boolean autoSave)
+ {
+ this.autoSave = autoSave;
+ }
+
+ public boolean isAutoSave()
+ {
+ return autoSave;
+ }
+
+ /**
+ * Save the configuration if the automatic persistence is enabled
+ * and if a file is specified.
+ */
+ protected void possiblySave()
+ {
+ if (autoSave && fileName != null)
+ {
+ try
+ {
+ save();
+ }
+ catch (ConfigurationException e)
+ {
+ throw new ConfigurationRuntimeException("Failed to auto-save", e);
+ }
+ }
+ }
+
+ /**
+ * Adds a new property to this configuration. This implementation checks if
+ * the auto save mode is enabled and saves the configuration if necessary.
+ *
+ * @param key the key of the new property
+ * @param value the value
+ */
+ @Override
+ public void addProperty(String key, Object value)
+ {
+ synchronized (reloadLock)
+ {
+ super.addProperty(key, value);
+ possiblySave();
+ }
+ }
+
+ /**
+ * Sets a new value for the specified property. This implementation checks
+ * if the auto save mode is enabled and saves the configuration if
+ * necessary.
+ *
+ * @param key the key of the affected property
+ * @param value the value
+ */
+ @Override
+ public void setProperty(String key, Object value)
+ {
+ synchronized (reloadLock)
+ {
+ super.setProperty(key, value);
+ possiblySave();
+ }
+ }
+
+ @Override
+ public void clearProperty(String key)
+ {
+ synchronized (reloadLock)
+ {
+ super.clearProperty(key);
+ possiblySave();
+ }
+ }
+
+ public ReloadingStrategy getReloadingStrategy()
+ {
+ return strategy;
+ }
+
+ public void setReloadingStrategy(ReloadingStrategy strategy)
+ {
+ this.strategy = strategy;
+ strategy.setConfiguration(this);
+ strategy.init();
+ }
+
+ /**
+ * Performs a reload operation if necessary. This method is called on each
+ * access of this configuration. It asks the associated reloading strategy
+ * whether a reload should be performed. If this is the case, the
+ * configuration is cleared and loaded again from its source. If this
+ * operation causes an exception, the registered error listeners will be
+ * notified. The error event passed to the listeners is of type
+ * {@code EVENT_RELOAD} and contains the exception that caused the
+ * event.
+ */
+ public void reload()
+ {
+ reload(false);
+ }
+
+ public boolean reload(boolean checkReload)
+ {
+ synchronized (reloadLock)
+ {
+ if (noReload == 0)
+ {
+ try
+ {
+ enterNoReload(); // avoid reentrant calls
+
+ if (strategy.reloadingRequired())
+ {
+ if (getLogger().isInfoEnabled())
+ {
+ getLogger().info("Reloading configuration. URL is " + getURL());
+ }
+ refresh();
+
+ // notify the strategy
+ strategy.reloadingPerformed();
+ }
+ }
+ catch (Exception e)
+ {
+ fireError(EVENT_RELOAD, null, null, e);
+ // todo rollback the changes if the file can't be reloaded
+ if (checkReload)
+ {
+ return false;
+ }
+ }
+ finally
+ {
+ exitNoReload();
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Reloads the associated configuration file. This method first clears the
+ * content of this configuration, then the associated configuration file is
+ * loaded again. Updates on this configuration which have not yet been saved
+ * are lost. Calling this method is like invoking {@code reload()}
+ * without checking the reloading strategy.
+ *
+ * @throws ConfigurationException if an error occurs
+ * @since 1.7
+ */
+ public void refresh() throws ConfigurationException
+ {
+ fireEvent(EVENT_RELOAD, null, getURL(), true);
+ setDetailEvents(false);
+ boolean autoSaveBak = this.isAutoSave(); // save the current state
+ this.setAutoSave(false); // deactivate autoSave to prevent information loss
+ try
+ {
+ clear();
+ load();
+ }
+ finally
+ {
+ this.setAutoSave(autoSaveBak); // set autoSave to previous value
+ setDetailEvents(true);
+ }
+ fireEvent(EVENT_RELOAD, null, getURL(), false);
+ }
+
+ /**
+ * Send notification that the configuration has changed.
+ */
+ public void configurationChanged()
+ {
+ fireEvent(EVENT_CONFIG_CHANGED, null, getURL(), true);
+ }
+
+ /**
+ * Enters the "No reloading mode". As long as this mode is active
+ * no reloading will be performed. This is necessary for some
+ * implementations of {@code save()} in derived classes, which may
+ * cause a reload while accessing the properties to save. This may cause the
+ * whole configuration to be erased. To avoid this, this method can be
+ * called first. After a call to this method there always must be a
+ * corresponding call of {@link #exitNoReload()} later! (If
+ * necessary, {@code finally} blocks must be used to ensure this.
+ */
+ protected void enterNoReload()
+ {
+ synchronized (reloadLock)
+ {
+ noReload++;
+ }
+ }
+
+ /**
+ * Leaves the "No reloading mode".
+ *
+ * @see #enterNoReload()
+ */
+ protected void exitNoReload()
+ {
+ synchronized (reloadLock)
+ {
+ if (noReload > 0) // paranoia check
+ {
+ noReload--;
+ }
+ }
+ }
+
+ /**
+ * Sends an event to all registered listeners. This implementation ensures
+ * that no reloads are performed while the listeners are invoked. So
+ * infinite loops can be avoided that can be caused by event listeners
+ * accessing the configuration's properties when they are invoked.
+ *
+ * @param type the event type
+ * @param propName the name of the property
+ * @param propValue the value of the property
+ * @param before the before update flag
+ */
+ @Override
+ protected void fireEvent(int type, String propName, Object propValue, boolean before)
+ {
+ enterNoReload();
+ try
+ {
+ super.fireEvent(type, propName, propValue, before);
+ }
+ finally
+ {
+ exitNoReload();
+ }
+ }
+
+ @Override
+ public Object getProperty(String key)
+ {
+ synchronized (reloadLock)
+ {
+ reload();
+ return super.getProperty(key);
+ }
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ reload();
+ synchronized (reloadLock)
+ {
+ return super.isEmpty();
+ }
+ }
+
+ @Override
+ public boolean containsKey(String key)
+ {
+ reload();
+ synchronized (reloadLock)
+ {
+ return super.containsKey(key);
+ }
+ }
+
+ /**
+ * Returns an {@code Iterator} with the keys contained in this
+ * configuration. This implementation performs a reload if necessary before
+ * obtaining the keys. The {@code Iterator} returned by this method
+ * points to a snapshot taken when this method was called. Later changes at
+ * the set of keys (including those caused by a reload) won't be visible.
+ * This is because a reload can happen at any time during iteration, and it
+ * is impossible to determine how this reload affects the current iteration.
+ * When using the iterator a client has to be aware that changes of the
+ * configuration are possible at any time. For instance, if after a reload
+ * operation some keys are no longer present, the iterator will still return
+ * those keys because they were found when it was created.
+ *
+ * @return an {@code Iterator} with the keys of this configuration
+ */
+ @Override
+ public Iterator<String> getKeys()
+ {
+ reload();
+ List<String> keyList = new LinkedList<String>();
+ enterNoReload();
+ try
+ {
+ for (Iterator<String> it = super.getKeys(); it.hasNext();)
+ {
+ keyList.add(it.next());
+ }
+
+ return keyList.iterator();
+ }
+ finally
+ {
+ exitNoReload();
+ }
+ }
+
+ public String getEncoding()
+ {
+ return encoding;
+ }
+
+ public void setEncoding(String encoding)
+ {
+ this.encoding = encoding;
+ }
+
+ /**
+ * Creates a copy of this configuration. The new configuration object will
+ * contain the same properties as the original, but it will lose any
+ * connection to a source file (if one exists); this includes setting the
+ * source URL, base path, and file name to <b>null</b>. This is done to
+ * avoid race conditions if both the original and the copy are modified and
+ * then saved.
+ *
+ * @return the copy
+ * @since 1.3
+ */
+ @Override
+ public Object clone()
+ {
+ AbstractFileConfiguration copy = (AbstractFileConfiguration) super.clone();
+ copy.setBasePath(null);
+ copy.setFileName(null);
+ copy.initReloadingStrategy();
+ return copy;
+ }
+
+ /**
+ * Helper method for initializing the reloading strategy.
+ */
+ private void initReloadingStrategy()
+ {
+ setReloadingStrategy(new InvariantReloadingStrategy());
+ }
+
+ /**
+ * A helper method for closing an output stream. Occurring exceptions will
+ * be ignored.
+ *
+ * @param out the output stream to be closed (may be <b>null</b>)
+ * @since 1.5
+ */
+ protected void closeSilent(OutputStream out)
+ {
+ try
+ {
+ if (out != null)
+ {
+ out.close();
+ }
+ }
+ catch (IOException e)
+ {
+ getLogger().warn("Could not close output stream", e);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/AbstractHierarchicalFileConfiguration.java b/src/main/java/org/apache/commons/configuration/AbstractHierarchicalFileConfiguration.java
new file mode 100644
index 0000000..7986776
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/AbstractHierarchicalFileConfiguration.java
@@ -0,0 +1,579 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.commons.configuration.event.ConfigurationErrorEvent;
+import org.apache.commons.configuration.event.ConfigurationErrorListener;
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.configuration.reloading.Reloadable;
+import org.apache.commons.configuration.reloading.ReloadingStrategy;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+
+/**
+ * <p>Base class for implementing file based hierarchical configurations.</p>
+ * <p>This class serves an analogous purpose as the
+ * {@link AbstractFileConfiguration} class for non hierarchical
+ * configurations. It behaves in exactly the same way, so please refer to the
+ * documentation of {@code AbstractFileConfiguration} for further details.</p>
+ *
+ * @since 1.2
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: AbstractHierarchicalFileConfiguration.java 1206575 2011-11-26 20:07:52Z oheger $
+ */
+public abstract class AbstractHierarchicalFileConfiguration
+extends HierarchicalConfiguration
+implements FileConfiguration, ConfigurationListener, ConfigurationErrorListener, FileSystemBased,
+ Reloadable
+{
+ /** Stores the delegate used for implementing functionality related to the
+ * {@code FileConfiguration} interface.
+ */
+ private FileConfigurationDelegate delegate;
+
+ /**
+ * Creates a new instance of {@code AbstractHierarchicalFileConfiguration}.
+ */
+ protected AbstractHierarchicalFileConfiguration()
+ {
+ initialize();
+ }
+
+ /**
+ * Creates a new instance of
+ * {@code AbstractHierarchicalFileConfiguration} and copies the
+ * content of the specified configuration into this object.
+ *
+ * @param c the configuration to copy
+ * @since 1.4
+ */
+ protected AbstractHierarchicalFileConfiguration(HierarchicalConfiguration c)
+ {
+ super(c);
+ initialize();
+ }
+
+ /**
+ * Creates and loads the configuration from the specified file.
+ *
+ * @param fileName The name of the plist file to load.
+ * @throws ConfigurationException Error while loading the file
+ */
+ public AbstractHierarchicalFileConfiguration(String fileName) throws ConfigurationException
+ {
+ this();
+ // store the file name
+ delegate.setFileName(fileName);
+
+ // load the file
+ load();
+ }
+
+ /**
+ * Creates and loads the configuration from the specified file.
+ *
+ * @param file The configuration file to load.
+ * @throws ConfigurationException Error while loading the file
+ */
+ public AbstractHierarchicalFileConfiguration(File file) throws ConfigurationException
+ {
+ this();
+ // set the file and update the url, the base path and the file name
+ setFile(file);
+
+ // load the file
+ if (file.exists())
+ {
+ load();
+ }
+ }
+
+ /**
+ * Creates and loads the configuration from the specified URL.
+ *
+ * @param url The location of the configuration file to load.
+ * @throws ConfigurationException Error while loading the file
+ */
+ public AbstractHierarchicalFileConfiguration(URL url) throws ConfigurationException
+ {
+ this();
+ // set the URL and update the base path and the file name
+ setURL(url);
+
+ // load the file
+ load();
+ }
+
+ /**
+ * Initializes this instance, mainly the internally used delegate object.
+ */
+ private void initialize()
+ {
+ delegate = createDelegate();
+ initDelegate(delegate);
+ }
+
+ @Override
+ protected void addPropertyDirect(String key, Object obj)
+ {
+ synchronized (delegate.getReloadLock())
+ {
+ super.addPropertyDirect(key, obj);
+ delegate.possiblySave();
+ }
+ }
+
+ @Override
+ public void clearProperty(String key)
+ {
+ synchronized (delegate.getReloadLock())
+ {
+ super.clearProperty(key);
+ delegate.possiblySave();
+ }
+ }
+
+ @Override
+ public void clearTree(String key)
+ {
+ synchronized (delegate.getReloadLock())
+ {
+ super.clearTree(key);
+ delegate.possiblySave();
+ }
+ }
+
+ @Override
+ public void setProperty(String key, Object value)
+ {
+ synchronized (delegate.getReloadLock())
+ {
+ super.setProperty(key, value);
+ delegate.possiblySave();
+ }
+ }
+
+ public void load() throws ConfigurationException
+ {
+ delegate.load();
+ }
+
+ public void load(String fileName) throws ConfigurationException
+ {
+ delegate.load(fileName);
+ }
+
+ public void load(File file) throws ConfigurationException
+ {
+ delegate.load(file);
+ }
+
+ public void load(URL url) throws ConfigurationException
+ {
+ delegate.load(url);
+ }
+
+ public void load(InputStream in) throws ConfigurationException
+ {
+ delegate.load(in);
+ }
+
+ public void load(InputStream in, String encoding) throws ConfigurationException
+ {
+ delegate.load(in, encoding);
+ }
+
+ public void save() throws ConfigurationException
+ {
+ delegate.save();
+ }
+
+ public void save(String fileName) throws ConfigurationException
+ {
+ delegate.save(fileName);
+ }
+
+ public void save(File file) throws ConfigurationException
+ {
+ delegate.save(file);
+ }
+
+ public void save(URL url) throws ConfigurationException
+ {
+ delegate.save(url);
+ }
+
+ public void save(OutputStream out) throws ConfigurationException
+ {
+ delegate.save(out);
+ }
+
+ public void save(OutputStream out, String encoding) throws ConfigurationException
+ {
+ delegate.save(out, encoding);
+ }
+
+ public String getFileName()
+ {
+ return delegate.getFileName();
+ }
+
+ public void setFileName(String fileName)
+ {
+ delegate.setFileName(fileName);
+ }
+
+ public String getBasePath()
+ {
+ return delegate.getBasePath();
+ }
+
+ public void setBasePath(String basePath)
+ {
+ delegate.setBasePath(basePath);
+ }
+
+ public File getFile()
+ {
+ return delegate.getFile();
+ }
+
+ public void setFile(File file)
+ {
+ delegate.setFile(file);
+ }
+
+ public URL getURL()
+ {
+ return delegate.getURL();
+ }
+
+ public void setURL(URL url)
+ {
+ delegate.setURL(url);
+ }
+
+ public void setAutoSave(boolean autoSave)
+ {
+ delegate.setAutoSave(autoSave);
+ }
+
+ public boolean isAutoSave()
+ {
+ return delegate.isAutoSave();
+ }
+
+ public ReloadingStrategy getReloadingStrategy()
+ {
+ return delegate.getReloadingStrategy();
+ }
+
+ public void setReloadingStrategy(ReloadingStrategy strategy)
+ {
+ delegate.setReloadingStrategy(strategy);
+ }
+
+ public void reload()
+ {
+ reload(false);
+ }
+
+ private boolean reload(boolean checkReload)
+ {
+ synchronized (delegate.getReloadLock())
+ {
+ setDetailEvents(false);
+ try
+ {
+ return delegate.reload(checkReload);
+ }
+ finally
+ {
+ setDetailEvents(true);
+ }
+ }
+ }
+
+ /**
+ * Reloads the associated configuration file. This method first clears the
+ * content of this configuration, then the associated configuration file is
+ * loaded again. Updates on this configuration which have not yet been saved
+ * are lost. Calling this method is like invoking {@code reload()}
+ * without checking the reloading strategy.
+ *
+ * @throws ConfigurationException if an error occurs
+ * @since 1.7
+ */
+ public void refresh() throws ConfigurationException
+ {
+ delegate.refresh();
+ }
+
+ public String getEncoding()
+ {
+ return delegate.getEncoding();
+ }
+
+ public void setEncoding(String encoding)
+ {
+ delegate.setEncoding(encoding);
+ }
+
+ @Override
+ public Object getReloadLock()
+ {
+ return delegate.getReloadLock();
+ }
+
+ @Override
+ public boolean containsKey(String key)
+ {
+ reload();
+ synchronized (delegate.getReloadLock())
+ {
+ return super.containsKey(key);
+ }
+ }
+
+ @Override
+ public Iterator<String> getKeys()
+ {
+ reload();
+ synchronized (delegate.getReloadLock())
+ {
+ return super.getKeys();
+ }
+ }
+
+ @Override
+ public Iterator<String> getKeys(String prefix)
+ {
+ reload();
+ synchronized (delegate.getReloadLock())
+ {
+ return super.getKeys(prefix);
+ }
+ }
+
+ @Override
+ public Object getProperty(String key)
+ {
+ if (reload(true))
+ {
+ // Avoid reloading again and getting the same error.
+ synchronized (delegate.getReloadLock())
+ {
+ return super.getProperty(key);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ reload();
+ synchronized (delegate.getReloadLock())
+ {
+ return super.isEmpty();
+ }
+ }
+
+ /**
+ * Directly adds sub nodes to this configuration. This implementation checks
+ * whether auto save is necessary after executing the operation.
+ *
+ * @param key the key where the nodes are to be added
+ * @param nodes a collection with the nodes to be added
+ * @since 1.5
+ */
+ @Override
+ public void addNodes(String key, Collection<? extends ConfigurationNode> nodes)
+ {
+ synchronized (delegate.getReloadLock())
+ {
+ super.addNodes(key, nodes);
+ delegate.possiblySave();
+ }
+ }
+
+ /**
+ * Fetches a list of nodes, which are selected by the specified key. This
+ * implementation will perform a reload if necessary.
+ *
+ * @param key the key
+ * @return a list with the selected nodes
+ */
+ @Override
+ protected List<ConfigurationNode> fetchNodeList(String key)
+ {
+ reload();
+ synchronized (delegate.getReloadLock())
+ {
+ return super.fetchNodeList(key);
+ }
+ }
+
+ /**
+ * Reacts on changes of an associated subnode configuration. If the auto
+ * save mechanism is active, the configuration must be saved.
+ *
+ * @param event the event describing the change
+ * @since 1.5
+ */
+ @Override
+ protected void subnodeConfigurationChanged(ConfigurationEvent event)
+ {
+ delegate.possiblySave();
+ super.subnodeConfigurationChanged(event);
+ }
+
+ /**
+ * Creates the file configuration delegate, i.e. the object that implements
+ * functionality required by the {@code FileConfiguration} interface.
+ * This base implementation will return an instance of the
+ * {@code FileConfigurationDelegate} class. Derived classes may
+ * override it to create a different delegate object.
+ *
+ * @return the file configuration delegate
+ */
+ protected FileConfigurationDelegate createDelegate()
+ {
+ return new FileConfigurationDelegate();
+ }
+
+ /**
+ * Helper method for initializing the file configuration delegate.
+ *
+ * @param del the delegate
+ */
+ private void initDelegate(FileConfigurationDelegate del)
+ {
+ del.addConfigurationListener(this);
+ del.addErrorListener(this);
+ del.setLogger(getLogger());
+ }
+
+ /**
+ * Reacts on configuration change events triggered by the delegate. These
+ * events are passed to the registered configuration listeners.
+ *
+ * @param event the triggered event
+ * @since 1.3
+ */
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ // deliver reload events to registered listeners
+ setDetailEvents(true);
+ try
+ {
+ fireEvent(event.getType(), event.getPropertyName(), event
+ .getPropertyValue(), event.isBeforeUpdate());
+ }
+ finally
+ {
+ setDetailEvents(false);
+ }
+ }
+
+ public void configurationError(ConfigurationErrorEvent event)
+ {
+ fireError(event.getType(), event.getPropertyName(), event.getPropertyValue(),
+ event.getCause());
+ }
+
+ /**
+ * Returns the file configuration delegate.
+ *
+ * @return the delegate
+ */
+ protected FileConfigurationDelegate getDelegate()
+ {
+ return delegate;
+ }
+
+ /**
+ * Allows to set the file configuration delegate.
+ * @param delegate the new delegate
+ */
+ protected void setDelegate(FileConfigurationDelegate delegate)
+ {
+ this.delegate = delegate;
+ }
+
+ /**
+ * Set the FileSystem to be used for this Configuration.
+ * @param fileSystem The FileSystem to use.
+ */
+ public void setFileSystem(FileSystem fileSystem)
+ {
+ delegate.setFileSystem(fileSystem);
+ }
+
+ /**
+ * Reset the FileSystem to the default;
+ */
+ public void resetFileSystem()
+ {
+ delegate.resetFileSystem();
+ }
+
+ /**
+ * Retrieve the FileSystem being used.
+ * @return The FileSystem.
+ */
+ public FileSystem getFileSystem()
+ {
+ return delegate.getFileSystem();
+ }
+
+ /**
+ * A special implementation of the {@code FileConfiguration} interface that is
+ * used internally to implement the {@code FileConfiguration} methods
+ * for hierarchical configurations.
+ */
+ protected class FileConfigurationDelegate extends AbstractFileConfiguration
+ {
+ public void load(Reader in) throws ConfigurationException
+ {
+ AbstractHierarchicalFileConfiguration.this.load(in);
+ }
+
+ public void save(Writer out) throws ConfigurationException
+ {
+ AbstractHierarchicalFileConfiguration.this.save(out);
+ }
+
+ @Override
+ public void clear()
+ {
+ AbstractHierarchicalFileConfiguration.this.clear();
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/BaseConfiguration.java b/src/main/java/org/apache/commons/configuration/BaseConfiguration.java
new file mode 100644
index 0000000..1e73d1c
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/BaseConfiguration.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Basic configuration class. Stores the configuration data but does not
+ * provide any load or save functions. If you want to load your Configuration
+ * from a file use PropertiesConfiguration or XmlConfiguration.
+ *
+ * This class extends normal Java properties by adding the possibility
+ * to use the same key many times concatenating the value strings
+ * instead of overwriting them.
+ *
+ * @author <a href="mailto:stefano at apache.org">Stefano Mazzocchi</a>
+ * @author <a href="mailto:jon at latchkey.com">Jon S. Stevens</a>
+ * @author <a href="mailto:daveb at miceda-data">Dave Bryson</a>
+ * @author <a href="mailto:geirm at optonline.net">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:leon at opticode.co.za">Leon Messerschmidt</a>
+ * @author <a href="mailto:kjohnson at transparent.com">Kent Johnson</a>
+ * @author <a href="mailto:dlr at finemaltcoding.com">Daniel Rall</a>
+ * @author <a href="mailto:ipriha at surfeu.fi">Ilkka Priha</a>
+ * @author <a href="mailto:jvanzyl at apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:mpoeschl at marmot.at">Martin Poeschl</a>
+ * @author <a href="mailto:hps at intermeta.de">Henning P. Schmiedehausen</a>
+ * @author <a href="mailto:ksh at scand.com">Konstantin Shaposhnikov</a>
+ * @version $Id: BaseConfiguration.java 1231721 2012-01-15 18:32:07Z oheger $
+ */
+public class BaseConfiguration extends AbstractConfiguration implements Cloneable
+{
+ /** stores the configuration key-value pairs */
+ private Map<String, Object> store = new LinkedHashMap<String, Object>();
+
+ /**
+ * Adds a key/value pair to the map. This routine does no magic morphing.
+ * It ensures the keylist is maintained
+ *
+ * @param key key to use for mapping
+ * @param value object to store
+ */
+ @Override
+ protected void addPropertyDirect(String key, Object value)
+ {
+ Object previousValue = getProperty(key);
+
+ if (previousValue == null)
+ {
+ store.put(key, value);
+ }
+ else if (previousValue instanceof List)
+ {
+ // safe to case because we have created the lists ourselves
+ @SuppressWarnings("unchecked")
+ List<Object> valueList = (List<Object>) previousValue;
+ // the value is added to the existing list
+ valueList.add(value);
+ }
+ else
+ {
+ // the previous value is replaced by a list containing the previous value and the new value
+ List<Object> list = new ArrayList<Object>();
+ list.add(previousValue);
+ list.add(value);
+
+ store.put(key, list);
+ }
+ }
+
+ /**
+ * Read property from underlying map.
+ *
+ * @param key key to use for mapping
+ *
+ * @return object associated with the given configuration key.
+ */
+ public Object getProperty(String key)
+ {
+ return store.get(key);
+ }
+
+ /**
+ * Check if the configuration is empty
+ *
+ * @return {@code true} if Configuration is empty,
+ * {@code false} otherwise.
+ */
+ public boolean isEmpty()
+ {
+ return store.isEmpty();
+ }
+
+ /**
+ * check if the configuration contains the key
+ *
+ * @param key the configuration key
+ *
+ * @return {@code true} if Configuration contain given key,
+ * {@code false} otherwise.
+ */
+ public boolean containsKey(String key)
+ {
+ return store.containsKey(key);
+ }
+
+ /**
+ * Clear a property in the configuration.
+ *
+ * @param key the key to remove along with corresponding value.
+ */
+ @Override
+ protected void clearPropertyDirect(String key)
+ {
+ if (containsKey(key))
+ {
+ store.remove(key);
+ }
+ }
+
+ @Override
+ public void clear()
+ {
+ fireEvent(EVENT_CLEAR, null, null, true);
+ store.clear();
+ fireEvent(EVENT_CLEAR, null, null, false);
+ }
+
+ /**
+ * Get the list of the keys contained in the configuration
+ * repository.
+ *
+ * @return An Iterator.
+ */
+ public Iterator<String> getKeys()
+ {
+ return store.keySet().iterator();
+ }
+
+ /**
+ * Creates a copy of this object. This implementation will create a deep
+ * clone, i.e. the map that stores the properties is cloned, too. So changes
+ * performed at the copy won't affect the original and vice versa.
+ *
+ * @return the copy
+ * @since 1.3
+ */
+ @Override
+ public Object clone()
+ {
+ try
+ {
+ BaseConfiguration copy = (BaseConfiguration) super.clone();
+ // This is safe because the type of the map is known
+ @SuppressWarnings("unchecked")
+ Map<String, Object> clonedStore = (Map<String, Object>) ConfigurationUtils.clone(store);
+ copy.store = clonedStore;
+
+ // Handle collections in the map; they have to be cloned, too
+ for (Map.Entry<String, Object> e : store.entrySet())
+ {
+ if (e.getValue() instanceof Collection)
+ {
+ // This is safe because the collections were created by ourselves
+ @SuppressWarnings("unchecked")
+ Collection<String> strList = (Collection<String>) e.getValue();
+ copy.store.put(e.getKey(), new ArrayList<String>(strList));
+ }
+ }
+
+ return copy;
+ }
+ catch (CloneNotSupportedException cex)
+ {
+ // should not happen
+ throw new ConfigurationRuntimeException(cex);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/BaseConfigurationXMLReader.java b/src/main/java/org/apache/commons/configuration/BaseConfigurationXMLReader.java
new file mode 100644
index 0000000..20a3e7b
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/BaseConfigurationXMLReader.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+/**
+ * <p>A specialized SAX2 XML parser that processes configuration objects.</p>
+ *
+ * <p>This class mimics to be a SAX compliant XML parser. It is able to iterate
+ * over the keys in a configuration object and to generate corresponding SAX
+ * events. By registering a {@code ContentHandler} at an instance
+ * it is possible to perform XML processing on a configuration object.</p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: BaseConfigurationXMLReader.java 1206765 2011-11-27 16:46:11Z oheger $
+ */
+public class BaseConfigurationXMLReader extends ConfigurationXMLReader
+{
+ /** Stores the actual configuration.*/
+ private Configuration config;
+
+ /**
+ * Creates a new instance of {@code BaseConfigurationXMLReader}.
+ */
+ public BaseConfigurationXMLReader()
+ {
+ super();
+ }
+
+ /**
+ * Creates a new instance of {@code BaseConfigurationXMLReader} and
+ * sets the configuration object to be parsed.
+ *
+ * @param conf the configuration to be parsed
+ */
+ public BaseConfigurationXMLReader(Configuration conf)
+ {
+ this();
+ setConfiguration(conf);
+ }
+
+ /**
+ * Returns the actual configuration to be processed.
+ *
+ * @return the actual configuration
+ */
+ public Configuration getConfiguration()
+ {
+ return config;
+ }
+
+ /**
+ * Sets the configuration to be processed.
+ *
+ * @param conf the configuration
+ */
+ public void setConfiguration(Configuration conf)
+ {
+ config = conf;
+ }
+
+ /**
+ * Returns the configuration to be processed.
+ *
+ * @return the actual configuration
+ */
+ @Override
+ public Configuration getParsedConfiguration()
+ {
+ return getConfiguration();
+ }
+
+ /**
+ * The main SAX event generation method. This element uses an internal
+ * {@code HierarchicalConfigurationConverter} object to iterate over
+ * all keys in the actual configuration and to generate corresponding SAX
+ * events.
+ */
+ @Override
+ protected void processKeys()
+ {
+ fireElementStart(getRootName(), null);
+ new SAXConverter().process(getConfiguration());
+ fireElementEnd(getRootName());
+ }
+
+ /**
+ * An internally used helper class to iterate over all configuration keys
+ * ant to generate corresponding SAX events.
+ *
+ */
+ class SAXConverter extends HierarchicalConfigurationConverter
+ {
+ /**
+ * Callback for the start of an element.
+ *
+ * @param name the element name
+ * @param value the element value
+ */
+ @Override
+ protected void elementStart(String name, Object value)
+ {
+ fireElementStart(name, null);
+ if (value != null)
+ {
+ fireCharacters(value.toString());
+ }
+ }
+
+ /**
+ * Callback for the end of an element.
+ *
+ * @param name the element name
+ */
+ @Override
+ protected void elementEnd(String name)
+ {
+ fireElementEnd(name);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/CombinedConfiguration.java b/src/main/java/org/apache/commons/configuration/CombinedConfiguration.java
new file mode 100644
index 0000000..6a96e32
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/CombinedConfiguration.java
@@ -0,0 +1,976 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.DefaultConfigurationKey;
+import org.apache.commons.configuration.tree.DefaultConfigurationNode;
+import org.apache.commons.configuration.tree.DefaultExpressionEngine;
+import org.apache.commons.configuration.tree.ExpressionEngine;
+import org.apache.commons.configuration.tree.NodeCombiner;
+import org.apache.commons.configuration.tree.TreeUtils;
+import org.apache.commons.configuration.tree.UnionCombiner;
+import org.apache.commons.configuration.tree.ViewNode;
+
+/**
+ * <p>
+ * A hierarchical composite configuration class.
+ * </p>
+ * <p>
+ * This class maintains a list of configuration objects, which can be added
+ * using the divers {@code addConfiguration()} methods. After that the
+ * configurations can be accessed either by name (if one was provided when the
+ * configuration was added) or by index. For the whole set of managed
+ * configurations a logical node structure is constructed. For this purpose a
+ * {@link org.apache.commons.configuration.tree.NodeCombiner NodeCombiner}
+ * object can be set. This makes it possible to specify different algorithms for
+ * the combination process.
+ * </p>
+ * <p>
+ * The big advantage of this class is that it creates a truly hierarchical
+ * structure of all the properties stored in the contained configurations - even
+ * if some of them are no hierarchical configurations per se. So all enhanced
+ * features provided by a hierarchical configuration (e.g. choosing an
+ * expression engine) are applicable.
+ * </p>
+ * <p>
+ * The class works by registering itself as an event listener at all added
+ * configurations. So it gets notified whenever one of these configurations is
+ * changed and can invalidate its internal node structure. The next time a
+ * property is accessed the node structure will be re-constructed using the
+ * current state of the managed configurations. Note that, depending on the used
+ * {@code NodeCombiner}, this may be a complex operation.
+ * </p>
+ * <p>
+ * Because of the way a {@code CombinedConfiguration} is working it has
+ * more or less view character: it provides a logic view on the configurations
+ * it contains. In this constellation not all methods defined for hierarchical
+ * configurations - especially methods that update the stored properties - can
+ * be implemented in a consistent manner. Using such methods (like
+ * {@code addProperty()}, or {@code clearProperty()} on a
+ * {@code CombinedConfiguration} is not strictly forbidden, however,
+ * depending on the current {@link NodeCombiner} and the involved
+ * properties, the results may be different than expected. Some examples may
+ * illustrate this:
+ * </p>
+ * <p>
+ * <ul>
+ * <li>Imagine a {@code CombinedConfiguration} <em>cc</em> containing
+ * two child configurations with the following content:
+ * <dl>
+ * <dt>user.properties</dt>
+ * <dd>
+ *
+ * <pre>
+ * gui.background = blue
+ * gui.position = (10, 10, 400, 200)
+ * </pre>
+ *
+ * </dd>
+ * <dt>default.properties</dt>
+ * <dd>
+ *
+ * <pre>
+ * gui.background = black
+ * gui.foreground = white
+ * home.dir = /data
+ * </pre>
+ *
+ * </dd>
+ * </dl>
+ * As a {@code NodeCombiner} a
+ * {@link org.apache.commons.configuration.tree.OverrideCombiner OverrideCombiner}
+ * is used. This combiner will ensure that defined user settings take precedence
+ * over the default values. If the resulting {@code CombinedConfiguration}
+ * is queried for the background color, {@code blue} will be returned
+ * because this value is defined in {@code user.properties}. Now
+ * consider what happens if the key {@code gui.background} is removed
+ * from the {@code CombinedConfiguration}:
+ *
+ * <pre>cc.clearProperty("gui.background");</pre>
+ *
+ * Will a {@code cc.containsKey("gui.background")} now return <b>false</b>?
+ * No, it won't! The {@code clearProperty()} operation is executed on the
+ * node set of the combined configuration, which was constructed from the nodes
+ * of the two child configurations. It causes the value of the
+ * <em>background</em> node to be cleared, which is also part of the first
+ * child configuration. This modification of one of its child configurations
+ * causes the {@code CombinedConfiguration} to be re-constructed. This
+ * time the {@code OverrideCombiner} cannot find a
+ * {@code gui.background} property in the first child configuration, but
+ * it finds one in the second, and adds it to the resulting combined
+ * configuration. So the property is still present (with a different value now).</li>
+ * <li>{@code addProperty()} can also be problematic: Most node
+ * combiners use special view nodes for linking parts of the original
+ * configurations' data together. If new properties are added to such a special
+ * node, they do not belong to any of the managed configurations and thus hang
+ * in the air. Using the same configurations as in the last example, the
+ * statement
+ *
+ * <pre>
+ * addProperty("database.user", "scott");
+ * </pre>
+ *
+ * would cause such a hanging property. If now one of the child configurations
+ * is changed and the {@code CombinedConfiguration} is re-constructed,
+ * this property will disappear! (Add operations are not problematic if they
+ * result in a child configuration being updated. For instance an
+ * {@code addProperty("home.url", "localhost");} will alter the second
+ * child configuration - because the prefix <em>home</em> is here already
+ * present; when the {@code CombinedConfiguration} is re-constructed,
+ * this change is taken into account.)</li>
+ * </ul>
+ * Because of such problems it is recommended to perform updates only on the
+ * managed child configurations.
+ * </p>
+ * <p>
+ * Whenever the node structure of a {@code CombinedConfiguration} becomes
+ * invalid (either because one of the contained configurations was modified or
+ * because the {@code invalidate()} method was directly called) an event
+ * is generated. So this can be detected by interested event listeners. This
+ * also makes it possible to add a combined configuration into another one.
+ * </p>
+ * <p>
+ * Implementation note: Adding and removing configurations to and from a
+ * combined configuration is not thread-safe. If a combined configuration is
+ * manipulated by multiple threads, the developer has to take care about
+ * properly synchronization.
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @since 1.3
+ * @version $Id: CombinedConfiguration.java 1234985 2012-01-23 21:09:09Z oheger $
+ */
+public class CombinedConfiguration extends HierarchicalReloadableConfiguration implements
+ ConfigurationListener, Cloneable
+{
+ /**
+ * Constant for the invalidate event that is fired when the internal node
+ * structure becomes invalid.
+ */
+ public static final int EVENT_COMBINED_INVALIDATE = 40;
+
+ /**
+ * The serial version ID.
+ */
+ private static final long serialVersionUID = 8338574525528692307L;
+
+ /** Constant for the expression engine for parsing the at path. */
+ private static final DefaultExpressionEngine AT_ENGINE = new DefaultExpressionEngine();
+
+ /** Constant for the default node combiner. */
+ private static final NodeCombiner DEFAULT_COMBINER = new UnionCombiner();
+
+ /** Constant for the name of the property used for the reload check.*/
+ private static final String PROP_RELOAD_CHECK = "CombinedConfigurationReloadCheck";
+
+ /** Stores the combiner. */
+ private NodeCombiner nodeCombiner;
+
+ /** Stores the combined root node. */
+ private volatile ConfigurationNode combinedRoot;
+
+ /** Stores a list with the contained configurations. */
+ private List<ConfigData> configurations;
+
+ /** Stores a map with the named configurations. */
+ private Map<String, AbstractConfiguration> namedConfigurations;
+
+ /** The default behavior is to ignore exceptions that occur during reload */
+ private boolean ignoreReloadExceptions = true;
+
+ /** Set to true when the backing file has changed */
+ private boolean reloadRequired;
+
+ /**
+ * An expression engine used for converting child configurations to
+ * hierarchical ones.
+ */
+ private ExpressionEngine conversionExpressionEngine;
+
+ /** A flag whether an enhanced reload check is to be performed.*/
+ private boolean forceReloadCheck;
+
+ /**
+ * Creates a new instance of {@code CombinedConfiguration} and
+ * initializes the combiner to be used.
+ *
+ * @param comb the node combiner (can be <b>null</b>, then a union combiner
+ * is used as default)
+ */
+ public CombinedConfiguration(NodeCombiner comb)
+ {
+ setNodeCombiner((comb != null) ? comb : DEFAULT_COMBINER);
+ clear();
+ }
+
+ public CombinedConfiguration(NodeCombiner comb, Lock lock)
+ {
+ super(lock);
+ setNodeCombiner((comb != null) ? comb : DEFAULT_COMBINER);
+ clear();
+ }
+
+ public CombinedConfiguration(Lock lock)
+ {
+ this(null, lock);
+ }
+
+ /**
+ * Creates a new instance of {@code CombinedConfiguration} that uses
+ * a union combiner.
+ *
+ * @see org.apache.commons.configuration.tree.UnionCombiner
+ */
+ public CombinedConfiguration()
+ {
+ this(null, null);
+ }
+
+ /**
+ * Returns the node combiner that is used for creating the combined node
+ * structure.
+ *
+ * @return the node combiner
+ */
+ public NodeCombiner getNodeCombiner()
+ {
+ return nodeCombiner;
+ }
+
+ /**
+ * Sets the node combiner. This object will be used when the combined node
+ * structure is to be constructed. It must not be <b>null</b>, otherwise an
+ * {@code IllegalArgumentException} exception is thrown. Changing the
+ * node combiner causes an invalidation of this combined configuration, so
+ * that the new combiner immediately takes effect.
+ *
+ * @param nodeCombiner the node combiner
+ */
+ public void setNodeCombiner(NodeCombiner nodeCombiner)
+ {
+ if (nodeCombiner == null)
+ {
+ throw new IllegalArgumentException(
+ "Node combiner must not be null!");
+ }
+ this.nodeCombiner = nodeCombiner;
+ invalidate();
+ }
+
+ /**
+ * Returns a flag whether an enhanced reload check must be performed.
+ *
+ * @return the force reload check flag
+ * @since 1.4
+ */
+ public boolean isForceReloadCheck()
+ {
+ return forceReloadCheck;
+ }
+
+ /**
+ * Sets the force reload check flag. If this flag is set, each property
+ * access on this configuration will cause a reload check on the contained
+ * configurations. This is a workaround for a problem with some reload
+ * implementations that only check if a reload is required when they are
+ * triggered. Per default this mode is disabled. If the force reload check
+ * flag is set to <b>true</b>, accessing properties will be less
+ * efficient, but reloads on contained configurations will be detected.
+ *
+ * @param forceReloadCheck the value of the flag
+ * @since 1.4
+ */
+ public void setForceReloadCheck(boolean forceReloadCheck)
+ {
+ this.forceReloadCheck = forceReloadCheck;
+ }
+
+ /**
+ * Returns the {@code ExpressionEngine} for converting flat child
+ * configurations to hierarchical ones.
+ *
+ * @return the conversion expression engine
+ * @since 1.6
+ */
+ public ExpressionEngine getConversionExpressionEngine()
+ {
+ return conversionExpressionEngine;
+ }
+
+ /**
+ * Sets the {@code ExpressionEngine} for converting flat child
+ * configurations to hierarchical ones. When constructing the root node for
+ * this combined configuration the properties of all child configurations
+ * must be combined to a single hierarchical node structure. In this
+ * process, non hierarchical configurations are converted to hierarchical
+ * ones first. This can be problematic if a child configuration contains
+ * keys that are no compatible with the default expression engine used by
+ * hierarchical configurations. Therefore it is possible to specify a
+ * specific expression engine to be used for this purpose.
+ *
+ * @param conversionExpressionEngine the conversion expression engine
+ * @see ConfigurationUtils#convertToHierarchical(Configuration, ExpressionEngine)
+ * @since 1.6
+ */
+ public void setConversionExpressionEngine(
+ ExpressionEngine conversionExpressionEngine)
+ {
+ this.conversionExpressionEngine = conversionExpressionEngine;
+ }
+
+ /**
+ * Retrieves the value of the ignoreReloadExceptions flag.
+ * @return true if exceptions are ignored, false otherwise.
+ */
+ public boolean isIgnoreReloadExceptions()
+ {
+ return ignoreReloadExceptions;
+ }
+
+ /**
+ * If set to true then exceptions that occur during reloading will be
+ * ignored. If false then the exceptions will be allowed to be thrown
+ * back to the caller.
+ * @param ignoreReloadExceptions true if exceptions should be ignored.
+ */
+ public void setIgnoreReloadExceptions(boolean ignoreReloadExceptions)
+ {
+ this.ignoreReloadExceptions = ignoreReloadExceptions;
+ }
+
+ /**
+ * Adds a new configuration to this combined configuration. It is possible
+ * (but not mandatory) to give the new configuration a name. This name must
+ * be unique, otherwise a {@code ConfigurationRuntimeException} will
+ * be thrown. With the optional {@code at} argument you can specify
+ * where in the resulting node structure the content of the added
+ * configuration should appear. This is a string that uses dots as property
+ * delimiters (independent on the current expression engine). For instance
+ * if you pass in the string {@code "database.tables"},
+ * all properties of the added configuration will occur in this branch.
+ *
+ * @param config the configuration to add (must not be <b>null</b>)
+ * @param name the name of this configuration (can be <b>null</b>)
+ * @param at the position of this configuration in the combined tree (can be
+ * <b>null</b>)
+ */
+ public void addConfiguration(AbstractConfiguration config, String name,
+ String at)
+ {
+ if (config == null)
+ {
+ throw new IllegalArgumentException(
+ "Added configuration must not be null!");
+ }
+ if (name != null && namedConfigurations.containsKey(name))
+ {
+ throw new ConfigurationRuntimeException(
+ "A configuration with the name '"
+ + name
+ + "' already exists in this combined configuration!");
+ }
+
+ ConfigData cd = new ConfigData(config, name, at);
+ if (getLogger().isDebugEnabled())
+ {
+ getLogger().debug("Adding configuration " + config + " with name " + name);
+ }
+ configurations.add(cd);
+ if (name != null)
+ {
+ namedConfigurations.put(name, config);
+ }
+
+ config.addConfigurationListener(this);
+ invalidate();
+ }
+
+ /**
+ * Adds a new configuration to this combined configuration with an optional
+ * name. The new configuration's properties will be added under the root of
+ * the combined node structure.
+ *
+ * @param config the configuration to add (must not be <b>null</b>)
+ * @param name the name of this configuration (can be <b>null</b>)
+ */
+ public void addConfiguration(AbstractConfiguration config, String name)
+ {
+ addConfiguration(config, name, null);
+ }
+
+ /**
+ * Adds a new configuration to this combined configuration. The new
+ * configuration is not given a name. Its properties will be added under the
+ * root of the combined node structure.
+ *
+ * @param config the configuration to add (must not be <b>null</b>)
+ */
+ public void addConfiguration(AbstractConfiguration config)
+ {
+ addConfiguration(config, null, null);
+ }
+
+ /**
+ * Returns the number of configurations that are contained in this combined
+ * configuration.
+ *
+ * @return the number of contained configurations
+ */
+ public int getNumberOfConfigurations()
+ {
+ return configurations.size();
+ }
+
+ /**
+ * Returns the configuration at the specified index. The contained
+ * configurations are numbered in the order they were added to this combined
+ * configuration. The index of the first configuration is 0.
+ *
+ * @param index the index
+ * @return the configuration at this index
+ */
+ public Configuration getConfiguration(int index)
+ {
+ ConfigData cd = configurations.get(index);
+ return cd.getConfiguration();
+ }
+
+ /**
+ * Returns the configuration with the given name. This can be <b>null</b>
+ * if no such configuration exists.
+ *
+ * @param name the name of the configuration
+ * @return the configuration with this name
+ */
+ public Configuration getConfiguration(String name)
+ {
+ return namedConfigurations.get(name);
+ }
+
+ /**
+ * Returns a List of all the configurations that have been added.
+ * @return A List of all the configurations.
+ * @since 1.7
+ */
+ public List<AbstractConfiguration> getConfigurations()
+ {
+ List<AbstractConfiguration> list = new ArrayList<AbstractConfiguration>(configurations.size());
+ for (ConfigData cd : configurations)
+ {
+ list.add(cd.getConfiguration());
+ }
+ return list;
+ }
+
+ /**
+ * Returns a List of the names of all the configurations that have been
+ * added in the order they were added. A NULL value will be present in
+ * the list for each configuration that was added without a name.
+ * @return A List of all the configuration names.
+ * @since 1.7
+ */
+ public List<String> getConfigurationNameList()
+ {
+ List<String> list = new ArrayList<String>(configurations.size());
+ for (ConfigData cd : configurations)
+ {
+ list.add(cd.getName());
+ }
+ return list;
+ }
+
+ /**
+ * Removes the specified configuration from this combined configuration.
+ *
+ * @param config the configuration to be removed
+ * @return a flag whether this configuration was found and could be removed
+ */
+ public boolean removeConfiguration(Configuration config)
+ {
+ for (int index = 0; index < getNumberOfConfigurations(); index++)
+ {
+ if (configurations.get(index).getConfiguration() == config)
+ {
+ removeConfigurationAt(index);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Removes the configuration at the specified index.
+ *
+ * @param index the index
+ * @return the removed configuration
+ */
+ public Configuration removeConfigurationAt(int index)
+ {
+ ConfigData cd = configurations.remove(index);
+ if (cd.getName() != null)
+ {
+ namedConfigurations.remove(cd.getName());
+ }
+ cd.getConfiguration().removeConfigurationListener(this);
+ invalidate();
+ return cd.getConfiguration();
+ }
+
+ /**
+ * Removes the configuration with the specified name.
+ *
+ * @param name the name of the configuration to be removed
+ * @return the removed configuration (<b>null</b> if this configuration
+ * was not found)
+ */
+ public Configuration removeConfiguration(String name)
+ {
+ Configuration conf = getConfiguration(name);
+ if (conf != null)
+ {
+ removeConfiguration(conf);
+ }
+ return conf;
+ }
+
+ /**
+ * Returns a set with the names of all configurations contained in this
+ * combined configuration. Of course here are only these configurations
+ * listed, for which a name was specified when they were added.
+ *
+ * @return a set with the names of the contained configurations (never
+ * <b>null</b>)
+ */
+ public Set<String> getConfigurationNames()
+ {
+ return namedConfigurations.keySet();
+ }
+
+ /**
+ * Invalidates this combined configuration. This means that the next time a
+ * property is accessed the combined node structure must be re-constructed.
+ * Invalidation of a combined configuration also means that an event of type
+ * {@code EVENT_COMBINED_INVALIDATE} is fired. Note that while other
+ * events most times appear twice (once before and once after an update),
+ * this event is only fired once (after update).
+ */
+ public void invalidate()
+ {
+ reloadRequired = true;
+ fireEvent(EVENT_COMBINED_INVALIDATE, null, null, false);
+ }
+
+ /**
+ * Event listener call back for configuration update events. This method is
+ * called whenever one of the contained configurations was modified. It
+ * invalidates this combined configuration.
+ *
+ * @param event the update event
+ */
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ if (event.getType() == AbstractFileConfiguration.EVENT_CONFIG_CHANGED)
+ {
+ fireEvent(event.getType(), event.getPropertyName(), event.getPropertyValue(), event.isBeforeUpdate());
+ }
+ else if (!event.isBeforeUpdate())
+ {
+ invalidate();
+ }
+ }
+
+ /**
+ * Returns the configuration root node of this combined configuration. This
+ * method will construct a combined node structure using the current node
+ * combiner if necessary.
+ *
+ * @return the combined root node
+ */
+ @Override
+ public ConfigurationNode getRootNode()
+ {
+ synchronized (getReloadLock())
+ {
+ if (reloadRequired || combinedRoot == null)
+ {
+ combinedRoot = constructCombinedNode();
+ reloadRequired = false;
+ }
+ return combinedRoot;
+ }
+ }
+
+ /**
+ * Clears this configuration. All contained configurations will be removed.
+ */
+ @Override
+ public void clear()
+ {
+ fireEvent(EVENT_CLEAR, null, null, true);
+ configurations = new ArrayList<ConfigData>();
+ namedConfigurations = new HashMap<String, AbstractConfiguration>();
+ fireEvent(EVENT_CLEAR, null, null, false);
+ invalidate();
+ }
+
+ /**
+ * Returns a copy of this object. This implementation performs a deep clone,
+ * i.e. all contained configurations will be cloned, too. For this to work,
+ * all contained configurations must be cloneable. Registered event
+ * listeners won't be cloned. The clone will use the same node combiner than
+ * the original.
+ *
+ * @return the copied object
+ */
+ @Override
+ public Object clone()
+ {
+ CombinedConfiguration copy = (CombinedConfiguration) super.clone();
+ copy.clear();
+ for (ConfigData cd : configurations)
+ {
+ copy.addConfiguration((AbstractConfiguration) ConfigurationUtils
+ .cloneConfiguration(cd.getConfiguration()), cd.getName(),
+ cd.getAt());
+ }
+
+ copy.setRootNode(new DefaultConfigurationNode());
+ return copy;
+ }
+
+ /**
+ * Returns the configuration source, in which the specified key is defined.
+ * This method will determine the configuration node that is identified by
+ * the given key. The following constellations are possible:
+ * <ul>
+ * <li>If no node object is found for this key, <b>null</b> is returned.</li>
+ * <li>If the key maps to multiple nodes belonging to different
+ * configuration sources, a {@code IllegalArgumentException} is
+ * thrown (in this case no unique source can be determined).</li>
+ * <li>If exactly one node is found for the key, the (child) configuration
+ * object, to which the node belongs is determined and returned.</li>
+ * <li>For keys that have been added directly to this combined
+ * configuration and that do not belong to the namespaces defined by
+ * existing child configurations this configuration will be returned.</li>
+ * </ul>
+ *
+ * @param key the key of a configuration property
+ * @return the configuration, to which this property belongs or <b>null</b>
+ * if the key cannot be resolved
+ * @throws IllegalArgumentException if the key maps to multiple properties
+ * and the source cannot be determined, or if the key is <b>null</b>
+ * @since 1.5
+ */
+ public Configuration getSource(String key)
+ {
+ if (key == null)
+ {
+ throw new IllegalArgumentException("Key must not be null!");
+ }
+
+ List<ConfigurationNode> nodes = fetchNodeList(key);
+ if (nodes.isEmpty())
+ {
+ return null;
+ }
+
+ Iterator<ConfigurationNode> it = nodes.iterator();
+ Configuration source = findSourceConfiguration(it.next());
+ while (it.hasNext())
+ {
+ Configuration src = findSourceConfiguration(it.next());
+ if (src != source)
+ {
+ throw new IllegalArgumentException("The key " + key
+ + " is defined by multiple sources!");
+ }
+ }
+
+ return source;
+ }
+
+ /**
+ * Evaluates the passed in property key and returns a list with the matching
+ * configuration nodes. This implementation also evaluates the
+ * <em>force reload check</em> flag. If it is set,
+ * {@code performReloadCheck()} is invoked.
+ *
+ * @param key the property key
+ * @return a list with the matching configuration nodes
+ */
+ @Override
+ protected List<ConfigurationNode> fetchNodeList(String key)
+ {
+ if (isForceReloadCheck())
+ {
+ performReloadCheck();
+ }
+
+ return super.fetchNodeList(key);
+ }
+
+ /**
+ * Triggers the contained configurations to perform a reload check if
+ * necessary. This method is called when a property of this combined
+ * configuration is accessed and the {@code forceReloadCheck} property
+ * is set to <b>true</b>.
+ *
+ * @see #setForceReloadCheck(boolean)
+ * @since 1.6
+ */
+ protected void performReloadCheck()
+ {
+ for (ConfigData cd : configurations)
+ {
+ try
+ {
+ // simply retrieve a property; this is enough for
+ // triggering a reload
+ cd.getConfiguration().getProperty(PROP_RELOAD_CHECK);
+ }
+ catch (Exception ex)
+ {
+ if (!ignoreReloadExceptions)
+ {
+ throw new ConfigurationRuntimeException(ex);
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates the root node of this combined configuration.
+ *
+ * @return the combined root node
+ */
+ private ConfigurationNode constructCombinedNode()
+ {
+ if (getNumberOfConfigurations() < 1)
+ {
+ if (getLogger().isDebugEnabled())
+ {
+ getLogger().debug("No configurations defined for " + this);
+ }
+ return new ViewNode();
+ }
+
+ else
+ {
+ Iterator<ConfigData> it = configurations.iterator();
+ ConfigurationNode node = it.next().getTransformedRoot();
+ while (it.hasNext())
+ {
+ node = getNodeCombiner().combine(node,
+ it.next().getTransformedRoot());
+ }
+ if (getLogger().isDebugEnabled())
+ {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ PrintStream stream = new PrintStream(os);
+ TreeUtils.printTree(stream, node);
+ getLogger().debug(os.toString());
+ }
+ return node;
+ }
+ }
+
+ /**
+ * Determines the configuration that owns the specified node.
+ *
+ * @param node the node
+ * @return the owning configuration
+ */
+ private Configuration findSourceConfiguration(ConfigurationNode node)
+ {
+ synchronized (getReloadLock())
+ {
+ ConfigurationNode root = null;
+ ConfigurationNode current = node;
+
+ // find the root node in this hierarchy
+ while (current != null)
+ {
+ root = current;
+ current = current.getParentNode();
+ }
+
+ // Check with the root nodes of the child configurations
+ for (ConfigData cd : configurations)
+ {
+ if (root == cd.getRootNode())
+ {
+ return cd.getConfiguration();
+ }
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * An internal helper class for storing information about contained
+ * configurations.
+ */
+ class ConfigData
+ {
+ /** Stores a reference to the configuration. */
+ private AbstractConfiguration configuration;
+
+ /** Stores the name under which the configuration is stored. */
+ private String name;
+
+ /** Stores the at information as path of nodes. */
+ private Collection<String> atPath;
+
+ /** Stores the at string.*/
+ private String at;
+
+ /** Stores the root node for this child configuration.*/
+ private ConfigurationNode rootNode;
+
+ /**
+ * Creates a new instance of {@code ConfigData} and initializes
+ * it.
+ *
+ * @param config the configuration
+ * @param n the name
+ * @param at the at position
+ */
+ public ConfigData(AbstractConfiguration config, String n, String at)
+ {
+ configuration = config;
+ name = n;
+ atPath = parseAt(at);
+ this.at = at;
+ }
+
+ /**
+ * Returns the stored configuration.
+ *
+ * @return the configuration
+ */
+ public AbstractConfiguration getConfiguration()
+ {
+ return configuration;
+ }
+
+ /**
+ * Returns the configuration's name.
+ *
+ * @return the name
+ */
+ public String getName()
+ {
+ return name;
+ }
+
+ /**
+ * Returns the at position of this configuration.
+ *
+ * @return the at position
+ */
+ public String getAt()
+ {
+ return at;
+ }
+
+ /**
+ * Returns the root node for this child configuration.
+ *
+ * @return the root node of this child configuration
+ * @since 1.5
+ */
+ public ConfigurationNode getRootNode()
+ {
+ return rootNode;
+ }
+
+ /**
+ * Returns the transformed root node of the stored configuration. The
+ * term "transformed" means that an eventually defined at path
+ * has been applied.
+ *
+ * @return the transformed root node
+ */
+ public ConfigurationNode getTransformedRoot()
+ {
+ ViewNode result = new ViewNode();
+ ViewNode atParent = result;
+
+ if (atPath != null)
+ {
+ // Build the complete path
+ for (String p : atPath)
+ {
+ ViewNode node = new ViewNode();
+ node.setName(p);
+ atParent.addChild(node);
+ atParent = node;
+ }
+ }
+
+ // Copy data of the root node to the new path
+ ConfigurationNode root = ConfigurationUtils
+ .convertToHierarchical(getConfiguration(),
+ getConversionExpressionEngine()).getRootNode();
+ atParent.appendChildren(root);
+ atParent.appendAttributes(root);
+ rootNode = root;
+
+ return result;
+ }
+
+ /**
+ * Splits the at path into its components.
+ *
+ * @param at the at string
+ * @return a collection with the names of the single components
+ */
+ private Collection<String> parseAt(String at)
+ {
+ if (at == null)
+ {
+ return null;
+ }
+
+ Collection<String> result = new ArrayList<String>();
+ DefaultConfigurationKey.KeyIterator it = new DefaultConfigurationKey(
+ AT_ENGINE, at).iterator();
+ while (it.hasNext())
+ {
+ result.add(it.nextKey());
+ }
+ return result;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/CompositeConfiguration.java b/src/main/java/org/apache/commons/configuration/CompositeConfiguration.java
new file mode 100644
index 0000000..80aa304
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/CompositeConfiguration.java
@@ -0,0 +1,578 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+/**
+ * <p>{@code CompositeConfiguration} allows you to add multiple {@code Configuration}
+ * objects to an aggregated configuration. If you add Configuration1, and then Configuration2,
+ * any properties shared will mean that the value defined by Configuration1
+ * will be returned. If Configuration1 doesn't have the property, then
+ * Configuration2 will be checked. You can add multiple different types or the
+ * same type of properties file.</p>
+ * <p>When querying properties the order in which child configurations have been
+ * added is relevant. To deal with property updates, a so-called <em>in-memory
+ * configuration</em> is used. Per default, such a configuration is created
+ * automatically. All property writes target this special configuration. There
+ * are constructors which allow you to provide a specific in-memory configuration.
+ * If used that way, the in-memory configuration is always the last one in the
+ * list of child configurations. This means that for query operations all other
+ * configurations take precedence.</p>
+ * <p>Alternatively it is possible to mark a child configuration as in-memory
+ * configuration when it is added. In this case the treatment of the in-memory
+ * configuration is slightly different: it remains in the list of child
+ * configurations at the position it was added, i.e. its priority for property
+ * queries can be defined by adding the child configurations in the correct
+ * order.</p>
+ *
+ * @author <a href="mailto:epugh at upstate.com">Eric Pugh</a>
+ * @author <a href="mailto:hps at intermeta.de">Henning P. Schmiedehausen</a>
+ * @version $Id: CompositeConfiguration.java 1534064 2013-10-21 08:44:33Z henning $
+ */
+public class CompositeConfiguration extends AbstractConfiguration
+implements Cloneable
+{
+ /** List holding all the configuration */
+ private List<Configuration> configList = new LinkedList<Configuration>();
+
+ /**
+ * Configuration that holds in memory stuff. Inserted as first so any
+ * setProperty() override anything else added.
+ */
+ private Configuration inMemoryConfiguration;
+
+ /**
+ * Stores a flag whether the current in-memory configuration is also a
+ * child configuration.
+ */
+ private boolean inMemoryConfigIsChild;
+
+ /**
+ * Creates an empty CompositeConfiguration object which can then
+ * be added some other Configuration files
+ */
+ public CompositeConfiguration()
+ {
+ clear();
+ }
+
+ /**
+ * Creates a CompositeConfiguration object with a specified <em>in-memory
+ * configuration</em>. This configuration will store any changes made to the
+ * {@code CompositeConfiguration}. Note: Use this constructor if you want to
+ * set a special type of in-memory configuration. If you have a
+ * configuration which should act as both a child configuration and as
+ * in-memory configuration, use
+ * {@link #addConfiguration(Configuration, boolean)} with a value of
+ * <b>true</b> instead.
+ *
+ * @param inMemoryConfiguration the in memory configuration to use
+ */
+ public CompositeConfiguration(Configuration inMemoryConfiguration)
+ {
+ configList.clear();
+ this.inMemoryConfiguration = inMemoryConfiguration;
+ configList.add(inMemoryConfiguration);
+ }
+
+ /**
+ * Create a CompositeConfiguration with an empty in memory configuration
+ * and adds the collection of configurations specified.
+ *
+ * @param configurations the collection of configurations to add
+ */
+ public CompositeConfiguration(Collection<? extends Configuration> configurations)
+ {
+ this(new BaseConfiguration(), configurations);
+ }
+
+ /**
+ * Creates a CompositeConfiguration with a specified <em>in-memory
+ * configuration</em>, and then adds the given collection of configurations.
+ *
+ * @param inMemoryConfiguration the in memory configuration to use
+ * @param configurations the collection of configurations to add
+ * @see #CompositeConfiguration(Configuration)
+ */
+ public CompositeConfiguration(Configuration inMemoryConfiguration,
+ Collection<? extends Configuration> configurations)
+ {
+ this(inMemoryConfiguration);
+
+ if (configurations != null)
+ {
+ for (Configuration c : configurations)
+ {
+ addConfiguration(c);
+ }
+ }
+ }
+
+ /**
+ * Add a configuration.
+ *
+ * @param config the configuration to add
+ */
+ public void addConfiguration(Configuration config)
+ {
+ addConfiguration(config, false);
+ }
+
+ /**
+ * Adds a child configuration and optionally makes it the <em>in-memory
+ * configuration</em>. This means that all future property write operations
+ * are executed on this configuration. Note that the current in-memory
+ * configuration is replaced by the new one. If it was created automatically
+ * or passed to the constructor, it is removed from the list of child
+ * configurations! Otherwise, it stays in the list of child configurations
+ * at its current position, but it passes its role as in-memory
+ * configuration to the new one.
+ *
+ * @param config the configuration to be added
+ * @param asInMemory <b>true</b> if this configuration becomes the new
+ * <em>in-memory</em> configuration, <b>false</b> otherwise
+ * @since 1.8
+ */
+ public void addConfiguration(Configuration config, boolean asInMemory)
+ {
+ if (!configList.contains(config))
+ {
+ if (asInMemory)
+ {
+ replaceInMemoryConfiguration(config);
+ inMemoryConfigIsChild = true;
+ }
+
+ if (!inMemoryConfigIsChild)
+ {
+ // As the inMemoryConfiguration contains all manually added
+ // keys, we must make sure that it is always last. "Normal", non
+ // composed configurations add their keys at the end of the
+ // configuration and we want to mimic this behavior.
+ configList.add(configList.indexOf(inMemoryConfiguration),
+ config);
+ }
+ else
+ {
+ // However, if the in-memory configuration is a regular child,
+ // only the order in which child configurations are added is
+ // relevant
+ configList.add(config);
+ }
+
+ if (config instanceof AbstractConfiguration)
+ {
+ ((AbstractConfiguration) config)
+ .setThrowExceptionOnMissing(isThrowExceptionOnMissing());
+ }
+ }
+ }
+
+ /**
+ * Remove a configuration. The in memory configuration cannot be removed.
+ *
+ * @param config The configuration to remove
+ */
+ public void removeConfiguration(Configuration config)
+ {
+ // Make sure that you can't remove the inMemoryConfiguration from
+ // the CompositeConfiguration object
+ if (!config.equals(inMemoryConfiguration))
+ {
+ configList.remove(config);
+ }
+ }
+
+ /**
+ * Return the number of configurations.
+ *
+ * @return the number of configuration
+ */
+ public int getNumberOfConfigurations()
+ {
+ return configList.size();
+ }
+
+ /**
+ * Removes all child configurations and reinitializes the <em>in-memory
+ * configuration</em>. <strong>Attention:</strong> A new in-memory
+ * configuration is created; the old one is lost.
+ */
+ @Override
+ public void clear()
+ {
+ configList.clear();
+ // recreate the in memory configuration
+ inMemoryConfiguration = new BaseConfiguration();
+ ((BaseConfiguration) inMemoryConfiguration).setThrowExceptionOnMissing(isThrowExceptionOnMissing());
+ ((BaseConfiguration) inMemoryConfiguration).setListDelimiter(getListDelimiter());
+ ((BaseConfiguration) inMemoryConfiguration).setDelimiterParsingDisabled(isDelimiterParsingDisabled());
+ configList.add(inMemoryConfiguration);
+ inMemoryConfigIsChild = false;
+ }
+
+ /**
+ * Add this property to the inmemory Configuration.
+ *
+ * @param key The Key to add the property to.
+ * @param token The Value to add.
+ */
+ @Override
+ protected void addPropertyDirect(String key, Object token)
+ {
+ inMemoryConfiguration.addProperty(key, token);
+ }
+
+ /**
+ * Read property from underlying composite
+ *
+ * @param key key to use for mapping
+ *
+ * @return object associated with the given configuration key.
+ */
+ public Object getProperty(String key)
+ {
+ Configuration firstMatchingConfiguration = null;
+ for (Configuration config : configList)
+ {
+ if (config.containsKey(key))
+ {
+ firstMatchingConfiguration = config;
+ break;
+ }
+ }
+
+ if (firstMatchingConfiguration != null)
+ {
+ return firstMatchingConfiguration.getProperty(key);
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ public Iterator<String> getKeys()
+ {
+ Set<String> keys = new LinkedHashSet<String>();
+ for (Configuration config : configList)
+ {
+ for (Iterator<String> it = config.getKeys(); it.hasNext();)
+ {
+ keys.add(it.next());
+ }
+ }
+
+ return keys.iterator();
+ }
+
+ @Override
+ public Iterator<String> getKeys(String key)
+ {
+ Set<String> keys = new LinkedHashSet<String>();
+ for (Configuration config : configList)
+ {
+ for (Iterator<String> it = config.getKeys(key); it.hasNext();)
+ {
+ keys.add(it.next());
+ }
+ }
+
+ return keys.iterator();
+ }
+
+ public boolean isEmpty()
+ {
+ for (Configuration config : configList)
+ {
+ if (!config.isEmpty())
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void clearPropertyDirect(String key)
+ {
+ for (Configuration config : configList)
+ {
+ config.clearProperty(key);
+ }
+ }
+
+ public boolean containsKey(String key)
+ {
+ for (Configuration config : configList)
+ {
+ if (config.containsKey(key))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public List<Object> getList(String key, List<?> defaultValue)
+ {
+ List<Object> list = new ArrayList<Object>();
+
+ // add all elements from the first configuration containing the requested key
+ Iterator<Configuration> it = configList.iterator();
+ while (it.hasNext() && list.isEmpty())
+ {
+ Configuration config = it.next();
+ if (config != inMemoryConfiguration && config.containsKey(key))
+ {
+ appendListProperty(list, config, key);
+ }
+ }
+
+ // add all elements from the in memory configuration
+ appendListProperty(list, inMemoryConfiguration, key);
+
+ if (list.isEmpty())
+ {
+ return (List<Object>) defaultValue;
+ }
+
+ ListIterator<Object> lit = list.listIterator();
+ while (lit.hasNext())
+ {
+ lit.set(interpolate(lit.next()));
+ }
+
+ return list;
+ }
+
+ @Override
+ public String[] getStringArray(String key)
+ {
+ List<Object> list = getList(key);
+
+ // transform property values into strings
+ String[] tokens = new String[list.size()];
+
+ for (int i = 0; i < tokens.length; i++)
+ {
+ tokens[i] = String.valueOf(list.get(i));
+ }
+
+ return tokens;
+ }
+
+ /**
+ * Return the configuration at the specified index.
+ *
+ * @param index The index of the configuration to retrieve
+ * @return the configuration at this index
+ */
+ public Configuration getConfiguration(int index)
+ {
+ return configList.get(index);
+ }
+
+ /**
+ * Returns the "in memory configuration". In this configuration
+ * changes are stored.
+ *
+ * @return the in memory configuration
+ */
+ public Configuration getInMemoryConfiguration()
+ {
+ return inMemoryConfiguration;
+ }
+
+ /**
+ * Returns a copy of this object. This implementation will create a deep
+ * clone, i.e. all configurations contained in this composite will also be
+ * cloned. This only works if all contained configurations support cloning;
+ * otherwise a runtime exception will be thrown. Registered event handlers
+ * won't get cloned.
+ *
+ * @return the copy
+ * @since 1.3
+ */
+ @Override
+ public Object clone()
+ {
+ try
+ {
+ CompositeConfiguration copy = (CompositeConfiguration) super
+ .clone();
+ copy.clearConfigurationListeners();
+ copy.configList = new LinkedList<Configuration>();
+ copy.inMemoryConfiguration = ConfigurationUtils
+ .cloneConfiguration(getInMemoryConfiguration());
+ copy.configList.add(copy.inMemoryConfiguration);
+
+ for (Configuration config : configList)
+ {
+ if (config != getInMemoryConfiguration())
+ {
+ copy.addConfiguration(ConfigurationUtils
+ .cloneConfiguration(config));
+ }
+ }
+
+ return copy;
+ }
+ catch (CloneNotSupportedException cnex)
+ {
+ // cannot happen
+ throw new ConfigurationRuntimeException(cnex);
+ }
+ }
+
+ /**
+ * Sets a flag whether added values for string properties should be checked
+ * for the list delimiter. This implementation ensures that the in memory
+ * configuration is correctly initialized.
+ *
+ * @param delimiterParsingDisabled the new value of the flag
+ * @since 1.4
+ */
+ @Override
+ public void setDelimiterParsingDisabled(boolean delimiterParsingDisabled)
+ {
+ if (inMemoryConfiguration instanceof AbstractConfiguration)
+ {
+ ((AbstractConfiguration) inMemoryConfiguration)
+ .setDelimiterParsingDisabled(delimiterParsingDisabled);
+ }
+ super.setDelimiterParsingDisabled(delimiterParsingDisabled);
+ }
+
+ /**
+ * Sets the character that is used as list delimiter. This implementation
+ * ensures that the in memory configuration is correctly initialized.
+ *
+ * @param listDelimiter the new list delimiter character
+ * @since 1.4
+ */
+ @Override
+ public void setListDelimiter(char listDelimiter)
+ {
+ if (inMemoryConfiguration instanceof AbstractConfiguration)
+ {
+ ((AbstractConfiguration) inMemoryConfiguration)
+ .setListDelimiter(listDelimiter);
+ }
+ super.setListDelimiter(listDelimiter);
+ }
+
+ /**
+ * Returns the configuration source, in which the specified key is defined.
+ * This method will iterate over all existing child configurations and check
+ * whether they contain the specified key. The following constellations are
+ * possible:
+ * <ul>
+ * <li>If exactly one child configuration contains the key, this
+ * configuration is returned as the source configuration. This may be the
+ * <em>in memory configuration</em> (this has to be explicitly checked by
+ * the calling application).</li>
+ * <li>If none of the child configurations contain the key, <b>null</b> is
+ * returned.</li>
+ * <li>If the key is contained in multiple child configurations or if the
+ * key is <b>null</b>, a {@code IllegalArgumentException} is thrown.
+ * In this case the source configuration cannot be determined.</li>
+ * </ul>
+ *
+ * @param key the key to be checked
+ * @return the source configuration of this key
+ * @throws IllegalArgumentException if the source configuration cannot be
+ * determined
+ * @since 1.5
+ */
+ public Configuration getSource(String key)
+ {
+ if (key == null)
+ {
+ throw new IllegalArgumentException("Key must not be null!");
+ }
+
+ Configuration source = null;
+ for (Configuration conf : configList)
+ {
+ if (conf.containsKey(key))
+ {
+ if (source != null)
+ {
+ throw new IllegalArgumentException("The key " + key
+ + " is defined by multiple sources!");
+ }
+ source = conf;
+ }
+ }
+
+ return source;
+ }
+
+ /**
+ * Replaces the current in-memory configuration by the given one.
+ *
+ * @param config the new in-memory configuration
+ */
+ private void replaceInMemoryConfiguration(Configuration config)
+ {
+ if (!inMemoryConfigIsChild)
+ {
+ // remove current in-memory configuration
+ configList.remove(inMemoryConfiguration);
+ }
+ inMemoryConfiguration = config;
+ }
+
+ /**
+ * Adds the value of a property to the given list. This method is used by
+ * {@code getList()} for gathering property values from the child
+ * configurations.
+ *
+ * @param dest the list for collecting the data
+ * @param config the configuration to query
+ * @param key the key of the property
+ */
+ private static void appendListProperty(List<Object> dest, Configuration config,
+ String key)
+ {
+ Object value = config.getProperty(key);
+ if (value != null)
+ {
+ if (value instanceof Collection)
+ {
+ Collection<?> col = (Collection<?>) value;
+ dest.addAll(col);
+ }
+ else
+ {
+ dest.add(value);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/Configuration.java b/src/main/java/org/apache/commons/configuration/Configuration.java
new file mode 100644
index 0000000..c7d8de0
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/Configuration.java
@@ -0,0 +1,598 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * <p>The main Configuration interface.</p>
+ * <p>This interface allows accessing and manipulating a configuration object.
+ * The major part of the methods defined in this interface deals with accessing
+ * properties of various data types. There is a generic {@code getProperty()}
+ * method, which returns the value of the queried property in its raw data
+ * type. Other getter methods try to convert this raw data type into a specific
+ * data type. If this fails, a {@code ConversionException} will be thrown.</p>
+ * <p>For most of the property getter methods an overloaded version exists that
+ * allows to specify a default value, which will be returned if the queried
+ * property cannot be found in the configuration. The behavior of the methods
+ * that do not take a default value in case of a missing property is not defined
+ * by this interface and depends on a concrete implementation. E.g. the
+ * {@link AbstractConfiguration} class, which is the base class
+ * of most configuration implementations provided by this package, per default
+ * returns <b>null</b> if a property is not found, but provides the
+ * {@link AbstractConfiguration#setThrowExceptionOnMissing(boolean)
+ * setThrowExceptionOnMissing()}
+ * method, with which it can be configured to throw a {@code NoSuchElementException}
+ * exception in that case. (Note that getter methods for primitive types in
+ * {@code AbstractConfiguration} always throw an exception for missing
+ * properties because there is no way of overloading the return value.)</p>
+ * <p>With the {@code addProperty()} and {@code setProperty()} methods
+ * new properties can be added to a configuration or the values of properties
+ * can be changed. With {@code clearProperty()} a property can be removed.
+ * Other methods allow to iterate over the contained properties or to create
+ * a subset configuration.</p>
+ *
+ * @author Commons Configuration team
+ * @version $Id: Configuration.java 1534064 2013-10-21 08:44:33Z henning $
+ */
+public interface Configuration
+{
+ /**
+ * Return a decorator Configuration containing every key from the current
+ * Configuration that starts with the specified prefix. The prefix is
+ * removed from the keys in the subset. For example, if the configuration
+ * contains the following properties:
+ *
+ * <pre>
+ * prefix.number = 1
+ * prefix.string = Apache
+ * prefixed.foo = bar
+ * prefix = Jakarta</pre>
+ *
+ * the Configuration returned by {@code subset("prefix")} will contain
+ * the properties:
+ *
+ * <pre>
+ * number = 1
+ * string = Apache
+ * = Jakarta</pre>
+ *
+ * (The key for the value "Jakarta" is an empty string)
+ * <p>
+ * Since the subset is a decorator and not a modified copy of the initial
+ * Configuration, any change made to the subset is available to the
+ * Configuration, and reciprocally.
+ *
+ * @param prefix The prefix used to select the properties.
+ * @return a subset configuration
+ *
+ * @see SubsetConfiguration
+ */
+ Configuration subset(String prefix);
+
+ /**
+ * Check if the configuration is empty.
+ *
+ * @return {@code true} if the configuration contains no property,
+ * {@code false} otherwise.
+ */
+ boolean isEmpty();
+
+ /**
+ * Check if the configuration contains the specified key.
+ *
+ * @param key the key whose presence in this configuration is to be tested
+ *
+ * @return {@code true} if the configuration contains a value for this
+ * key, {@code false} otherwise
+ */
+ boolean containsKey(String key);
+
+ /**
+ * Add a property to the configuration. If it already exists then the value
+ * stated here will be added to the configuration entry. For example, if
+ * the property:
+ *
+ * <pre>resource.loader = file</pre>
+ *
+ * is already present in the configuration and you call
+ *
+ * <pre>addProperty("resource.loader", "classpath")</pre>
+ *
+ * Then you will end up with a List like the following:
+ *
+ * <pre>["file", "classpath"]</pre>
+ *
+ * @param key The key to add the property to.
+ * @param value The value to add.
+ */
+ void addProperty(String key, Object value);
+
+ /**
+ * Set a property, this will replace any previously set values. Set values
+ * is implicitly a call to clearProperty(key), addProperty(key, value).
+ *
+ * @param key The key of the property to change
+ * @param value The new value
+ */
+ void setProperty(String key, Object value);
+
+ /**
+ * Remove a property from the configuration.
+ *
+ * @param key the key to remove along with corresponding value.
+ */
+ void clearProperty(String key);
+
+ /**
+ * Remove all properties from the configuration.
+ */
+ void clear();
+
+ /**
+ * Gets a property from the configuration. This is the most basic get
+ * method for retrieving values of properties. In a typical implementation
+ * of the {@code Configuration} interface the other get methods (that
+ * return specific data types) will internally make use of this method. On
+ * this level variable substitution is not yet performed. The returned
+ * object is an internal representation of the property value for the passed
+ * in key. It is owned by the {@code Configuration} object. So a caller
+ * should not modify this object. It cannot be guaranteed that this object
+ * will stay constant over time (i.e. further update operations on the
+ * configuration may change its internal state).
+ *
+ * @param key property to retrieve
+ * @return the value to which this configuration maps the specified key, or
+ * null if the configuration contains no mapping for this key.
+ */
+ Object getProperty(String key);
+
+ /**
+ * Get the list of the keys contained in the configuration that match the
+ * specified prefix. For instance, if the configuration contains the
+ * following keys:<br>
+ * {@code db.user, db.pwd, db.url, window.xpos, window.ypos},<br>
+ * an invocation of {@code getKeys("db");}<br>
+ * will return the keys below:<br>
+ * {@code db.user, db.pwd, db.url}.<br>
+ * Note that the prefix itself is included in the result set if there is a
+ * matching key. The exact behavior - how the prefix is actually
+ * interpreted - depends on a concrete implementation.
+ *
+ * @param prefix The prefix to test against.
+ * @return An Iterator of keys that match the prefix.
+ * @see #getKeys()
+ */
+ Iterator<String> getKeys(String prefix);
+
+ /**
+ * Get the list of the keys contained in the configuration. The returned
+ * iterator can be used to obtain all defined keys. Note that the exact
+ * behavior of the iterator's {@code remove()} method is specific to
+ * a concrete implementation. It <em>may</em> remove the corresponding
+ * property from the configuration, but this is not guaranteed. In any case
+ * it is no replacement for calling
+ * {@link #clearProperty(String)} for this property. So it is
+ * highly recommended to avoid using the iterator's {@code remove()}
+ * method.
+ *
+ * @return An Iterator.
+ */
+ Iterator<String> getKeys();
+
+ /**
+ * Get a list of properties associated with the given configuration key.
+ * This method expects the given key to have an arbitrary number of String
+ * values, each of which is of the form {code key=value}. These
+ * strings are split at the equals sign, and the key parts will become
+ * keys of the returned {@code Properties} object, the value parts
+ * become values.
+ *
+ * @param key The configuration key.
+ * @return The associated properties if key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a String/List.
+ *
+ * @throws IllegalArgumentException if one of the tokens is
+ * malformed (does not contain an equals sign).
+ */
+ Properties getProperties(String key);
+
+ /**
+ * Get a boolean associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @return The associated boolean.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Boolean.
+ */
+ boolean getBoolean(String key);
+
+ /**
+ * Get a boolean associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated boolean.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Boolean.
+ */
+ boolean getBoolean(String key, boolean defaultValue);
+
+ /**
+ * Get a {@link Boolean} associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated boolean if key is found and has valid
+ * format, default value otherwise.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Boolean.
+ */
+ Boolean getBoolean(String key, Boolean defaultValue);
+
+ /**
+ * Get a byte associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @return The associated byte.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Byte.
+ */
+ byte getByte(String key);
+
+ /**
+ * Get a byte associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated byte.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Byte.
+ */
+ byte getByte(String key, byte defaultValue);
+
+ /**
+ * Get a {@link Byte} associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated byte if key is found and has valid format, default
+ * value otherwise.
+ *
+ * @throws ConversionException is thrown if the key maps to an object that
+ * is not a Byte.
+ */
+ Byte getByte(String key, Byte defaultValue);
+
+ /**
+ * Get a double associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @return The associated double.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Double.
+ */
+ double getDouble(String key);
+
+ /**
+ * Get a double associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated double.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Double.
+ */
+ double getDouble(String key, double defaultValue);
+
+ /**
+ * Get a {@link Double} associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated double if key is found and has valid
+ * format, default value otherwise.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Double.
+ */
+ Double getDouble(String key, Double defaultValue);
+
+ /**
+ * Get a float associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @return The associated float.
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Float.
+ */
+ float getFloat(String key);
+
+ /**
+ * Get a float associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated float.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Float.
+ */
+ float getFloat(String key, float defaultValue);
+
+ /**
+ * Get a {@link Float} associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated float if key is found and has valid
+ * format, default value otherwise.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Float.
+ */
+ Float getFloat(String key, Float defaultValue);
+
+ /**
+ * Get a int associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @return The associated int.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Integer.
+ */
+ int getInt(String key);
+
+ /**
+ * Get a int associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated int.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Integer.
+ */
+ int getInt(String key, int defaultValue);
+
+ /**
+ * Get an {@link Integer} associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated int if key is found and has valid format, default
+ * value otherwise.
+ *
+ * @throws ConversionException is thrown if the key maps to an object that
+ * is not a Integer.
+ */
+ Integer getInteger(String key, Integer defaultValue);
+
+ /**
+ * Get a long associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @return The associated long.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Long.
+ */
+ long getLong(String key);
+
+ /**
+ * Get a long associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated long.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Long.
+ */
+ long getLong(String key, long defaultValue);
+
+ /**
+ * Get a {@link Long} associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated long if key is found and has valid
+ * format, default value otherwise.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Long.
+ */
+ Long getLong(String key, Long defaultValue);
+
+ /**
+ * Get a short associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @return The associated short.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Short.
+ */
+ short getShort(String key);
+
+ /**
+ * Get a short associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated short.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Short.
+ */
+ short getShort(String key, short defaultValue);
+
+ /**
+ * Get a {@link Short} associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated short if key is found and has valid
+ * format, default value otherwise.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Short.
+ */
+ Short getShort(String key, Short defaultValue);
+
+ /**
+ * Get a {@link BigDecimal} associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @return The associated BigDecimal if key is found and has valid format
+ */
+ BigDecimal getBigDecimal(String key);
+
+ /**
+ * Get a {@link BigDecimal} associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ *
+ * @return The associated BigDecimal if key is found and has valid
+ * format, default value otherwise.
+ */
+ BigDecimal getBigDecimal(String key, BigDecimal defaultValue);
+
+ /**
+ * Get a {@link BigInteger} associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ *
+ * @return The associated BigInteger if key is found and has valid format
+ */
+ BigInteger getBigInteger(String key);
+
+ /**
+ * Get a {@link BigInteger} associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ *
+ * @return The associated BigInteger if key is found and has valid
+ * format, default value otherwise.
+ */
+ BigInteger getBigInteger(String key, BigInteger defaultValue);
+
+ /**
+ * Get a string associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @return The associated string.
+ *
+ * @throws ConversionException is thrown if the key maps to an object that
+ * is not a String.
+ */
+ String getString(String key);
+
+ /**
+ * Get a string associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated string if key is found and has valid
+ * format, default value otherwise.
+ *
+ * @throws ConversionException is thrown if the key maps to an object that
+ * is not a String.
+ */
+ String getString(String key, String defaultValue);
+
+ /**
+ * Get an array of strings associated with the given configuration key.
+ * If the key doesn't map to an existing object an empty array is returned
+ *
+ * @param key The configuration key.
+ * @return The associated string array if key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a String/List of Strings.
+ */
+ String[] getStringArray(String key);
+
+ /**
+ * Get a List of strings associated with the given configuration key.
+ * If the key doesn't map to an existing object an empty List is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated List.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a List.
+ */
+ List<Object> getList(String key);
+
+ /**
+ * Get a List of strings associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated List of strings.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a List.
+ */
+ List<Object> getList(String key, List<?> defaultValue);
+}
diff --git a/src/main/java/org/apache/commons/configuration/ConfigurationBuilder.java b/src/main/java/org/apache/commons/configuration/ConfigurationBuilder.java
new file mode 100644
index 0000000..1a83b43
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/ConfigurationBuilder.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+/**
+ * <p>
+ * Definition of an interface for objects that can create a configuration.
+ * </p>
+ * <p>
+ * This interface defines an abstract way of creating a
+ * {@code Configuration} object. It does not assume any specific way of
+ * how this is done; this is completely in the responsibility of an
+ * implementation class. There is just a single method that returns the
+ * configuration constructed by this builder.
+ * </p>
+ *
+ * @author <a href="http://commons.apache.org/configuration/team-list.html">Commons Configuration team</a>
+ * @version $Id: ConfigurationBuilder.java 1208781 2011-11-30 21:11:02Z oheger $
+ */
+public interface ConfigurationBuilder
+{
+ /**
+ * Returns the configuration provided by this builder. An implementation has
+ * to perform all necessary steps for creating and initializing a
+ * {@code Configuration} object.
+ *
+ * @return the configuration
+ * @throws ConfigurationException if an error occurs
+ */
+ Configuration getConfiguration() throws ConfigurationException;
+}
diff --git a/src/main/java/org/apache/commons/configuration/ConfigurationComparator.java b/src/main/java/org/apache/commons/configuration/ConfigurationComparator.java
new file mode 100644
index 0000000..299b8e8
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/ConfigurationComparator.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+/**
+ * Comparator for configurations interface.
+ *
+ * @since 1.0
+ *
+ * @author <a href="mailto:herve.quiroz at esil.univ-mrs.fr">Herve Quiroz</a>
+ * @version $Id: ConfigurationComparator.java 1208783 2011-11-30 21:12:45Z oheger $
+ */
+public interface ConfigurationComparator
+{
+ /**
+ * Compare two configuration objects.
+ *
+ * @param a the first configuration
+ * @param b the second configuration
+ * @return true if the two configurations are identical according to
+ * the implemented rules
+ */
+ boolean compare(Configuration a, Configuration b);
+}
+
diff --git a/src/main/java/org/apache/commons/configuration/ConfigurationConverter.java b/src/main/java/org/apache/commons/configuration/ConfigurationConverter.java
new file mode 100644
index 0000000..8db7b23
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/ConfigurationConverter.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.commons.collections.ExtendedProperties;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Configuration converter. Helper class to convert between Configuration,
+ * ExtendedProperties and standard Properties.
+ *
+ * @author <a href="mailto:mpoeschl at marmot.at">Martin Poeschl</a>
+ * @version $Id: ConfigurationConverter.java 1208788 2011-11-30 21:15:37Z oheger $
+ */
+public final class ConfigurationConverter
+{
+ /**
+ * Private constructor prevents instances from being created.
+ */
+ private ConfigurationConverter()
+ {
+ // to prevent instanciation...
+ }
+
+ /**
+ * Convert a ExtendedProperties class into a Configuration class.
+ *
+ * @param eprops ExtendedProperties object to convert
+ * @return Configuration created from the ExtendedProperties
+ */
+ public static Configuration getConfiguration(ExtendedProperties eprops)
+ {
+ return new MapConfiguration(eprops);
+ }
+
+ /**
+ * Convert a standard Properties class into a configuration class.
+ *
+ * @param props properties object to convert
+ * @return Configuration configuration created from the Properties
+ */
+ public static Configuration getConfiguration(Properties props)
+ {
+ return new MapConfiguration(props);
+ }
+
+ /**
+ * Convert a Configuration class into a ExtendedProperties class.
+ *
+ * @param config Configuration object to convert
+ * @return ExtendedProperties created from the Configuration
+ */
+ public static ExtendedProperties getExtendedProperties(Configuration config)
+ {
+ ExtendedProperties props = new ExtendedProperties();
+
+ for (Iterator<String> keys = config.getKeys(); keys.hasNext();)
+ {
+ String key = keys.next();
+ Object property = config.getProperty(key);
+
+ // turn lists into vectors
+ if (property instanceof List)
+ {
+ property = new ArrayList<Object>((List<?>) property);
+ }
+
+ props.setProperty(key, property);
+ }
+
+ return props;
+ }
+
+ /**
+ * Convert a Configuration class into a Properties class. List properties
+ * are joined into a string using the delimiter of the configuration if it
+ * extends AbstractConfiguration, and a comma otherwise.
+ *
+ * @param config Configuration object to convert
+ * @return Properties created from the Configuration
+ */
+ public static Properties getProperties(Configuration config)
+ {
+ Properties props = new Properties();
+
+ char delimiter = (config instanceof AbstractConfiguration)
+ ? ((AbstractConfiguration) config).getListDelimiter() : ',';
+
+ for (Iterator<String> keys = config.getKeys(); keys.hasNext();)
+ {
+ String key = keys.next();
+ List<Object> list = config.getList(key);
+
+ // turn the list into a string
+ props.setProperty(key, StringUtils.join(list.iterator(), delimiter));
+ }
+
+ return props;
+ }
+
+ /**
+ * Convert a Configuration class into a Map class.
+ *
+ * @param config Configuration object to convert
+ * @return Map created from the Configuration
+ */
+ public static Map<Object, Object> getMap(Configuration config)
+ {
+ return new ConfigurationMap(config);
+ }
+
+}
diff --git a/src/main/java/org/apache/commons/configuration/ConfigurationException.java b/src/main/java/org/apache/commons/configuration/ConfigurationException.java
new file mode 100644
index 0000000..d852b02
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/ConfigurationException.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import org.apache.commons.lang.exception.NestableException;
+
+/**
+ * Any exception that occurs while initializing a Configuration
+ * object.
+ *
+ * @author Eric Pugh
+ * @version $Id: ConfigurationException.java 1208784 2011-11-30 21:13:18Z oheger $
+ */
+public class ConfigurationException extends NestableException
+{
+ /**
+ * The serial version ID.
+ */
+ private static final long serialVersionUID = -1316746661346991484L;
+
+ /**
+ * Constructs a new {@code ConfigurationException} without specified
+ * detail message.
+ */
+ public ConfigurationException()
+ {
+ super();
+ }
+
+ /**
+ * Constructs a new {@code ConfigurationException} with specified
+ * detail message.
+ *
+ * @param message the error message
+ */
+ public ConfigurationException(String message)
+ {
+ super(message);
+ }
+
+ /**
+ * Constructs a new {@code ConfigurationException} with specified
+ * nested {@code Throwable}.
+ *
+ * @param cause the exception or error that caused this exception to be thrown
+ */
+ public ConfigurationException(Throwable cause)
+ {
+ super(cause);
+ }
+
+ /**
+ * Constructs a new {@code ConfigurationException} with specified
+ * detail message and nested {@code Throwable}.
+ *
+ * @param message the error message
+ * @param cause the exception or error that caused this exception to be thrown
+ */
+ public ConfigurationException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/ConfigurationFactory.java b/src/main/java/org/apache/commons/configuration/ConfigurationFactory.java
new file mode 100644
index 0000000..0016810
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/ConfigurationFactory.java
@@ -0,0 +1,903 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.Map;
+
+import org.apache.commons.configuration.plist.PropertyListConfiguration;
+import org.apache.commons.configuration.plist.XMLPropertyListConfiguration;
+import org.apache.commons.digester.AbstractObjectCreationFactory;
+import org.apache.commons.digester.CallMethodRule;
+import org.apache.commons.digester.Digester;
+import org.apache.commons.digester.ObjectCreationFactory;
+import org.apache.commons.digester.Substitutor;
+import org.apache.commons.digester.substitution.MultiVariableExpander;
+import org.apache.commons.digester.substitution.VariableSubstitutor;
+import org.apache.commons.digester.xmlrules.DigesterLoader;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+/**
+ * <p>
+ * Factory class to create a CompositeConfiguration from a .xml file using
+ * Digester. By default it can handle the Configurations from commons-
+ * configuration. If you need to add your own, then you can pass in your own
+ * digester rules to use. It is also namespace aware, by providing a
+ * digesterRuleNamespaceURI.
+ * </p>
+ * <p>
+ * <em>Note:</em> Almost all of the features provided by this class and many
+ * more are also available for the {@link DefaultConfigurationBuilder}
+ * class. {@code DefaultConfigurationBuilder} also has a more robust
+ * merge algorithm for constructing combined configurations. So it is
+ * recommended to use this class instead of {@code ConfigurationFactory}.
+ * </p>
+ *
+ * @author <a href="mailto:epugh at upstate.com">Eric Pugh</a>
+ * @author <a href="mailto:hps at intermeta.de">Henning P. Schmiedehausen</a>
+ * @version $Id: ConfigurationFactory.java 1209685 2011-12-02 20:47:44Z oheger $
+ * @deprecated Use {@link DefaultConfigurationBuilder} instead; this class
+ * provides the same features as ConfigurationFactory plus some more; it can
+ * also process the same configuration definition files.
+ */
+ at Deprecated
+public class ConfigurationFactory
+{
+ /** Constant for the root element in the info file.*/
+ private static final String SEC_ROOT = "configuration/";
+
+ /** Constant for the override section.*/
+ private static final String SEC_OVERRIDE = SEC_ROOT + "override/";
+
+ /** Constant for the additional section.*/
+ private static final String SEC_ADDITIONAL = SEC_ROOT + "additional/";
+
+ /** Constant for the optional attribute.*/
+ private static final String ATTR_OPTIONAL = "optional";
+
+ /** Constant for the fileName attribute.*/
+ private static final String ATTR_FILENAME = "fileName";
+
+ /** Constant for the load method.*/
+ private static final String METH_LOAD = "load";
+
+ /** Constant for the default base path (points to actual directory).*/
+ private static final String DEF_BASE_PATH = ".";
+
+ /** static logger */
+ private static Log log = LogFactory.getLog(ConfigurationFactory.class);
+
+ /** The XML file with the details about the configuration to load */
+ private String configurationFileName;
+
+ /** The URL to the XML file with the details about the configuration to load. */
+ private URL configurationURL;
+
+ /**
+ * The implicit base path for included files. This path is determined by
+ * the configuration to load and used unless no other base path was
+ * explicitly specified.
+ */
+ private String implicitBasePath;
+
+ /** The basePath to prefix file paths for file based property files. */
+ private String basePath;
+
+ /** URL for xml digester rules file */
+ private URL digesterRules;
+
+ /** The digester namespace to parse */
+ private String digesterRuleNamespaceURI;
+
+ /**
+ * Constructor
+ */
+ public ConfigurationFactory()
+ {
+ setBasePath(DEF_BASE_PATH);
+ }
+ /**
+ * Constructor with ConfigurationFile Name passed
+ *
+ * @param configurationFileName The path to the configuration file
+ */
+ public ConfigurationFactory(String configurationFileName)
+ {
+ setConfigurationFileName(configurationFileName);
+ }
+
+ /**
+ * Return the configuration provided by this factory. It loads the
+ * configuration file which is a XML description of the actual
+ * configurations to load. It can contain various different types of
+ * configuration, e.g. Properties, XML and JNDI.
+ *
+ * @return A Configuration object
+ * @throws ConfigurationException A generic exception that we had trouble during the
+ * loading of the configuration data.
+ */
+ public Configuration getConfiguration() throws ConfigurationException
+ {
+ Digester digester;
+ InputStream input = null;
+ ConfigurationBuilder builder = new ConfigurationBuilder();
+ URL url = getConfigurationURL();
+ try
+ {
+ if (url == null)
+ {
+ url = ConfigurationUtils.locate(implicitBasePath, getConfigurationFileName());
+ }
+ input = url.openStream();
+ }
+ catch (Exception e)
+ {
+ log.error("Exception caught opening stream to URL", e);
+ throw new ConfigurationException("Exception caught opening stream to URL", e);
+ }
+
+ if (getDigesterRules() == null)
+ {
+ digester = new Digester();
+ configureNamespace(digester);
+ initDefaultDigesterRules(digester);
+ }
+ else
+ {
+ digester = DigesterLoader.createDigester(getDigesterRules());
+ // This might already be too late. As far as I can see, the namespace
+ // awareness must be configured before the digester rules are loaded.
+ configureNamespace(digester);
+ }
+
+ // Configure digester to always enable the context class loader
+ digester.setUseContextClassLoader(true);
+ // Add a substitutor to resolve system properties
+ enableDigesterSubstitutor(digester);
+ // Put the composite builder object below all of the other objects.
+ digester.push(builder);
+ // Parse the input stream to configure our mappings
+ try
+ {
+ digester.parse(input);
+ input.close();
+ }
+ catch (SAXException saxe)
+ {
+ log.error("SAX Exception caught", saxe);
+ throw new ConfigurationException("SAX Exception caught", saxe);
+ }
+ catch (IOException ioe)
+ {
+ log.error("IO Exception caught", ioe);
+ throw new ConfigurationException("IO Exception caught", ioe);
+ }
+ return builder.getConfiguration();
+ }
+
+ /**
+ * Returns the configurationFile.
+ *
+ * @return The name of the configuration file. Can be null.
+ */
+ public String getConfigurationFileName()
+ {
+ return configurationFileName;
+ }
+
+ /**
+ * Sets the configurationFile.
+ *
+ * @param configurationFileName The name of the configurationFile to use.
+ */
+ public void setConfigurationFileName(String configurationFileName)
+ {
+ File file = new File(configurationFileName).getAbsoluteFile();
+ this.configurationFileName = file.getName();
+ implicitBasePath = file.getParent();
+ }
+
+ /**
+ * Returns the URL of the configuration file to be loaded.
+ *
+ * @return the URL of the configuration to load
+ */
+ public URL getConfigurationURL()
+ {
+ return configurationURL;
+ }
+
+ /**
+ * Sets the URL of the configuration to load. This configuration can be
+ * either specified by a file name or by a URL.
+ *
+ * @param url the URL of the configuration to load
+ */
+ public void setConfigurationURL(URL url)
+ {
+ configurationURL = url;
+ implicitBasePath = url.toString();
+ }
+
+ /**
+ * Returns the digesterRules.
+ *
+ * @return URL
+ */
+ public URL getDigesterRules()
+ {
+ return digesterRules;
+ }
+
+ /**
+ * Sets the digesterRules.
+ *
+ * @param digesterRules The digesterRules to set
+ */
+ public void setDigesterRules(URL digesterRules)
+ {
+ this.digesterRules = digesterRules;
+ }
+
+ /**
+ * Adds a substitutor to interpolate system properties
+ *
+ * @param digester The digester to which we add the substitutor
+ */
+ protected void enableDigesterSubstitutor(Digester digester)
+ {
+ // This is ugly, but it is safe because the Properties object returned
+ // by System.getProperties() (which is actually a Map<Object, Object>)
+ // contains only String keys.
+ @SuppressWarnings("unchecked")
+ Map<String, Object> systemProperties =
+ (Map<String, Object>) (Object) System.getProperties();
+ MultiVariableExpander expander = new MultiVariableExpander();
+ expander.addSource("$", systemProperties);
+
+ // allow expansion in both xml attributes and element text
+ Substitutor substitutor = new VariableSubstitutor(expander);
+ digester.setSubstitutor(substitutor);
+ }
+
+ /**
+ * Initializes the parsing rules for the default digester
+ *
+ * This allows the Configuration Factory to understand the default types:
+ * Properties, XML and JNDI. Two special sections are introduced:
+ * <code><override></code> and <code><additional></code>.
+ *
+ * @param digester The digester to configure
+ */
+ protected void initDefaultDigesterRules(Digester digester)
+ {
+ initDigesterSectionRules(digester, SEC_ROOT, false);
+ initDigesterSectionRules(digester, SEC_OVERRIDE, false);
+ initDigesterSectionRules(digester, SEC_ADDITIONAL, true);
+ }
+
+ /**
+ * Sets up digester rules for a specified section of the configuration
+ * info file.
+ *
+ * @param digester the current digester instance
+ * @param matchString specifies the section
+ * @param additional a flag if rules for the additional section are to be
+ * added
+ */
+ protected void initDigesterSectionRules(Digester digester, String matchString, boolean additional)
+ {
+ setupDigesterInstance(
+ digester,
+ matchString + "properties",
+ new PropertiesConfigurationFactory(),
+ METH_LOAD,
+ additional);
+
+ setupDigesterInstance(
+ digester,
+ matchString + "plist",
+ new PropertyListConfigurationFactory(),
+ METH_LOAD,
+ additional);
+
+ setupDigesterInstance(
+ digester,
+ matchString + "xml",
+ new FileConfigurationFactory(XMLConfiguration.class),
+ METH_LOAD,
+ additional);
+
+ setupDigesterInstance(
+ digester,
+ matchString + "hierarchicalXml",
+ new FileConfigurationFactory(XMLConfiguration.class),
+ METH_LOAD,
+ additional);
+
+ setupDigesterInstance(
+ digester,
+ matchString + "jndi",
+ new JNDIConfigurationFactory(),
+ null,
+ additional);
+
+ setupDigesterInstance(
+ digester,
+ matchString + "system",
+ new SystemConfigurationFactory(),
+ null,
+ additional);
+ }
+
+ /**
+ * Sets up digester rules for a configuration to be loaded.
+ *
+ * @param digester the current digester
+ * @param matchString the pattern to match with this rule
+ * @param factory an ObjectCreationFactory instance to use for creating new
+ * objects
+ * @param method the name of a method to be called or <b>null</b> for none
+ * @param additional a flag if rules for the additional section are to be
+ * added
+ */
+ protected void setupDigesterInstance(
+ Digester digester,
+ String matchString,
+ ObjectCreationFactory factory,
+ String method,
+ boolean additional)
+ {
+ if (additional)
+ {
+ setupUnionRules(digester, matchString);
+ }
+
+ digester.addFactoryCreate(matchString, factory);
+ digester.addSetProperties(matchString);
+
+ if (method != null)
+ {
+ digester.addRule(matchString, new CallOptionalMethodRule(method));
+ }
+
+ digester.addSetNext(matchString, "addConfiguration", Configuration.class.getName());
+ }
+
+ /**
+ * Sets up rules for configurations in the additional section.
+ *
+ * @param digester the current digester
+ * @param matchString the pattern to match with this rule
+ */
+ protected void setupUnionRules(Digester digester, String matchString)
+ {
+ digester.addObjectCreate(matchString,
+ AdditionalConfigurationData.class);
+ digester.addSetProperties(matchString);
+ digester.addSetNext(matchString, "addAdditionalConfig",
+ AdditionalConfigurationData.class.getName());
+ }
+
+ /**
+ * Returns the digesterRuleNamespaceURI.
+ *
+ * @return A String with the digesterRuleNamespaceURI.
+ */
+ public String getDigesterRuleNamespaceURI()
+ {
+ return digesterRuleNamespaceURI;
+ }
+
+ /**
+ * Sets the digesterRuleNamespaceURI.
+ *
+ * @param digesterRuleNamespaceURI The new digesterRuleNamespaceURI to use
+ */
+ public void setDigesterRuleNamespaceURI(String digesterRuleNamespaceURI)
+ {
+ this.digesterRuleNamespaceURI = digesterRuleNamespaceURI;
+ }
+
+ /**
+ * Configure the current digester to be namespace aware and to have
+ * a Configuration object to which all of the other configurations
+ * should be added
+ *
+ * @param digester The Digester to configure
+ */
+ private void configureNamespace(Digester digester)
+ {
+ if (getDigesterRuleNamespaceURI() != null)
+ {
+ digester.setNamespaceAware(true);
+ digester.setRuleNamespaceURI(getDigesterRuleNamespaceURI());
+ }
+ else
+ {
+ digester.setNamespaceAware(false);
+ }
+ digester.setValidating(false);
+ }
+
+ /**
+ * Returns the Base path from which this Configuration Factory operates.
+ * This is never null. If you set the BasePath to null, then a base path
+ * according to the configuration to load is returned.
+ *
+ * @return The base Path of this configuration factory.
+ */
+ public String getBasePath()
+ {
+ String path = StringUtils.isEmpty(basePath)
+ || DEF_BASE_PATH.equals(basePath) ? implicitBasePath : basePath;
+ return StringUtils.isEmpty(path) ? DEF_BASE_PATH : path;
+ }
+
+ /**
+ * Sets the basePath for all file references from this Configuration Factory.
+ * Normally a base path need not to be set because it is determined by
+ * the location of the configuration file to load. All relative pathes in
+ * this file are resolved relative to this file. Setting a base path makes
+ * sense if such relative pathes should be otherwise resolved, e.g. if
+ * the configuration file is loaded from the class path and all sub
+ * configurations it refers to are stored in a special config directory.
+ *
+ * @param basePath The new basePath to set.
+ */
+ public void setBasePath(String basePath)
+ {
+ this.basePath = basePath;
+ }
+
+ /**
+ * A base class for digester factory classes. This base class maintains
+ * a default class for the objects to be created.
+ * There will be sub classes for specific configuration implementations.
+ */
+ public class DigesterConfigurationFactory extends AbstractObjectCreationFactory
+ {
+ /** Actual class to use. */
+ private Class<?> clazz;
+
+ /**
+ * Creates a new instance of {@code DigesterConfigurationFactory}.
+ *
+ * @param clazz the class which we should instantiate
+ */
+ public DigesterConfigurationFactory(Class<?> clazz)
+ {
+ this.clazz = clazz;
+ }
+
+ /**
+ * Creates an instance of the specified class.
+ *
+ * @param attribs the attributes (ignored)
+ * @return the new object
+ * @throws Exception if object creation fails
+ */
+ @Override
+ public Object createObject(Attributes attribs) throws Exception
+ {
+ return clazz.newInstance();
+ }
+ }
+
+ /**
+ * A tiny inner class that allows the Configuration Factory to
+ * let the digester construct FileConfiguration objects
+ * that already have the correct base Path set.
+ *
+ */
+ public class FileConfigurationFactory extends DigesterConfigurationFactory
+ {
+ /**
+ * C'tor
+ *
+ * @param clazz The class which we should instantiate.
+ */
+ public FileConfigurationFactory(Class<?> clazz)
+ {
+ super(clazz);
+ }
+
+ /**
+ * Gets called by the digester.
+ *
+ * @param attributes the actual attributes
+ * @return the new object
+ * @throws Exception Couldn't instantiate the requested object.
+ */
+ @Override
+ public Object createObject(Attributes attributes) throws Exception
+ {
+ FileConfiguration conf = createConfiguration(attributes);
+ conf.setBasePath(getBasePath());
+ return conf;
+ }
+
+ /**
+ * Creates the object, a {@code FileConfiguration}.
+ *
+ * @param attributes the actual attributes
+ * @return the file configuration
+ * @throws Exception if the object could not be created
+ */
+ protected FileConfiguration createConfiguration(Attributes attributes) throws Exception
+ {
+ return (FileConfiguration) super.createObject(attributes);
+ }
+ }
+
+ /**
+ * A factory that returns an XMLPropertiesConfiguration for .xml files
+ * and a PropertiesConfiguration for the others.
+ *
+ * @since 1.2
+ */
+ public class PropertiesConfigurationFactory extends FileConfigurationFactory
+ {
+ /**
+ * Creates a new instance of {@code PropertiesConfigurationFactory}.
+ */
+ public PropertiesConfigurationFactory()
+ {
+ super(null);
+ }
+
+ /**
+ * Creates the new configuration object. Based on the file name
+ * provided in the attributes either a {@code PropertiesConfiguration}
+ * or a {@code XMLPropertiesConfiguration} object will be
+ * returned.
+ *
+ * @param attributes the attributes
+ * @return the new configuration object
+ * @throws Exception if an error occurs
+ */
+ @Override
+ protected FileConfiguration createConfiguration(Attributes attributes) throws Exception
+ {
+ String filename = attributes.getValue(ATTR_FILENAME);
+
+ if (filename != null && filename.toLowerCase().trim().endsWith(".xml"))
+ {
+ return new XMLPropertiesConfiguration();
+ }
+ else
+ {
+ return new PropertiesConfiguration();
+ }
+ }
+ }
+
+ /**
+ * A factory that returns an XMLPropertyListConfiguration for .xml files
+ * and a PropertyListConfiguration for the others.
+ *
+ * @since 1.2
+ */
+ public class PropertyListConfigurationFactory extends FileConfigurationFactory
+ {
+ /**
+ * Creates a new instance of PropertyListConfigurationFactory</code>.
+ */
+ public PropertyListConfigurationFactory()
+ {
+ super(null);
+ }
+
+ /**
+ * Creates the new configuration object. Based on the file name
+ * provided in the attributes either a {@code XMLPropertyListConfiguration}
+ * or a {@code PropertyListConfiguration} object will be
+ * returned.
+ *
+ * @param attributes the attributes
+ * @return the new configuration object
+ * @throws Exception if an error occurs
+ */
+ @Override
+ protected FileConfiguration createConfiguration(Attributes attributes) throws Exception
+ {
+ String filename = attributes.getValue(ATTR_FILENAME);
+
+ if (filename != null && filename.toLowerCase().trim().endsWith(".xml"))
+ {
+ return new XMLPropertyListConfiguration();
+ }
+ else
+ {
+ return new PropertyListConfiguration();
+ }
+ }
+ }
+
+ /**
+ * A tiny inner class that allows the Configuration Factory to
+ * let the digester construct JNDIConfiguration objects.
+ */
+ private class JNDIConfigurationFactory extends DigesterConfigurationFactory
+ {
+ /**
+ * Creates a new instance of {@code JNDIConfigurationFactory}.
+ */
+ public JNDIConfigurationFactory()
+ {
+ super(JNDIConfiguration.class);
+ }
+ }
+
+ /**
+ * A tiny inner class that allows the Configuration Factory to
+ * let the digester construct SystemConfiguration objects.
+ */
+ private class SystemConfigurationFactory extends DigesterConfigurationFactory
+ {
+ /**
+ * Creates a new instance of {@code SystemConfigurationFactory}.
+ */
+ public SystemConfigurationFactory()
+ {
+ super(SystemConfiguration.class);
+ }
+ }
+
+ /**
+ * A simple data class that holds all information about a configuration
+ * from the <code><additional></code> section.
+ */
+ public static class AdditionalConfigurationData
+ {
+ /** Stores the configuration object.*/
+ private Configuration configuration;
+
+ /** Stores the location of this configuration in the global tree.*/
+ private String at;
+
+ /**
+ * Returns the value of the {@code at} attribute.
+ *
+ * @return the at attribute
+ */
+ public String getAt()
+ {
+ return at;
+ }
+
+ /**
+ * Sets the value of the {@code at} attribute.
+ *
+ * @param string the attribute value
+ */
+ public void setAt(String string)
+ {
+ at = string;
+ }
+
+ /**
+ * Returns the configuration object.
+ *
+ * @return the configuration
+ */
+ public Configuration getConfiguration()
+ {
+ return configuration;
+ }
+
+ /**
+ * Sets the configuration object. Note: Normally this method should be
+ * named {@code setConfiguration()}, but the name
+ * {@code addConfiguration()} is required by some of the digester
+ * rules.
+ *
+ * @param config the configuration to set
+ */
+ public void addConfiguration(Configuration config)
+ {
+ configuration = config;
+ }
+ }
+
+ /**
+ * An internally used helper class for constructing the composite
+ * configuration object.
+ */
+ public static class ConfigurationBuilder
+ {
+ /** Stores the composite configuration.*/
+ private CompositeConfiguration config;
+
+ /** Stores a collection with the configs from the additional section.*/
+ private Collection<AdditionalConfigurationData> additionalConfigs;
+
+ /**
+ * Creates a new instance of {@code ConfigurationBuilder}.
+ */
+ public ConfigurationBuilder()
+ {
+ config = new CompositeConfiguration();
+ additionalConfigs = new LinkedList<AdditionalConfigurationData>();
+ }
+
+ /**
+ * Adds a new configuration to this object. This method is called by
+ * Digester.
+ *
+ * @param conf the configuration to be added
+ */
+ public void addConfiguration(Configuration conf)
+ {
+ config.addConfiguration(conf);
+ }
+
+ /**
+ * Adds information about an additional configuration. This method is
+ * called by Digester.
+ *
+ * @param data the data about the additional configuration
+ */
+ public void addAdditionalConfig(AdditionalConfigurationData data)
+ {
+ additionalConfigs.add(data);
+ }
+
+ /**
+ * Returns the final composite configuration.
+ *
+ * @return the final configuration object
+ */
+ public CompositeConfiguration getConfiguration()
+ {
+ if (!additionalConfigs.isEmpty())
+ {
+ Configuration unionConfig = createAdditionalConfiguration(additionalConfigs);
+ if (unionConfig != null)
+ {
+ addConfiguration(unionConfig);
+ }
+ additionalConfigs.clear();
+ }
+
+ return config;
+ }
+
+ /**
+ * Creates a configuration object with the union of all properties
+ * defined in the <code><additional></code> section. This
+ * implementation returns a {@code HierarchicalConfiguration}
+ * object.
+ *
+ * @param configs a collection with
+ * {@code AdditionalConfigurationData} objects
+ * @return the union configuration (can be <b>null</b>)
+ */
+ protected Configuration createAdditionalConfiguration(Collection<AdditionalConfigurationData> configs)
+ {
+ HierarchicalConfiguration result = new HierarchicalConfiguration();
+
+ for (AdditionalConfigurationData cdata : configs)
+ {
+ result.addNodes(cdata.getAt(),
+ createRootNode(cdata).getChildren());
+ }
+
+ return result.isEmpty() ? null : result;
+ }
+
+ /**
+ * Creates a configuration root node for the specified configuration.
+ *
+ * @param cdata the configuration data object
+ * @return a root node for this configuration
+ */
+ private HierarchicalConfiguration.Node createRootNode(AdditionalConfigurationData cdata)
+ {
+ if (cdata.getConfiguration() instanceof HierarchicalConfiguration)
+ {
+ // we can directly use this configuration's root node
+ return ((HierarchicalConfiguration) cdata.getConfiguration()).getRoot();
+ }
+ else
+ {
+ // transform configuration to a hierarchical root node
+ HierarchicalConfiguration hc = new HierarchicalConfiguration();
+ ConfigurationUtils.copy(cdata.getConfiguration(), hc);
+ return hc.getRoot();
+ }
+ }
+ }
+
+ /**
+ * A special implementation of Digester's {@code CallMethodRule} that
+ * is internally used for calling a file configuration's {@code load()}
+ * method. This class differs from its ancestor that it catches all occurring
+ * exceptions when the specified method is called. It then checks whether
+ * for the corresponding configuration the optional attribute is set. If
+ * this is the case, the exception will simply be ignored.
+ *
+ * @since 1.4
+ */
+ private static class CallOptionalMethodRule extends CallMethodRule
+ {
+ /** A flag whether the optional attribute is set for this node. */
+ private boolean optional;
+
+ /**
+ * Creates a new instance of {@code CallOptionalMethodRule} and
+ * sets the name of the method to invoke.
+ *
+ * @param methodName the name of the method
+ */
+ public CallOptionalMethodRule(String methodName)
+ {
+ super(methodName);
+ }
+
+ /**
+ * Checks if the optional attribute is set.
+ *
+ * @param attrs the attributes
+ * @throws Exception if an error occurs
+ */
+ @Override
+ public void begin(Attributes attrs) throws Exception
+ {
+ optional = attrs.getValue(ATTR_OPTIONAL) != null
+ && PropertyConverter.toBoolean(
+ attrs.getValue(ATTR_OPTIONAL)).booleanValue();
+ super.begin(attrs);
+ }
+
+ /**
+ * Calls the method. If the optional attribute was set, occurring
+ * exceptions will be ignored.
+ *
+ * @throws Exception if an error occurs
+ */
+ @Override
+ public void end() throws Exception
+ {
+ try
+ {
+ super.end();
+ }
+ catch (Exception ex)
+ {
+ if (optional)
+ {
+ log.warn("Could not create optional configuration!", ex);
+ }
+ else
+ {
+ throw ex;
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/ConfigurationKey.java b/src/main/java/org/apache/commons/configuration/ConfigurationKey.java
new file mode 100644
index 0000000..76a55f4
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/ConfigurationKey.java
@@ -0,0 +1,687 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.io.Serializable;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * <p>A simple class that supports creation of and iteration on complex
+ * configuration keys.</p>
+ *
+ * <p>For key creation the class works similar to a StringBuilder: There are
+ * several {@code appendXXXX()} methods with which single parts
+ * of a key can be constructed. All these methods return a reference to the
+ * actual object so they can be written in a chain. When using this methods
+ * the exact syntax for keys need not be known.</p>
+ *
+ * <p>This class also defines a specialized iterator for configuration keys.
+ * With such an iterator a key can be tokenized into its single parts. For
+ * each part it can be checked whether it has an associated index.</p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationKey.java 1231749 2012-01-15 20:48:56Z oheger $
+ * @deprecated Use {@link org.apache.commons.configuration.tree.DefaultConfigurationKey}
+ * instead. It is associated with a {@code DefaultExpressionEngine} and thus
+ * can produce correct keys even if key separators have been changed.
+ */
+ at Deprecated
+public class ConfigurationKey implements Serializable
+{
+ /** Constant for a property delimiter.*/
+ public static final char PROPERTY_DELIMITER = '.';
+
+ /** Constant for an escaped delimiter. */
+ public static final String ESCAPED_DELIMITER =
+ String.valueOf(PROPERTY_DELIMITER) + String.valueOf(PROPERTY_DELIMITER);
+
+ /** Constant for an attribute start marker.*/
+ private static final String ATTRIBUTE_START = "[@";
+
+ /** Constant for an attribute end marker.*/
+ private static final String ATTRIBUTE_END = "]";
+
+ /** Constant for an index start marker.*/
+ private static final char INDEX_START = '(';
+
+ /** Constant for an index end marker.*/
+ private static final char INDEX_END = ')';
+
+ /** Constant for the initial StringBuilder size.*/
+ private static final int INITIAL_SIZE = 32;
+
+ /**
+ * The serial version ID.
+ */
+ private static final long serialVersionUID = -4299732083605277656L;
+
+ /** Holds a buffer with the so far created key.*/
+ private StringBuilder keyBuffer;
+
+ /**
+ * Creates a new, empty instance of {@code ConfigurationKey}.
+ */
+ public ConfigurationKey()
+ {
+ keyBuffer = new StringBuilder(INITIAL_SIZE);
+ }
+
+ /**
+ * Creates a new instance of {@code ConfigurationKey} and
+ * initializes it with the given key.
+ *
+ * @param key the key as a string
+ */
+ public ConfigurationKey(String key)
+ {
+ keyBuffer = new StringBuilder(key);
+ removeTrailingDelimiter();
+ }
+
+ /**
+ * Appends the name of a property to this key. If necessary, a
+ * property delimiter will be added.
+ *
+ * @param property the name of the property to be added
+ * @return a reference to this object
+ */
+ public ConfigurationKey append(String property)
+ {
+ if (keyBuffer.length() > 0 && !hasDelimiter() && !isAttributeKey(property))
+ {
+ keyBuffer.append(PROPERTY_DELIMITER);
+ }
+
+ keyBuffer.append(property);
+ removeTrailingDelimiter();
+ return this;
+ }
+
+ /**
+ * Appends an index to this configuration key.
+ *
+ * @param index the index to be appended
+ * @return a reference to this object
+ */
+ public ConfigurationKey appendIndex(int index)
+ {
+ keyBuffer.append(INDEX_START).append(index);
+ keyBuffer.append(INDEX_END);
+ return this;
+ }
+
+ /**
+ * Appends an attribute to this configuration key.
+ *
+ * @param attr the name of the attribute to be appended
+ * @return a reference to this object
+ */
+ public ConfigurationKey appendAttribute(String attr)
+ {
+ keyBuffer.append(constructAttributeKey(attr));
+ return this;
+ }
+
+ /**
+ * Checks if this key is an attribute key.
+ *
+ * @return a flag if this key is an attribute key
+ */
+ public boolean isAttributeKey()
+ {
+ return isAttributeKey(keyBuffer.toString());
+ }
+
+ /**
+ * Checks if the passed in key is an attribute key. Such attribute keys
+ * start and end with certain marker strings. In some cases they must be
+ * treated slightly different.
+ *
+ * @param key the key (part) to be checked
+ * @return a flag if this key is an attribute key
+ */
+ public static boolean isAttributeKey(String key)
+ {
+ return key != null
+ && key.startsWith(ATTRIBUTE_START)
+ && key.endsWith(ATTRIBUTE_END);
+ }
+
+ /**
+ * Decorates the given key so that it represents an attribute. Adds
+ * special start and end markers.
+ *
+ * @param key the key to be decorated
+ * @return the decorated attribute key
+ */
+ public static String constructAttributeKey(String key)
+ {
+ StringBuilder buf = new StringBuilder();
+ buf.append(ATTRIBUTE_START).append(key).append(ATTRIBUTE_END);
+ return buf.toString();
+ }
+
+ /**
+ * Extracts the name of the attribute from the given attribute key.
+ * This method removes the attribute markers - if any - from the
+ * specified key.
+ *
+ * @param key the attribute key
+ * @return the name of the corresponding attribute
+ */
+ public static String attributeName(String key)
+ {
+ return isAttributeKey(key) ? removeAttributeMarkers(key) : key;
+ }
+
+ /**
+ * Helper method for removing attribute markers from a key.
+ *
+ * @param key the key
+ * @return the key with removed attribute markers
+ */
+ static String removeAttributeMarkers(String key)
+ {
+ return key.substring(ATTRIBUTE_START.length(), key.length() - ATTRIBUTE_END.length());
+ }
+
+ /**
+ * Helper method that checks if the actual buffer ends with a property
+ * delimiter.
+ *
+ * @return a flag if there is a trailing delimiter
+ */
+ private boolean hasDelimiter()
+ {
+ int count = 0;
+ for (int idx = keyBuffer.length() - 1; idx >= 0
+ && keyBuffer.charAt(idx) == PROPERTY_DELIMITER; idx--)
+ {
+ count++;
+ }
+ return count % 2 != 0;
+ }
+
+ /**
+ * Removes a trailing delimiter if there is any.
+ */
+ private void removeTrailingDelimiter()
+ {
+ while (hasDelimiter())
+ {
+ keyBuffer.deleteCharAt(keyBuffer.length() - 1);
+ }
+ }
+
+ /**
+ * Returns a string representation of this object. This is the
+ * configuration key as a plain string.
+ *
+ * @return a string for this object
+ */
+ @Override
+ public String toString()
+ {
+ return keyBuffer.toString();
+ }
+
+ /**
+ * Returns an iterator for iterating over the single components of
+ * this configuration key.
+ *
+ * @return an iterator for this key
+ */
+ public KeyIterator iterator()
+ {
+ return new KeyIterator();
+ }
+
+ /**
+ * Returns the actual length of this configuration key.
+ *
+ * @return the length of this key
+ */
+ public int length()
+ {
+ return keyBuffer.length();
+ }
+
+ /**
+ * Sets the new length of this configuration key. With this method it is
+ * possible to truncate the key, e.g. to return to a state prior calling
+ * some {@code append()} methods. The semantic is the same as
+ * the {@code setLength()} method of {@code StringBuilder}.
+ *
+ * @param len the new length of the key
+ */
+ public void setLength(int len)
+ {
+ keyBuffer.setLength(len);
+ }
+
+ /**
+ * Checks if two {@code ConfigurationKey} objects are equal. The
+ * method can be called with strings or other objects, too.
+ *
+ * @param c the object to compare
+ * @return a flag if both objects are equal
+ */
+ @Override
+ public boolean equals(Object c)
+ {
+ if (c == null)
+ {
+ return false;
+ }
+
+ return keyBuffer.toString().equals(c.toString());
+ }
+
+ /**
+ * Returns the hash code for this object.
+ *
+ * @return the hash code
+ */
+ @Override
+ public int hashCode()
+ {
+ return String.valueOf(keyBuffer).hashCode();
+ }
+
+ /**
+ * Returns a configuration key object that is initialized with the part
+ * of the key that is common to this key and the passed in key.
+ *
+ * @param other the other key
+ * @return a key object with the common key part
+ */
+ public ConfigurationKey commonKey(ConfigurationKey other)
+ {
+ if (other == null)
+ {
+ throw new IllegalArgumentException("Other key must no be null!");
+ }
+
+ ConfigurationKey result = new ConfigurationKey();
+ KeyIterator it1 = iterator();
+ KeyIterator it2 = other.iterator();
+
+ while (it1.hasNext() && it2.hasNext() && partsEqual(it1, it2))
+ {
+ if (it1.isAttribute())
+ {
+ result.appendAttribute(it1.currentKey());
+ }
+ else
+ {
+ result.append(it1.currentKey());
+ if (it1.hasIndex)
+ {
+ result.appendIndex(it1.getIndex());
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns the "difference key" to a given key. This value
+ * is the part of the passed in key that differs from this key. There is
+ * the following relation:
+ * {@code other = key.commonKey(other) + key.differenceKey(other)}
+ * for an arbitrary configuration key {@code key}.
+ *
+ * @param other the key for which the difference is to be calculated
+ * @return the difference key
+ */
+ public ConfigurationKey differenceKey(ConfigurationKey other)
+ {
+ ConfigurationKey common = commonKey(other);
+ ConfigurationKey result = new ConfigurationKey();
+
+ if (common.length() < other.length())
+ {
+ String k = other.toString().substring(common.length());
+ // skip trailing delimiters
+ int i = 0;
+ while (i < k.length() && k.charAt(i) == PROPERTY_DELIMITER)
+ {
+ i++;
+ }
+
+ if (i < k.length())
+ {
+ result.append(k.substring(i));
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Helper method for comparing two key parts.
+ *
+ * @param it1 the iterator with the first part
+ * @param it2 the iterator with the second part
+ * @return a flag if both parts are equal
+ */
+ private static boolean partsEqual(KeyIterator it1, KeyIterator it2)
+ {
+ return it1.nextKey().equals(it2.nextKey())
+ && it1.getIndex() == it2.getIndex()
+ && it1.isAttribute() == it2.isAttribute();
+ }
+
+ /**
+ * A specialized iterator class for tokenizing a configuration key.
+ * This class implements the normal iterator interface. In addition it
+ * provides some specific methods for configuration keys.
+ */
+ public class KeyIterator implements Iterator<Object>, Cloneable
+ {
+ /** Stores the current key name.*/
+ private String current;
+
+ /** Stores the start index of the actual token.*/
+ private int startIndex;
+
+ /** Stores the end index of the actual token.*/
+ private int endIndex;
+
+ /** Stores the index of the actual property if there is one.*/
+ private int indexValue;
+
+ /** Stores a flag if the actual property has an index.*/
+ private boolean hasIndex;
+
+ /** Stores a flag if the actual property is an attribute.*/
+ private boolean attribute;
+
+ /**
+ * Helper method for determining the next indices.
+ *
+ * @return the next key part
+ */
+ private String findNextIndices()
+ {
+ startIndex = endIndex;
+ // skip empty names
+ while (startIndex < keyBuffer.length()
+ && keyBuffer.charAt(startIndex) == PROPERTY_DELIMITER)
+ {
+ startIndex++;
+ }
+
+ // Key ends with a delimiter?
+ if (startIndex >= keyBuffer.length())
+ {
+ endIndex = keyBuffer.length();
+ startIndex = endIndex - 1;
+ return keyBuffer.substring(startIndex, endIndex);
+ }
+ else
+ {
+ return nextKeyPart();
+ }
+ }
+
+ /**
+ * Helper method for extracting the next key part. Takes escaping of
+ * delimiter characters into account.
+ *
+ * @return the next key part
+ */
+ private String nextKeyPart()
+ {
+ StringBuilder key = new StringBuilder(INITIAL_SIZE);
+ int idx = startIndex;
+ int endIdx = keyBuffer.toString().indexOf(ATTRIBUTE_START,
+ startIndex);
+ if (endIdx < 0 || endIdx == startIndex)
+ {
+ endIdx = keyBuffer.length();
+ }
+ boolean found = false;
+
+ while (!found && idx < endIdx)
+ {
+ char c = keyBuffer.charAt(idx);
+ if (c == PROPERTY_DELIMITER)
+ {
+ // a duplicated delimiter means escaping
+ if (idx == endIdx - 1
+ || keyBuffer.charAt(idx + 1) != PROPERTY_DELIMITER)
+ {
+ found = true;
+ }
+ else
+ {
+ idx++;
+ }
+ }
+ if (!found)
+ {
+ key.append(c);
+ idx++;
+ }
+ }
+
+ endIndex = idx;
+ return key.toString();
+ }
+
+ /**
+ * Returns the next key part of this configuration key. This is a short
+ * form of {@code nextKey(false)}.
+ *
+ * @return the next key part
+ */
+ public String nextKey()
+ {
+ return nextKey(false);
+ }
+
+ /**
+ * Returns the next key part of this configuration key. The boolean
+ * parameter indicates wheter a decorated key should be returned. This
+ * affects only attribute keys: if the parameter is <b>false</b>, the
+ * attribute markers are stripped from the key; if it is <b>true</b>,
+ * they remain.
+ *
+ * @param decorated a flag if the decorated key is to be returned
+ * @return the next key part
+ */
+ public String nextKey(boolean decorated)
+ {
+ if (!hasNext())
+ {
+ throw new NoSuchElementException("No more key parts!");
+ }
+
+ hasIndex = false;
+ indexValue = -1;
+ String key = findNextIndices();
+
+ current = key;
+ hasIndex = checkIndex(key);
+ attribute = checkAttribute(current);
+
+ return currentKey(decorated);
+ }
+
+ /**
+ * Helper method for checking if the passed key is an attribute.
+ * If this is the case, the internal fields will be set.
+ *
+ * @param key the key to be checked
+ * @return a flag if the key is an attribute
+ */
+ private boolean checkAttribute(String key)
+ {
+ if (isAttributeKey(key))
+ {
+ current = removeAttributeMarkers(key);
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ /**
+ * Helper method for checking if the passed key contains an index.
+ * If this is the case, internal fields will be set.
+ *
+ * @param key the key to be checked
+ * @return a flag if an index is defined
+ */
+ private boolean checkIndex(String key)
+ {
+ boolean result = false;
+
+ int idx = key.lastIndexOf(INDEX_START);
+ if (idx > 0)
+ {
+ int endidx = key.indexOf(INDEX_END, idx);
+
+ if (endidx > idx + 1)
+ {
+ indexValue = Integer.parseInt(key.substring(idx + 1, endidx));
+ current = key.substring(0, idx);
+ result = true;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Checks if there is a next element.
+ *
+ * @return a flag if there is a next element
+ */
+ public boolean hasNext()
+ {
+ return endIndex < keyBuffer.length();
+ }
+
+ /**
+ * Returns the next object in the iteration.
+ *
+ * @return the next object
+ */
+ public Object next()
+ {
+ return nextKey();
+ }
+
+ /**
+ * Removes the current object in the iteration. This method is not
+ * supported by this iterator type, so an exception is thrown.
+ */
+ public void remove()
+ {
+ throw new UnsupportedOperationException("Remove not supported!");
+ }
+
+ /**
+ * Returns the current key of the iteration (without skipping to the
+ * next element). This is the same key the previous {@code next()}
+ * call had returned. (Short form of {@code currentKey(false)}.
+ *
+ * @return the current key
+ */
+ public String currentKey()
+ {
+ return currentKey(false);
+ }
+
+ /**
+ * Returns the current key of the iteration (without skipping to the
+ * next element). The boolean parameter indicates wheter a decorated
+ * key should be returned. This affects only attribute keys: if the
+ * parameter is <b>false</b>, the attribute markers are stripped from
+ * the key; if it is <b>true</b>, they remain.
+ *
+ * @param decorated a flag if the decorated key is to be returned
+ * @return the current key
+ */
+ public String currentKey(boolean decorated)
+ {
+ return (decorated && isAttribute()) ? constructAttributeKey(current) : current;
+ }
+
+ /**
+ * Returns a flag if the current key is an attribute. This method can
+ * be called after {@code next()}.
+ *
+ * @return a flag if the current key is an attribute
+ */
+ public boolean isAttribute()
+ {
+ return attribute;
+ }
+
+ /**
+ * Returns the index value of the current key. If the current key does
+ * not have an index, return value is -1. This method can be called
+ * after {@code next()}.
+ *
+ * @return the index value of the current key
+ */
+ public int getIndex()
+ {
+ return indexValue;
+ }
+
+ /**
+ * Returns a flag if the current key has an associated index.
+ * This method can be called after {@code next()}.
+ *
+ * @return a flag if the current key has an index
+ */
+ public boolean hasIndex()
+ {
+ return hasIndex;
+ }
+
+ /**
+ * Creates a clone of this object.
+ *
+ * @return a clone of this object
+ */
+ @Override
+ public Object clone()
+ {
+ try
+ {
+ return super.clone();
+ }
+ catch (CloneNotSupportedException cex)
+ {
+ // should not happen
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/ConfigurationMap.java b/src/main/java/org/apache/commons/configuration/ConfigurationMap.java
new file mode 100644
index 0000000..2f93f6c
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/ConfigurationMap.java
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * <p>The {@code ConfigurationMap} wraps a
+ * configuration-collection
+ * {@link org.apache.commons.configuration.Configuration}
+ * instance to provide a {@code Map} interface.</p>
+ *
+ * <p><em>Note:</em> This implementation is incomplete.</p>
+ *
+ * @author <a href="mailto:ricardo.gladwell at btinternet.com">Ricardo Gladwell</a>
+ * @version $Id: ConfigurationMap.java 1301959 2012-03-17 16:43:18Z oheger $
+ * @since 1.0
+ */
+public class ConfigurationMap extends AbstractMap<Object, Object>
+{
+ /**
+ * The {@code Configuration} wrapped by this class.
+ */
+ private final Configuration configuration;
+
+ /**
+ * Creates a new instance of a {@code ConfigurationMap}
+ * that wraps the specified {@code Configuration}
+ * instance.
+ * @param configuration {@code Configuration}
+ * instance.
+ */
+ public ConfigurationMap(Configuration configuration)
+ {
+ this.configuration = configuration;
+ }
+
+ /**
+ * Returns the wrapped {@code Configuration} object.
+ *
+ * @return the wrapped configuration
+ * @since 1.2
+ */
+ public Configuration getConfiguration()
+ {
+ return configuration;
+ }
+
+ /**
+ * Returns a set with the entries contained in this configuration-based map.
+ *
+ * @return a set with the contained entries
+ * @see java.util.Map#entrySet()
+ */
+ @Override
+ public Set<Map.Entry<Object, Object>> entrySet()
+ {
+ return new ConfigurationSet(configuration);
+ }
+
+ /**
+ * Stores the value for the specified key. The value is stored in the
+ * underlying configuration.
+ *
+ * @param key the key (will be converted to a string)
+ * @param value the value
+ * @return the old value of this key or <b>null</b> if it is new
+ * @see java.util.Map#put(java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public Object put(Object key, Object value)
+ {
+ String strKey = String.valueOf(key);
+ Object old = configuration.getProperty(strKey);
+ configuration.setProperty(strKey, value);
+ return old;
+ }
+
+ /**
+ * Returns the value of the specified key. The key is converted to a string
+ * and then passed to the underlying configuration.
+ *
+ * @param key the key
+ * @return the value of this key
+ * @see java.util.Map#get(java.lang.Object)
+ */
+ @Override
+ public Object get(Object key)
+ {
+ return configuration.getProperty(String.valueOf(key));
+ }
+
+ /**
+ * Set of entries in the map.
+ */
+ static class ConfigurationSet extends AbstractSet<Map.Entry<Object, Object>>
+ {
+ /** The configuration mapped to this entry set. */
+ private final Configuration configuration;
+
+ /**
+ * A Map entry in the ConfigurationMap.
+ */
+ private final class Entry implements Map.Entry<Object, Object>
+ {
+ /** The key of the map entry. */
+ private Object key;
+
+ private Entry(Object key)
+ {
+ this.key = key;
+ }
+
+ public Object getKey()
+ {
+ return key;
+ }
+
+ public Object getValue()
+ {
+ return configuration.getProperty((String) key);
+ }
+
+ public Object setValue(Object value)
+ {
+ Object old = getValue();
+ configuration.setProperty((String) key, value);
+ return old;
+ }
+ }
+
+ /**
+ * Iterator over the entries in the ConfigurationMap.
+ */
+ private final class ConfigurationSetIterator implements Iterator<Map.Entry<Object, Object>>
+ {
+ /** An iterator over the keys in the configuration. */
+ private final Iterator<String> keys;
+
+ private ConfigurationSetIterator()
+ {
+ keys = configuration.getKeys();
+ }
+
+ public boolean hasNext()
+ {
+ return keys.hasNext();
+ }
+
+ public Map.Entry<Object, Object> next()
+ {
+ return new Entry(keys.next());
+ }
+
+ public void remove()
+ {
+ keys.remove();
+ }
+ }
+
+ ConfigurationSet(Configuration configuration)
+ {
+ this.configuration = configuration;
+ }
+
+ /**
+ * @see java.util.Collection#size()
+ */
+ @Override
+ public int size()
+ {
+ // Ouch. Now _that_ one is expensive...
+ int count = 0;
+ for (Iterator<String> iterator = configuration.getKeys(); iterator.hasNext();)
+ {
+ iterator.next();
+ count++;
+ }
+ return count;
+ }
+
+ /**
+ * @see java.util.Collection#iterator()
+ */
+ @Override
+ public Iterator<Map.Entry<Object, Object>> iterator()
+ {
+ return new ConfigurationSetIterator();
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/ConfigurationRuntimeException.java b/src/main/java/org/apache/commons/configuration/ConfigurationRuntimeException.java
new file mode 100644
index 0000000..44d9d43
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/ConfigurationRuntimeException.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import org.apache.commons.lang.exception.NestableRuntimeException;
+
+/**
+ * A configuration related runtime exception.
+ *
+ * @since 1.0
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: ConfigurationRuntimeException.java 1208785 2011-11-30 21:13:50Z oheger $
+ */
+public class ConfigurationRuntimeException extends NestableRuntimeException
+{
+ /**
+ * The serial version ID.
+ */
+ private static final long serialVersionUID = -7838702245512140996L;
+
+ /**
+ * Constructs a new {@code ConfigurationRuntimeException} without
+ * specified detail message.
+ */
+ public ConfigurationRuntimeException()
+ {
+ super();
+ }
+
+ /**
+ * Constructs a new {@code ConfigurationRuntimeException} with
+ * specified detail message.
+ *
+ * @param message the error message
+ */
+ public ConfigurationRuntimeException(String message)
+ {
+ super(message);
+ }
+
+ /**
+ * Constructs a new {@code ConfigurationRuntimeException} with
+ * specified nested {@code Throwable}.
+ *
+ * @param cause the exception or error that caused this exception to be thrown
+ */
+ public ConfigurationRuntimeException(Throwable cause)
+ {
+ super(cause);
+ }
+
+ /**
+ * Constructs a new {@code ConfigurationRuntimeException} with
+ * specified detail message and nested {@code Throwable}.
+ *
+ * @param message the error message
+ * @param cause the exception or error that caused this exception to be thrown
+ */
+ public ConfigurationRuntimeException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/ConfigurationUtils.java b/src/main/java/org/apache/commons/configuration/ConfigurationUtils.java
new file mode 100644
index 0000000..2fd9b55
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/ConfigurationUtils.java
@@ -0,0 +1,759 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Iterator;
+
+import org.apache.commons.configuration.event.ConfigurationErrorEvent;
+import org.apache.commons.configuration.event.ConfigurationErrorListener;
+import org.apache.commons.configuration.event.EventSource;
+import org.apache.commons.configuration.reloading.Reloadable;
+import org.apache.commons.configuration.tree.ExpressionEngine;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Miscellaneous utility methods for configurations.
+ *
+ * @see ConfigurationConverter Utility methods to convert configurations.
+ *
+ * @author <a href="mailto:herve.quiroz at esil.univ-mrs.fr">Herve Quiroz</a>
+ * @author Emmanuel Bourg
+ * @version $Id: ConfigurationUtils.java 1208795 2011-11-30 21:18:17Z oheger $
+ */
+public final class ConfigurationUtils
+{
+ /** Constant for the file URL protocol.*/
+ static final String PROTOCOL_FILE = "file";
+
+ /** Constant for the resource path separator.*/
+ static final String RESOURCE_PATH_SEPARATOR = "/";
+
+ /** Constant for the file URL protocol */
+ private static final String FILE_SCHEME = "file:";
+
+ /** Constant for the name of the clone() method.*/
+ private static final String METHOD_CLONE = "clone";
+
+ /** Constant for parsing numbers in hex format. */
+ private static final int HEX = 16;
+
+ /** The logger.*/
+ private static final Log LOG = LogFactory.getLog(ConfigurationUtils.class);
+
+ /**
+ * Private constructor. Prevents instances from being created.
+ */
+ private ConfigurationUtils()
+ {
+ // to prevent instantiation...
+ }
+
+ /**
+ * Dump the configuration key/value mappings to some ouput stream.
+ *
+ * @param configuration the configuration
+ * @param out the output stream to dump the configuration to
+ */
+ public static void dump(Configuration configuration, PrintStream out)
+ {
+ dump(configuration, new PrintWriter(out));
+ }
+
+ /**
+ * Dump the configuration key/value mappings to some writer.
+ *
+ * @param configuration the configuration
+ * @param out the writer to dump the configuration to
+ */
+ public static void dump(Configuration configuration, PrintWriter out)
+ {
+ for (Iterator<String> keys = configuration.getKeys(); keys.hasNext();)
+ {
+ String key = keys.next();
+ Object value = configuration.getProperty(key);
+ out.print(key);
+ out.print("=");
+ out.print(value);
+
+ if (keys.hasNext())
+ {
+ out.println();
+ }
+ }
+
+ out.flush();
+ }
+
+ /**
+ * Get a string representation of the key/value mappings of a
+ * configuration.
+ *
+ * @param configuration the configuration
+ * @return a string representation of the configuration
+ */
+ public static String toString(Configuration configuration)
+ {
+ StringWriter writer = new StringWriter();
+ dump(configuration, new PrintWriter(writer));
+ return writer.toString();
+ }
+
+ /**
+ * <p>Copy all properties from the source configuration to the target
+ * configuration. Properties in the target configuration are replaced with
+ * the properties with the same key in the source configuration.</p>
+ * <p><em>Note:</em> This method is not able to handle some specifics of
+ * configurations derived from {@code AbstractConfiguration} (e.g.
+ * list delimiters). For a full support of all of these features the
+ * {@code copy()} method of {@code AbstractConfiguration} should
+ * be used. In a future release this method might become deprecated.</p>
+ *
+ * @param source the source configuration
+ * @param target the target configuration
+ * @since 1.1
+ */
+ public static void copy(Configuration source, Configuration target)
+ {
+ for (Iterator<String> keys = source.getKeys(); keys.hasNext();)
+ {
+ String key = keys.next();
+ target.setProperty(key, source.getProperty(key));
+ }
+ }
+
+ /**
+ * <p>Append all properties from the source configuration to the target
+ * configuration. Properties in the source configuration are appended to
+ * the properties with the same key in the target configuration.</p>
+ * <p><em>Note:</em> This method is not able to handle some specifics of
+ * configurations derived from {@code AbstractConfiguration} (e.g.
+ * list delimiters). For a full support of all of these features the
+ * {@code copy()} method of {@code AbstractConfiguration} should
+ * be used. In a future release this method might become deprecated.</p>
+ *
+ * @param source the source configuration
+ * @param target the target configuration
+ * @since 1.1
+ */
+ public static void append(Configuration source, Configuration target)
+ {
+ for (Iterator<String> keys = source.getKeys(); keys.hasNext();)
+ {
+ String key = keys.next();
+ target.addProperty(key, source.getProperty(key));
+ }
+ }
+
+ /**
+ * Converts the passed in configuration to a hierarchical one. If the
+ * configuration is already hierarchical, it is directly returned. Otherwise
+ * all properties are copied into a new hierarchical configuration.
+ *
+ * @param conf the configuration to convert
+ * @return the new hierarchical configuration (the result is <b>null</b> if
+ * and only if the passed in configuration is <b>null</b>)
+ * @since 1.3
+ */
+ public static HierarchicalConfiguration convertToHierarchical(
+ Configuration conf)
+ {
+ return convertToHierarchical(conf, null);
+ }
+
+ /**
+ * Converts the passed in {@code Configuration} object to a
+ * hierarchical one using the specified {@code ExpressionEngine}. This
+ * conversion works by adding the keys found in the configuration to a newly
+ * created hierarchical configuration. When adding new keys to a
+ * hierarchical configuration the keys are interpreted by its
+ * {@code ExpressionEngine}. If they contain special characters (e.g.
+ * brackets) that are treated in a special way by the default expression
+ * engine, it may be necessary using a specific engine that can deal with
+ * such characters. Otherwise <b>null</b> can be passed in for the
+ * {@code ExpressionEngine}; then the default expression engine is
+ * used. If the passed in configuration is already hierarchical, it is
+ * directly returned. (However, the {@code ExpressionEngine} is set if
+ * it is not <b>null</b>.) Otherwise all properties are copied into a new
+ * hierarchical configuration.
+ *
+ * @param conf the configuration to convert
+ * @param engine the {@code ExpressionEngine} for the hierarchical
+ * configuration or <b>null</b> for the default
+ * @return the new hierarchical configuration (the result is <b>null</b> if
+ * and only if the passed in configuration is <b>null</b>)
+ * @since 1.6
+ */
+ public static HierarchicalConfiguration convertToHierarchical(
+ Configuration conf, ExpressionEngine engine)
+ {
+ if (conf == null)
+ {
+ return null;
+ }
+
+ if (conf instanceof HierarchicalConfiguration)
+ {
+ HierarchicalConfiguration hc;
+ if (conf instanceof Reloadable)
+ {
+ Object lock = ((Reloadable) conf).getReloadLock();
+ synchronized (lock)
+ {
+ hc = new HierarchicalConfiguration((HierarchicalConfiguration) conf);
+ }
+ }
+ else
+ {
+ hc = (HierarchicalConfiguration) conf;
+ }
+ if (engine != null)
+ {
+ hc.setExpressionEngine(engine);
+ }
+
+ return hc;
+ }
+ else
+ {
+ HierarchicalConfiguration hc = new HierarchicalConfiguration();
+ if (engine != null)
+ {
+ hc.setExpressionEngine(engine);
+ }
+
+ // Workaround for problem with copy()
+ boolean delimiterParsingStatus = hc.isDelimiterParsingDisabled();
+ hc.setDelimiterParsingDisabled(true);
+ hc.append(conf);
+ hc.setDelimiterParsingDisabled(delimiterParsingStatus);
+ return hc;
+ }
+ }
+
+ /**
+ * Clones the given configuration object if this is possible. If the passed
+ * in configuration object implements the {@code Cloneable}
+ * interface, its {@code clone()} method will be invoked. Otherwise
+ * an exception will be thrown.
+ *
+ * @param config the configuration object to be cloned (can be <b>null</b>)
+ * @return the cloned configuration (<b>null</b> if the argument was
+ * <b>null</b>, too)
+ * @throws ConfigurationRuntimeException if cloning is not supported for
+ * this object
+ * @since 1.3
+ */
+ public static Configuration cloneConfiguration(Configuration config)
+ throws ConfigurationRuntimeException
+ {
+ if (config == null)
+ {
+ return null;
+ }
+ else
+ {
+ try
+ {
+ return (Configuration) clone(config);
+ }
+ catch (CloneNotSupportedException cnex)
+ {
+ throw new ConfigurationRuntimeException(cnex);
+ }
+ }
+ }
+
+ /**
+ * An internally used helper method for cloning objects. This implementation
+ * is not very sophisticated nor efficient. Maybe it can be replaced by an
+ * implementation from Commons Lang later. The method checks whether the
+ * passed in object implements the {@code Cloneable} interface. If
+ * this is the case, the {@code clone()} method is invoked by
+ * reflection. Errors that occur during the cloning process are re-thrown as
+ * runtime exceptions.
+ *
+ * @param obj the object to be cloned
+ * @return the cloned object
+ * @throws CloneNotSupportedException if the object cannot be cloned
+ */
+ static Object clone(Object obj) throws CloneNotSupportedException
+ {
+ if (obj instanceof Cloneable)
+ {
+ try
+ {
+ Method m = obj.getClass().getMethod(METHOD_CLONE);
+ return m.invoke(obj);
+ }
+ catch (NoSuchMethodException nmex)
+ {
+ throw new CloneNotSupportedException(
+ "No clone() method found for class"
+ + obj.getClass().getName());
+ }
+ catch (IllegalAccessException iaex)
+ {
+ throw new ConfigurationRuntimeException(iaex);
+ }
+ catch (InvocationTargetException itex)
+ {
+ throw new ConfigurationRuntimeException(itex);
+ }
+ }
+ else
+ {
+ throw new CloneNotSupportedException(obj.getClass().getName()
+ + " does not implement Cloneable");
+ }
+ }
+
+ /**
+ * Constructs a URL from a base path and a file name. The file name can
+ * be absolute, relative or a full URL. If necessary the base path URL is
+ * applied.
+ *
+ * @param basePath the base path URL (can be <b>null</b>)
+ * @param file the file name
+ * @return the resulting URL
+ * @throws MalformedURLException if URLs are invalid
+ */
+ public static URL getURL(String basePath, String file) throws MalformedURLException
+ {
+ return FileSystem.getDefaultFileSystem().getURL(basePath, file);
+ }
+
+ /**
+ * Helper method for constructing a file object from a base path and a
+ * file name. This method is called if the base path passed to
+ * {@code getURL()} does not seem to be a valid URL.
+ *
+ * @param basePath the base path
+ * @param fileName the file name
+ * @return the resulting file
+ */
+ static File constructFile(String basePath, String fileName)
+ {
+ File file;
+
+ File absolute = null;
+ if (fileName != null)
+ {
+ absolute = new File(fileName);
+ }
+
+ if (StringUtils.isEmpty(basePath) || (absolute != null && absolute.isAbsolute()))
+ {
+ file = new File(fileName);
+ }
+ else
+ {
+ StringBuilder fName = new StringBuilder();
+ fName.append(basePath);
+
+ // My best friend. Paranoia.
+ if (!basePath.endsWith(File.separator))
+ {
+ fName.append(File.separator);
+ }
+
+ //
+ // We have a relative path, and we have
+ // two possible forms here. If we have the
+ // "./" form then just strip that off first
+ // before continuing.
+ //
+ if (fileName.startsWith("." + File.separator))
+ {
+ fName.append(fileName.substring(2));
+ }
+ else
+ {
+ fName.append(fileName);
+ }
+
+ file = new File(fName.toString());
+ }
+
+ return file;
+ }
+
+ /**
+ * Return the location of the specified resource by searching the user home
+ * directory, the current classpath and the system classpath.
+ *
+ * @param name the name of the resource
+ *
+ * @return the location of the resource
+ */
+ public static URL locate(String name)
+ {
+ return locate(null, name);
+ }
+
+ /**
+ * Return the location of the specified resource by searching the user home
+ * directory, the current classpath and the system classpath.
+ *
+ * @param base the base path of the resource
+ * @param name the name of the resource
+ *
+ * @return the location of the resource
+ */
+ public static URL locate(String base, String name)
+ {
+ return locate(FileSystem.getDefaultFileSystem(), base, name);
+ }
+
+ /**
+ * Return the location of the specified resource by searching the user home
+ * directory, the current classpath and the system classpath.
+ *
+ * @param fileSystem the FileSystem to use.
+ * @param base the base path of the resource
+ * @param name the name of the resource
+ *
+ * @return the location of the resource
+ */
+ public static URL locate(FileSystem fileSystem, String base, String name)
+ {
+ if (LOG.isDebugEnabled())
+ {
+ StringBuilder buf = new StringBuilder();
+ buf.append("ConfigurationUtils.locate(): base is ").append(base);
+ buf.append(", name is ").append(name);
+ LOG.debug(buf.toString());
+ }
+
+ if (name == null)
+ {
+ // undefined, always return null
+ return null;
+ }
+
+ // attempt to create an URL directly
+
+ URL url = fileSystem.locateFromURL(base, name);
+
+ // attempt to load from an absolute path
+ if (url == null)
+ {
+ File file = new File(name);
+ if (file.isAbsolute() && file.exists()) // already absolute?
+ {
+ try
+ {
+ url = toURL(file);
+ LOG.debug("Loading configuration from the absolute path " + name);
+ }
+ catch (MalformedURLException e)
+ {
+ LOG.warn("Could not obtain URL from file", e);
+ }
+ }
+ }
+
+ // attempt to load from the base directory
+ if (url == null)
+ {
+ try
+ {
+ File file = constructFile(base, name);
+ if (file != null && file.exists())
+ {
+ url = toURL(file);
+ }
+
+ if (url != null)
+ {
+ LOG.debug("Loading configuration from the path " + file);
+ }
+ }
+ catch (MalformedURLException e)
+ {
+ LOG.warn("Could not obtain URL from file", e);
+ }
+ }
+
+ // attempt to load from the user home directory
+ if (url == null)
+ {
+ try
+ {
+ File file = constructFile(System.getProperty("user.home"), name);
+ if (file != null && file.exists())
+ {
+ url = toURL(file);
+ }
+
+ if (url != null)
+ {
+ LOG.debug("Loading configuration from the home path " + file);
+ }
+
+ }
+ catch (MalformedURLException e)
+ {
+ LOG.warn("Could not obtain URL from file", e);
+ }
+ }
+
+ // attempt to load from classpath
+ if (url == null)
+ {
+ url = locateFromClasspath(name);
+ }
+ return url;
+ }
+
+ /**
+ * Tries to find a resource with the given name in the classpath.
+ * @param resourceName the name of the resource
+ * @return the URL to the found resource or <b>null</b> if the resource
+ * cannot be found
+ */
+ static URL locateFromClasspath(String resourceName)
+ {
+ URL url = null;
+ // attempt to load from the context classpath
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ if (loader != null)
+ {
+ url = loader.getResource(resourceName);
+
+ if (url != null)
+ {
+ LOG.debug("Loading configuration from the context classpath (" + resourceName + ")");
+ }
+ }
+
+ // attempt to load from the system classpath
+ if (url == null)
+ {
+ url = ClassLoader.getSystemResource(resourceName);
+
+ if (url != null)
+ {
+ LOG.debug("Loading configuration from the system classpath (" + resourceName + ")");
+ }
+ }
+ return url;
+ }
+
+ /**
+ * Return the path without the file name, for example http://xyz.net/foo/bar.xml
+ * results in http://xyz.net/foo/
+ *
+ * @param url the URL from which to extract the path
+ * @return the path component of the passed in URL
+ */
+ static String getBasePath(URL url)
+ {
+ if (url == null)
+ {
+ return null;
+ }
+
+ String s = url.toString();
+ if (s.startsWith(FILE_SCHEME) && !s.startsWith("file://"))
+ {
+ s = "file://" + s.substring(FILE_SCHEME.length());
+ }
+
+ if (s.endsWith("/") || StringUtils.isEmpty(url.getPath()))
+ {
+ return s;
+ }
+ else
+ {
+ return s.substring(0, s.lastIndexOf("/") + 1);
+ }
+ }
+
+ /**
+ * Extract the file name from the specified URL.
+ *
+ * @param url the URL from which to extract the file name
+ * @return the extracted file name
+ */
+ static String getFileName(URL url)
+ {
+ if (url == null)
+ {
+ return null;
+ }
+
+ String path = url.getPath();
+
+ if (path.endsWith("/") || StringUtils.isEmpty(path))
+ {
+ return null;
+ }
+ else
+ {
+ return path.substring(path.lastIndexOf("/") + 1);
+ }
+ }
+
+ /**
+ * Tries to convert the specified base path and file name into a file object.
+ * This method is called e.g. by the save() methods of file based
+ * configurations. The parameter strings can be relative files, absolute
+ * files and URLs as well. This implementation checks first whether the passed in
+ * file name is absolute. If this is the case, it is returned. Otherwise
+ * further checks are performed whether the base path and file name can be
+ * combined to a valid URL or a valid file name. <em>Note:</em> The test
+ * if the passed in file name is absolute is performed using
+ * {@code java.io.File.isAbsolute()}. If the file name starts with a
+ * slash, this method will return <b>true</b> on Unix, but <b>false</b> on
+ * Windows. So to ensure correct behavior for relative file names on all
+ * platforms you should never let relative paths start with a slash. E.g.
+ * in a configuration definition file do not use something like that:
+ * <pre>
+ * <properties fileName="/subdir/my.properties"/>
+ * </pre>
+ * Under Windows this path would be resolved relative to the configuration
+ * definition file. Under Unix this would be treated as an absolute path
+ * name.
+ *
+ * @param basePath the base path
+ * @param fileName the file name
+ * @return the file object (<b>null</b> if no file can be obtained)
+ */
+ public static File getFile(String basePath, String fileName)
+ {
+ // Check if the file name is absolute
+ File f = new File(fileName);
+ if (f.isAbsolute())
+ {
+ return f;
+ }
+
+ // Check if URLs are involved
+ URL url;
+ try
+ {
+ url = new URL(new URL(basePath), fileName);
+ }
+ catch (MalformedURLException mex1)
+ {
+ try
+ {
+ url = new URL(fileName);
+ }
+ catch (MalformedURLException mex2)
+ {
+ url = null;
+ }
+ }
+
+ if (url != null)
+ {
+ return fileFromURL(url);
+ }
+
+ return constructFile(basePath, fileName);
+ }
+
+ /**
+ * Tries to convert the specified URL to a file object. If this fails,
+ * <b>null</b> is returned. Note: This code has been copied from the
+ * {@code FileUtils} class from <em>Commons IO</em>.
+ *
+ * @param url the URL
+ * @return the resulting file object
+ */
+ public static File fileFromURL(URL url)
+ {
+ if (url == null || !url.getProtocol().equals(PROTOCOL_FILE))
+ {
+ return null;
+ }
+ else
+ {
+ String filename = url.getFile().replace('/', File.separatorChar);
+ int pos = 0;
+ while ((pos = filename.indexOf('%', pos)) >= 0)
+ {
+ if (pos + 2 < filename.length())
+ {
+ String hexStr = filename.substring(pos + 1, pos + 3);
+ char ch = (char) Integer.parseInt(hexStr, HEX);
+ filename = filename.substring(0, pos) + ch
+ + filename.substring(pos + 3);
+ }
+ }
+ return new File(filename);
+ }
+ }
+
+ /**
+ * Convert the specified file into an URL. This method is equivalent
+ * to file.toURI().toURL(). It was used to work around a bug in the JDK
+ * preventing the transformation of a file into an URL if the file name
+ * contains a '#' character. See the issue CONFIGURATION-300 for
+ * more details. Now that we switched to JDK 1.4 we can directly use
+ * file.toURI().toURL().
+ *
+ * @param file the file to be converted into an URL
+ */
+ static URL toURL(File file) throws MalformedURLException
+ {
+ return file.toURI().toURL();
+ }
+
+ /**
+ * Enables runtime exceptions for the specified configuration object. This
+ * method can be used for configuration implementations that may face errors
+ * on normal property access, e.g. {@code DatabaseConfiguration} or
+ * {@code JNDIConfiguration}. Per default such errors are simply
+ * logged and then ignored. This implementation will register a special
+ * {@link ConfigurationErrorListener} that throws a runtime
+ * exception (namely a {@code ConfigurationRuntimeException}) on
+ * each received error event.
+ *
+ * @param src the configuration, for which runtime exceptions are to be
+ * enabled; this configuration must be derived from
+ * {@link EventSource}
+ */
+ public static void enableRuntimeExceptions(Configuration src)
+ {
+ if (!(src instanceof EventSource))
+ {
+ throw new IllegalArgumentException(
+ "Configuration must be derived from EventSource!");
+ }
+ ((EventSource) src).addErrorListener(new ConfigurationErrorListener()
+ {
+ public void configurationError(ConfigurationErrorEvent event)
+ {
+ // Throw a runtime exception
+ throw new ConfigurationRuntimeException(event.getCause());
+ }
+ });
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/ConfigurationXMLReader.java b/src/main/java/org/apache/commons/configuration/ConfigurationXMLReader.java
new file mode 100644
index 0000000..58190f4
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/ConfigurationXMLReader.java
@@ -0,0 +1,364 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.io.IOException;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.DTDHandler;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.AttributesImpl;
+
+/**
+ * <p>A base class for "faked" {@code XMLReader} classes
+ * that transform a configuration object in a set of SAX parsing events.</p>
+ * <p>This class provides dummy implementations for most of the methods
+ * defined in the {@code XMLReader} interface that are not used for this
+ * special purpose. There will be concrete sub classes that process specific
+ * configuration classes.</p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationXMLReader.java 1208805 2011-11-30 21:33:33Z oheger $
+ */
+public abstract class ConfigurationXMLReader implements XMLReader
+{
+ /** Constant for the namespace URI.*/
+ protected static final String NS_URI = "";
+
+ /** Constant for the default name of the root element.*/
+ private static final String DEFAULT_ROOT_NAME = "config";
+
+ /** An empty attributes object.*/
+ private static final Attributes EMPTY_ATTRS = new AttributesImpl();
+
+ /** Stores the content handler.*/
+ private ContentHandler contentHandler;
+
+ /** Stores an exception that occurred during parsing.*/
+ private SAXException exception;
+
+ /** Stores the name for the root element.*/
+ private String rootName;
+
+ /**
+ * Creates a new instance of {@code ConfigurationXMLReader}.
+ */
+ protected ConfigurationXMLReader()
+ {
+ super();
+ setRootName(DEFAULT_ROOT_NAME);
+ }
+
+ /**
+ * Parses the acutal configuration object. The passed system ID will be
+ * ignored.
+ *
+ * @param systemId the system ID (ignored)
+ * @throws IOException if no configuration was specified
+ * @throws SAXException if an error occurs during parsing
+ */
+ public void parse(String systemId) throws IOException, SAXException
+ {
+ parseConfiguration();
+ }
+
+ /**
+ * Parses the actual configuration object. The passed input source will be
+ * ignored.
+ *
+ * @param input the input source (ignored)
+ * @throws IOException if no configuration was specified
+ * @throws SAXException if an error occurs during parsing
+ */
+ public void parse(InputSource input) throws IOException, SAXException
+ {
+ parseConfiguration();
+ }
+
+ /**
+ * Dummy implementation of the interface method.
+ *
+ * @param name the name of the feature
+ * @return always <b>false</b> (no features are supported)
+ */
+ public boolean getFeature(String name)
+ {
+ return false;
+ }
+
+ /**
+ * Dummy implementation of the interface method.
+ *
+ * @param name the name of the feature to be set
+ * @param value the value of the feature
+ */
+ public void setFeature(String name, boolean value)
+ {
+ }
+
+ /**
+ * Returns the actually set content handler.
+ *
+ * @return the content handler
+ */
+ public ContentHandler getContentHandler()
+ {
+ return contentHandler;
+ }
+
+ /**
+ * Sets the content handler. The object specified here will receive SAX
+ * events during parsing.
+ *
+ * @param handler the content handler
+ */
+ public void setContentHandler(ContentHandler handler)
+ {
+ contentHandler = handler;
+ }
+
+ /**
+ * Returns the DTD handler. This class does not support DTD handlers,
+ * so this method always returns <b>null</b>.
+ *
+ * @return the DTD handler
+ */
+ public DTDHandler getDTDHandler()
+ {
+ return null;
+ }
+
+ /**
+ * Sets the DTD handler. The passed value is ignored.
+ *
+ * @param handler the handler to be set
+ */
+ public void setDTDHandler(DTDHandler handler)
+ {
+ }
+
+ /**
+ * Returns the entity resolver. This class does not support an entity
+ * resolver, so this method always returns <b>null</b>.
+ *
+ * @return the entity resolver
+ */
+ public EntityResolver getEntityResolver()
+ {
+ return null;
+ }
+
+ /**
+ * Sets the entity resolver. The passed value is ignored.
+ *
+ * @param resolver the entity resolver
+ */
+ public void setEntityResolver(EntityResolver resolver)
+ {
+ }
+
+ /**
+ * Returns the error handler. This class does not support an error handler,
+ * so this method always returns <b>null</b>.
+ *
+ * @return the error handler
+ */
+ public ErrorHandler getErrorHandler()
+ {
+ return null;
+ }
+
+ /**
+ * Sets the error handler. The passed value is ignored.
+ *
+ * @param handler the error handler
+ */
+ public void setErrorHandler(ErrorHandler handler)
+ {
+ }
+
+ /**
+ * Dummy implementation of the interface method. No properties are
+ * supported, so this method always returns <b>null</b>.
+ *
+ * @param name the name of the requested property
+ * @return the property value
+ */
+ public Object getProperty(String name)
+ {
+ return null;
+ }
+
+ /**
+ * Dummy implementation of the interface method. No properties are
+ * supported, so a call of this method just has no effect.
+ *
+ * @param name the property name
+ * @param value the property value
+ */
+ public void setProperty(String name, Object value)
+ {
+ }
+
+ /**
+ * Returns the name to be used for the root element.
+ *
+ * @return the name for the root element
+ */
+ public String getRootName()
+ {
+ return rootName;
+ }
+
+ /**
+ * Sets the name for the root element.
+ *
+ * @param string the name for the root element.
+ */
+ public void setRootName(String string)
+ {
+ rootName = string;
+ }
+
+ /**
+ * Fires a SAX element start event.
+ *
+ * @param name the name of the actual element
+ * @param attribs the attributes of this element (can be <b>null</b>)
+ */
+ protected void fireElementStart(String name, Attributes attribs)
+ {
+ if (getException() == null)
+ {
+ try
+ {
+ Attributes at = (attribs == null) ? EMPTY_ATTRS : attribs;
+ getContentHandler().startElement(NS_URI, name, name, at);
+ }
+ catch (SAXException ex)
+ {
+ exception = ex;
+ }
+ }
+ }
+
+ /**
+ * Fires a SAX element end event.
+ *
+ * @param name the name of the affected element
+ */
+ protected void fireElementEnd(String name)
+ {
+ if (getException() == null)
+ {
+ try
+ {
+ getContentHandler().endElement(NS_URI, name, name);
+ }
+ catch (SAXException ex)
+ {
+ exception = ex;
+ }
+ }
+ }
+
+ /**
+ * Fires a SAX characters event.
+ *
+ * @param text the text
+ */
+ protected void fireCharacters(String text)
+ {
+ if (getException() == null)
+ {
+ try
+ {
+ char[] ch = text.toCharArray();
+ getContentHandler().characters(ch, 0, ch.length);
+ }
+ catch (SAXException ex)
+ {
+ exception = ex;
+ }
+ }
+ }
+
+ /**
+ * Returns a reference to an exception that occurred during parsing.
+ *
+ * @return a SAXExcpetion or <b>null</b> if none occurred
+ */
+ public SAXException getException()
+ {
+ return exception;
+ }
+
+ /**
+ * Parses the configuration object and generates SAX events. This is the
+ * main processing method.
+ *
+ * @throws IOException if no configuration has been specified
+ * @throws SAXException if an error occurs during parsing
+ */
+ protected void parseConfiguration() throws IOException, SAXException
+ {
+ if (getParsedConfiguration() == null)
+ {
+ throw new IOException("No configuration specified!");
+ }
+
+ if (getContentHandler() != null)
+ {
+ exception = null;
+ getContentHandler().startDocument();
+ processKeys();
+ if (getException() != null)
+ {
+ throw getException();
+ }
+ getContentHandler().endDocument();
+ }
+ }
+
+ /**
+ * Returns a reference to the configuration that is parsed by this object.
+ *
+ * @return the parsed configuration
+ */
+ public abstract Configuration getParsedConfiguration();
+
+ /**
+ * Processes all keys stored in the actual configuration. This method is
+ * called by {@code parseConfiguration()} to start the main parsing
+ * process. {@code parseConfiguration()} calls the content handler's
+ * {@code startDocument()} and {@code endElement()} methods
+ * and cares for exception handling. The remaining actions are left to this
+ * method that must be implemented in a concrete sub class.
+ *
+ * @throws IOException if an IO error occurs
+ * @throws SAXException if a SAX error occurs
+ */
+ protected abstract void processKeys() throws IOException, SAXException;
+}
diff --git a/src/main/java/org/apache/commons/configuration/ConversionException.java b/src/main/java/org/apache/commons/configuration/ConversionException.java
new file mode 100644
index 0000000..179b2b9
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/ConversionException.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+/**
+ * Exception thrown when a property is incompatible with the type requested.
+ *
+ * @since 1.0
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: ConversionException.java 1208806 2011-11-30 21:34:11Z oheger $
+ */
+public class ConversionException extends ConfigurationRuntimeException
+{
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = -5167943099293540392L;
+
+ /**
+ * Constructs a new {@code ConversionException} without specified
+ * detail message.
+ */
+ public ConversionException()
+ {
+ super();
+ }
+
+ /**
+ * Constructs a new {@code ConversionException} with specified
+ * detail message.
+ *
+ * @param message the error message
+ */
+ public ConversionException(String message)
+ {
+ super(message);
+ }
+
+ /**
+ * Constructs a new {@code ConversionException} with specified
+ * nested {@code Throwable}.
+ *
+ * @param cause the exception or error that caused this exception to be thrown
+ */
+ public ConversionException(Throwable cause)
+ {
+ super(cause);
+ }
+
+ /**
+ * Constructs a new {@code ConversionException} with specified
+ * detail message and nested {@code Throwable}.
+ *
+ * @param message the error message
+ * @param cause the exception or error that caused this exception to be thrown
+ */
+ public ConversionException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/DataConfiguration.java b/src/main/java/org/apache/commons/configuration/DataConfiguration.java
new file mode 100644
index 0000000..fef8ff2
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/DataConfiguration.java
@@ -0,0 +1,1979 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.awt.Color;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.NoSuchElementException;
+
+import org.apache.commons.lang.ClassUtils;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Decorator providing additional getters for any Configuration. This extended
+ * Configuration supports more types:
+ * <ul>
+ * <li>{@link java.net.URL}</li>
+ * <li>{@link java.util.Locale}</li>
+ * <li>{@link java.util.Date}</li>
+ * <li>{@link java.util.Calendar}</li>
+ * <li>{@link java.awt.Color}</li>
+ * <li>{@link java.net.InetAddress}</li>
+ * <li>{@link javax.mail.internet.InternetAddress} (requires Javamail in the classpath)</li>
+ * <li>{@link java.lang.Enum} (Java 5 enumeration types)</li>
+ * </ul>
+ *
+ * Lists and arrays are available for all types.
+ *
+ * <h4>Example</h4>
+ *
+ * Configuration file <tt>config.properties</tt>:
+ * <pre>
+ * title.color = #0000FF
+ * remote.host = 192.168.0.53
+ * default.locales = fr,en,de
+ * email.contact = ebourg at apache.org, oheger at apache.org
+ * </pre>
+ *
+ * Usage:
+ *
+ * <pre>
+ * DataConfiguration config = new DataConfiguration(new PropertiesConfiguration("config.properties"));
+ *
+ * // retrieve a property using a specialized getter
+ * Color color = config.getColor("title.color");
+ *
+ * // retrieve a property using a generic getter
+ * InetAddress host = (InetAddress) config.get(InetAddress.class, "remote.host");
+ * Locale[] locales = (Locale[]) config.getArray(Locale.class, "default.locales");
+ * List contacts = config.getList(InternetAddress.class, "email.contact");
+ * </pre>
+ *
+ * <h4>Dates</h4>
+ *
+ * Date objects are expected to be formatted with the pattern <tt>yyyy-MM-dd HH:mm:ss</tt>.
+ * This default format can be changed by specifying another format in the
+ * getters, or by putting a date format in the configuration under the key
+ * <tt>org.apache.commons.configuration.format.date</tt>.
+ *
+ * @author <a href="ebourg at apache.org">Emmanuel Bourg</a>
+ * @version $Id: DataConfiguration.java 1234985 2012-01-23 21:09:09Z oheger $
+ * @since 1.1
+ */
+public class DataConfiguration extends AbstractConfiguration implements Serializable
+{
+ /** The key of the property storing the user defined date format. */
+ public static final String DATE_FORMAT_KEY = "org.apache.commons.configuration.format.date";
+
+ /** The default format for dates. */
+ public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
+
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = -69011336405718640L;
+
+ /** Stores the wrapped configuration.*/
+ protected Configuration configuration;
+
+ /**
+ * Creates a new instance of {@code DataConfiguration} and sets the
+ * wrapped configuration.
+ *
+ * @param configuration the wrapped configuration
+ */
+ public DataConfiguration(Configuration configuration)
+ {
+ this.configuration = configuration;
+ }
+
+ /**
+ * Return the configuration decorated by this DataConfiguration.
+ *
+ * @return the wrapped configuration
+ */
+ public Configuration getConfiguration()
+ {
+ return configuration;
+ }
+
+ public Object getProperty(String key)
+ {
+ return configuration.getProperty(key);
+ }
+
+ @Override
+ protected void addPropertyDirect(String key, Object obj)
+ {
+ if (configuration instanceof AbstractConfiguration)
+ {
+ ((AbstractConfiguration) configuration).addPropertyDirect(key, obj);
+ }
+ else
+ {
+ configuration.addProperty(key, obj);
+ }
+ }
+
+ @Override
+ public void addProperty(String key, Object value)
+ {
+ getConfiguration().addProperty(key, value);
+ }
+
+ public boolean isEmpty()
+ {
+ return configuration.isEmpty();
+ }
+
+ public boolean containsKey(String key)
+ {
+ return configuration.containsKey(key);
+ }
+
+ @Override
+ public void clearProperty(String key)
+ {
+ configuration.clearProperty(key);
+ }
+
+ @Override
+ public void setProperty(String key, Object value)
+ {
+ configuration.setProperty(key, value);
+ }
+
+ public Iterator<String> getKeys()
+ {
+ return configuration.getKeys();
+ }
+
+ /**
+ * Get an object of the specified type associated with the given
+ * configuration key. If the key doesn't map to an existing object, the
+ * method returns null unless {@link #isThrowExceptionOnMissing()} is set
+ * to <tt>true</tt>.
+ *
+ * @param <T> the target type of the value
+ * @param cls the target class of the value
+ * @param key the key of the value
+ *
+ * @return the value of the requested type for the key
+ *
+ * @throws NoSuchElementException if the key doesn't map to an existing
+ * object and <tt>throwExceptionOnMissing=true</tt>
+ * @throws ConversionException if the value is not compatible with the requested type
+ *
+ * @since 1.5
+ */
+ public <T> T get(Class<T> cls, String key)
+ {
+ T value = get(cls, key, null);
+ if (value != null)
+ {
+ return value;
+ }
+ else if (isThrowExceptionOnMissing())
+ {
+ throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Get an object of the specified type associated with the given
+ * configuration key. If the key doesn't map to an existing object, the
+ * default value is returned.
+ *
+ * @param <T> the target type of the value
+ * @param cls the target class of the value
+ * @param key the key of the value
+ * @param defaultValue the default value
+ *
+ * @return the value of the requested type for the key
+ *
+ * @throws ConversionException if the value is not compatible with the requested type
+ *
+ * @since 1.5
+ */
+ public <T> T get(Class<T> cls, String key, T defaultValue)
+ {
+ Object value = resolveContainerStore(key);
+
+ if (value == null)
+ {
+ return defaultValue;
+ }
+
+ if (Date.class.equals(cls) || Calendar.class.equals(cls))
+ {
+ return convert(cls, key, interpolate(value), new String[] {getDefaultDateFormat()});
+ }
+ else
+ {
+ return convert(cls, key, interpolate(value), null);
+ }
+ }
+
+ /**
+ * Get a list of typed objects associated with the given configuration key.
+ * If the key doesn't map to an existing object, an empty list is returned.
+ *
+ * @param <T> the type expected for the elements of the list
+ * @param cls the class expected for the elements of the list
+ * @param key The configuration key.
+ * @return The associated list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an object that
+ * is not compatible with a list of the specified class.
+ *
+ * @since 1.5
+ */
+ public <T> List<T> getList(Class<T> cls, String key)
+ {
+ return getList(cls, key, new ArrayList<T>());
+ }
+
+ /**
+ * Get a list of typed objects associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value is
+ * returned.
+ *
+ * @param <T> the type expected for the elements of the list
+ * @param cls the class expected for the elements of the list
+ * @param key the configuration key.
+ * @param defaultValue the default value.
+ * @return The associated List.
+ *
+ * @throws ConversionException is thrown if the key maps to an object that
+ * is not compatible with a list of the specified class.
+ *
+ * @since 1.5
+ */
+ public <T> List<T> getList(Class<T> cls, String key, List<T> defaultValue)
+ {
+ Object value = getProperty(key);
+ Class<?> valueClass = value != null ? value.getClass() : null;
+
+ List<T> list;
+
+ if (value == null || (value instanceof String && StringUtils.isEmpty((String) value)))
+ {
+ // the value is null or is an empty string
+ list = defaultValue;
+ }
+ else
+ {
+ list = new ArrayList<T>();
+
+ Object[] params = null;
+ if (cls.equals(Date.class) || cls.equals(Calendar.class))
+ {
+ params = new Object[] {getDefaultDateFormat()};
+ }
+
+ if (valueClass.isArray())
+ {
+ // get the class of the objects contained in the array
+ Class<?> arrayType = valueClass.getComponentType();
+ int length = Array.getLength(value);
+
+ if (arrayType.equals(cls)
+ || (arrayType.isPrimitive() && cls.equals(ClassUtils.primitiveToWrapper(arrayType))))
+ {
+ // the value is an array of the specified type, or an array
+ // of the primitive type derived from the specified type
+ for (int i = 0; i < length; i++)
+ {
+ list.add(cls.cast(Array.get(value, i)));
+ }
+ }
+ else
+ {
+ // attempt to convert the elements of the array
+ for (int i = 0; i < length; i++)
+ {
+ list.add(convert(cls, key, interpolate(Array.get(value, i)), params));
+ }
+ }
+ }
+ else if (value instanceof Collection)
+ {
+ Collection<?> values = (Collection<?>) value;
+
+ for (Object o : values)
+ {
+ list.add(convert(cls, key, interpolate(o), params));
+ }
+ }
+ else
+ {
+ // attempt to convert a single value
+ list.add(convert(cls, key, interpolate(value), params));
+ }
+ }
+
+ return list;
+ }
+
+ /**
+ * Get an array of typed objects associated with the given configuration key.
+ * If the key doesn't map to an existing object, an empty list is returned.
+ *
+ * @param cls the type expected for the elements of the array
+ * @param key The configuration key.
+ * @return The associated array if the key is found, and the value compatible with the type specified.
+ *
+ * @throws ConversionException is thrown if the key maps to an object that
+ * is not compatible with a list of the specified class.
+ *
+ * @since 1.5
+ */
+ public Object getArray(Class<?> cls, String key)
+ {
+ return getArray(cls, key, Array.newInstance(cls, 0));
+ }
+
+ /**
+ * Get an array of typed objects associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value is returned.
+ *
+ * @param cls the type expected for the elements of the array
+ * @param key the configuration key.
+ * @param defaultValue the default value
+ * @return The associated array if the key is found, and the value compatible with the type specified.
+ *
+ * @throws ConversionException is thrown if the key maps to an object that
+ * is not compatible with an array of the specified class.
+ * @throws IllegalArgumentException if the default value is not an array of the specified type
+ *
+ * @since 1.5
+ */
+ public Object getArray(Class<?> cls, String key, Object defaultValue)
+ {
+ // check the type of the default value
+ if (defaultValue != null
+ && (!defaultValue.getClass().isArray() || !cls
+ .isAssignableFrom(defaultValue.getClass()
+ .getComponentType())))
+ {
+ throw new IllegalArgumentException(
+ "The type of the default value (" + defaultValue.getClass()
+ + ")" + " is not an array of the specified class ("
+ + cls + ")");
+ }
+
+ if (cls.isPrimitive())
+ {
+ return getPrimitiveArray(cls, key, defaultValue);
+ }
+
+ List<?> list = getList(cls, key);
+ if (list.isEmpty())
+ {
+ return defaultValue;
+ }
+ else
+ {
+ return list.toArray((Object[]) Array.newInstance(cls, list.size()));
+ }
+ }
+
+ /**
+ * Get an array of primitive values associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value is returned.
+ *
+ * @param cls the primitive type expected for the elements of the array
+ * @param key the configuration key.
+ * @param defaultValue the default value
+ * @return The associated array if the key is found, and the value compatible with the type specified.
+ *
+ * @throws ConversionException is thrown if the key maps to an object that
+ * is not compatible with an array of the specified class.
+ *
+ * @since 1.5
+ */
+ private Object getPrimitiveArray(Class<?> cls, String key, Object defaultValue)
+ {
+ Object value = getProperty(key);
+ Class<?> valueClass = value != null ? value.getClass() : null;
+
+ Object array;
+
+ if (value == null || (value instanceof String && StringUtils.isEmpty((String) value)))
+ {
+ // the value is null or is an empty string
+ array = defaultValue;
+ }
+ else
+ {
+ if (valueClass.isArray())
+ {
+ // get the class of the objects contained in the array
+ Class<?> arrayType = valueClass.getComponentType();
+ int length = Array.getLength(value);
+
+ if (arrayType.equals(cls))
+ {
+ // the value is an array of the same primitive type
+ array = value;
+ }
+ else if (arrayType.equals(ClassUtils.primitiveToWrapper(cls)))
+ {
+ // the value is an array of the wrapper type derived from the specified primitive type
+ array = Array.newInstance(cls, length);
+
+ for (int i = 0; i < length; i++)
+ {
+ Array.set(array, i, Array.get(value, i));
+ }
+ }
+ else
+ {
+ throw new ConversionException('\'' + key + "' (" + arrayType + ")"
+ + " doesn't map to a compatible array of " + cls);
+ }
+ }
+ else if (value instanceof Collection)
+ {
+ Collection<?> values = (Collection<?>) value;
+
+ array = Array.newInstance(cls, values.size());
+
+ int i = 0;
+ for (Object o : values)
+ {
+ // This is safe because PropertyConverter can handle
+ // conversion to wrapper classes correctly.
+ @SuppressWarnings("unchecked")
+ Object convertedValue = convert(ClassUtils.primitiveToWrapper(cls), key, interpolate(o), null);
+ Array.set(array, i++, convertedValue);
+ }
+ }
+ else
+ {
+ // attempt to convert a single value
+ // This is safe because PropertyConverter can handle
+ // conversion to wrapper classes correctly.
+ @SuppressWarnings("unchecked")
+ Object convertedValue = convert(ClassUtils.primitiveToWrapper(cls), key, interpolate(value), null);
+
+ // create an array of one element
+ array = Array.newInstance(cls, 1);
+ Array.set(array, 0, convertedValue);
+ }
+ }
+
+ return array;
+ }
+
+ /**
+ * Get a list of Boolean objects associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty list is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated Boolean list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of booleans.
+ */
+ public List<Boolean> getBooleanList(String key)
+ {
+ return getBooleanList(key, new ArrayList<Boolean>());
+ }
+
+ /**
+ * Get a list of Boolean objects associated with the given
+ * configuration key. If the key doesn't map to an existing object,
+ * the default value is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated List of Booleans.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of booleans.
+ */
+ public List<Boolean> getBooleanList(String key, List<Boolean> defaultValue)
+ {
+ return getList(Boolean.class, key, defaultValue);
+ }
+
+ /**
+ * Get an array of boolean primitives associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated boolean array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of booleans.
+ */
+ public boolean[] getBooleanArray(String key)
+ {
+ return (boolean[]) getArray(Boolean.TYPE, key);
+ }
+
+ /**
+ * Get an array of boolean primitives associated with the given
+ * configuration key. If the key doesn't map to an existing object,
+ * the default value is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated boolean array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of booleans.
+ */
+ public boolean[] getBooleanArray(String key, boolean[] defaultValue)
+ {
+ return (boolean[]) getArray(Boolean.TYPE, key, defaultValue);
+ }
+
+ /**
+ * Get a list of Byte objects associated with the given configuration key.
+ * If the key doesn't map to an existing object an empty list is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated Byte list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of bytes.
+ */
+ public List<Byte> getByteList(String key)
+ {
+ return getByteList(key, new ArrayList<Byte>());
+ }
+
+ /**
+ * Get a list of Byte objects associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value is
+ * returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated List of Bytes.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of bytes.
+ */
+ public List<Byte> getByteList(String key, List<Byte> defaultValue)
+ {
+ return getList(Byte.class, key, defaultValue);
+ }
+
+ /**
+ * Get an array of byte primitives associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated byte array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of bytes.
+ */
+ public byte[] getByteArray(String key)
+ {
+ return getByteArray(key, new byte[0]);
+ }
+
+ /**
+ * Get an array of byte primitives associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue the default value, which will be returned if the property is not found
+ * @return The associated byte array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of bytes.
+ */
+ public byte[] getByteArray(String key, byte[] defaultValue)
+ {
+ return (byte[]) getArray(Byte.TYPE, key, defaultValue);
+ }
+
+ /**
+ * Get a list of Short objects associated with the given configuration key.
+ * If the key doesn't map to an existing object an empty list is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated Short list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of shorts.
+ */
+ public List<Short> getShortList(String key)
+ {
+ return getShortList(key, new ArrayList<Short>());
+ }
+
+ /**
+ * Get a list of Short objects associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value is
+ * returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated List of Shorts.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of shorts.
+ */
+ public List<Short> getShortList(String key, List<Short> defaultValue)
+ {
+ return getList(Short.class, key, defaultValue);
+ }
+
+ /**
+ * Get an array of short primitives associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated short array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of shorts.
+ */
+ public short[] getShortArray(String key)
+ {
+ return getShortArray(key, new short[0]);
+ }
+
+ /**
+ * Get an array of short primitives associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue the default value, which will be returned if the property is not found
+ * @return The associated short array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of shorts.
+ */
+ public short[] getShortArray(String key, short[] defaultValue)
+ {
+ return (short[]) getArray(Short.TYPE, key, defaultValue);
+ }
+
+ /**
+ * Get a list of Integer objects associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty list is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated Integer list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of integers.
+ */
+ public List<Integer> getIntegerList(String key)
+ {
+ return getIntegerList(key, new ArrayList<Integer>());
+ }
+
+ /**
+ * Get a list of Integer objects associated with the given
+ * configuration key. If the key doesn't map to an existing object,
+ * the default value is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated List of Integers.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of integers.
+ */
+ public List<Integer> getIntegerList(String key, List<Integer> defaultValue)
+ {
+ return getList(Integer.class, key, defaultValue);
+ }
+
+ /**
+ * Get an array of int primitives associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated int array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of integers.
+ */
+ public int[] getIntArray(String key)
+ {
+ return getIntArray(key, new int[0]);
+ }
+
+ /**
+ * Get an array of int primitives associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue the default value, which will be returned if the property is not found
+ * @return The associated int array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of integers.
+ */
+ public int[] getIntArray(String key, int[] defaultValue)
+ {
+ return (int[]) getArray(Integer.TYPE, key, defaultValue);
+ }
+
+ /**
+ * Get a list of Long objects associated with the given configuration key.
+ * If the key doesn't map to an existing object an empty list is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated Long list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of longs.
+ */
+ public List<Long> getLongList(String key)
+ {
+ return getLongList(key, new ArrayList<Long>());
+ }
+
+ /**
+ * Get a list of Long objects associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value is
+ * returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated List of Longs.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of longs.
+ */
+ public List<Long> getLongList(String key, List<Long> defaultValue)
+ {
+ return getList(Long.class, key, defaultValue);
+ }
+
+ /**
+ * Get an array of long primitives associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated long array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of longs.
+ */
+ public long[] getLongArray(String key)
+ {
+ return getLongArray(key, new long[0]);
+ }
+
+ /**
+ * Get an array of long primitives associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue the default value, which will be returned if the property is not found
+ * @return The associated long array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of longs.
+ */
+ public long[] getLongArray(String key, long[] defaultValue)
+ {
+ return (long[]) getArray(Long.TYPE, key, defaultValue);
+ }
+
+ /**
+ * Get a list of Float objects associated with the given configuration key.
+ * If the key doesn't map to an existing object an empty list is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated Float list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of floats.
+ */
+ public List<Float> getFloatList(String key)
+ {
+ return getFloatList(key, new ArrayList<Float>());
+ }
+
+ /**
+ * Get a list of Float objects associated with the given
+ * configuration key. If the key doesn't map to an existing object,
+ * the default value is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated List of Floats.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of floats.
+ */
+ public List<Float> getFloatList(String key, List<Float> defaultValue)
+ {
+ return getList(Float.class, key, defaultValue);
+ }
+
+ /**
+ * Get an array of float primitives associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated float array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of floats.
+ */
+ public float[] getFloatArray(String key)
+ {
+ return getFloatArray(key, new float[0]);
+ }
+
+ /**
+ * Get an array of float primitives associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue the default value, which will be returned if the property is not found
+ * @return The associated float array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of floats.
+ */
+ public float[] getFloatArray(String key, float[] defaultValue)
+ {
+ return (float[]) getArray(Float.TYPE, key, defaultValue);
+ }
+
+ /**
+ * Get a list of Double objects associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty list is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated Double list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of doubles.
+ */
+ public List<Double> getDoubleList(String key)
+ {
+ return getDoubleList(key, new ArrayList<Double>());
+ }
+
+ /**
+ * Get a list of Double objects associated with the given
+ * configuration key. If the key doesn't map to an existing object,
+ * the default value is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated List of Doubles.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of doubles.
+ */
+ public List<Double> getDoubleList(String key, List<Double> defaultValue)
+ {
+ return getList(Double.class, key, defaultValue);
+ }
+
+ /**
+ * Get an array of double primitives associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated double array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of doubles.
+ */
+ public double[] getDoubleArray(String key)
+ {
+ return getDoubleArray(key, new double[0]);
+ }
+
+ /**
+ * Get an array of double primitives associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue the default value, which will be returned if the property is not found
+ * @return The associated double array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of doubles.
+ */
+ public double[] getDoubleArray(String key, double[] defaultValue)
+ {
+ return (double[]) getArray(Double.TYPE, key, defaultValue);
+ }
+
+ /**
+ * Get a list of BigIntegers associated with the given configuration key.
+ * If the key doesn't map to an existing object an empty list is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated BigInteger list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of BigIntegers.
+ */
+ public List<BigInteger> getBigIntegerList(String key)
+ {
+ return getBigIntegerList(key, new ArrayList<BigInteger>());
+ }
+
+ /**
+ * Get a list of BigIntegers associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value is
+ * returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated List of BigIntegers.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of BigIntegers.
+ */
+ public List<BigInteger> getBigIntegerList(String key, List<BigInteger> defaultValue)
+ {
+ return getList(BigInteger.class, key, defaultValue);
+ }
+
+ /**
+ * Get an array of BigIntegers associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated BigInteger array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of BigIntegers.
+ */
+ public BigInteger[] getBigIntegerArray(String key)
+ {
+ return getBigIntegerArray(key, new BigInteger[0]);
+ }
+
+ /**
+ * Get an array of BigIntegers associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue the default value, which will be returned if the property is not found
+ * @return The associated BigInteger array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of BigIntegers.
+ */
+ public BigInteger[] getBigIntegerArray(String key, BigInteger[] defaultValue)
+ {
+ return (BigInteger[]) getArray(BigInteger.class, key, defaultValue);
+ }
+
+ /**
+ * Get a list of BigDecimals associated with the given configuration key.
+ * If the key doesn't map to an existing object an empty list is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated BigDecimal list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of BigDecimals.
+ */
+ public List<BigDecimal> getBigDecimalList(String key)
+ {
+ return getBigDecimalList(key, new ArrayList<BigDecimal>());
+ }
+
+ /**
+ * Get a list of BigDecimals associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value is
+ * returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated List of BigDecimals.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of BigDecimals.
+ */
+ public List<BigDecimal> getBigDecimalList(String key, List<BigDecimal> defaultValue)
+ {
+ return getList(BigDecimal.class, key, defaultValue);
+ }
+
+ /**
+ * Get an array of BigDecimals associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated BigDecimal array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of BigDecimals.
+ */
+ public BigDecimal[] getBigDecimalArray(String key)
+ {
+ return getBigDecimalArray(key, new BigDecimal[0]);
+ }
+
+ /**
+ * Get an array of BigDecimals associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue the default value, which will be returned if the property is not found
+ * @return The associated BigDecimal array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of BigDecimals.
+ */
+ public BigDecimal[] getBigDecimalArray(String key, BigDecimal[] defaultValue)
+ {
+ return (BigDecimal[]) getArray(BigDecimal.class, key, defaultValue);
+ }
+
+ /**
+ * Get an URL associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @return The associated URL.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not an URL.
+ */
+ public URL getURL(String key)
+ {
+ return get(URL.class, key);
+ }
+
+ /**
+ * Get an URL associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated URL.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not an URL.
+ */
+ public URL getURL(String key, URL defaultValue)
+ {
+ return get(URL.class, key, defaultValue);
+ }
+
+ /**
+ * Get a list of URLs associated with the given configuration key.
+ * If the key doesn't map to an existing object an empty list is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated URL list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of URLs.
+ */
+ public List<URL> getURLList(String key)
+ {
+ return getURLList(key, new ArrayList<URL>());
+ }
+
+ /**
+ * Get a list of URLs associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value is
+ * returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated List of URLs.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of URLs.
+ */
+ public List<URL> getURLList(String key, List<URL> defaultValue)
+ {
+ return getList(URL.class, key, defaultValue);
+ }
+
+ /**
+ * Get an array of URLs associated with the given configuration key.
+ * If the key doesn't map to an existing object an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated URL array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of URLs.
+ */
+ public URL[] getURLArray(String key)
+ {
+ return getURLArray(key, new URL[0]);
+ }
+
+ /**
+ * Get an array of URLs associated with the given configuration key.
+ * If the key doesn't map to an existing object an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue the default value, which will be returned if the property is not found
+ * @return The associated URL array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of URLs.
+ */
+ public URL[] getURLArray(String key, URL[] defaultValue)
+ {
+ return (URL[]) getArray(URL.class, key, defaultValue);
+ }
+
+ /**
+ * Get a Date associated with the given configuration key. If the property
+ * is a String, it will be parsed with the format defined by the user in
+ * the {@link #DATE_FORMAT_KEY} property, or if it's not defined with the
+ * {@link #DEFAULT_DATE_FORMAT} pattern.
+ *
+ * @param key The configuration key.
+ * @return The associated Date.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Date.
+ */
+ public Date getDate(String key)
+ {
+ return get(Date.class, key);
+ }
+
+ /**
+ * Get a Date associated with the given configuration key. If the property
+ * is a String, it will be parsed with the specified format pattern.
+ *
+ * @param key The configuration key.
+ * @param format The non-localized {@link java.text.DateFormat} pattern.
+ * @return The associated Date
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Date.
+ */
+ public Date getDate(String key, String format)
+ {
+ Date value = getDate(key, null, format);
+ if (value != null)
+ {
+ return value;
+ }
+ else if (isThrowExceptionOnMissing())
+ {
+ throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Get a Date associated with the given configuration key. If the property
+ * is a String, it will be parsed with the format defined by the user in
+ * the {@link #DATE_FORMAT_KEY} property, or if it's not defined with the
+ * {@link #DEFAULT_DATE_FORMAT} pattern. If the key doesn't map to an
+ * existing object, the default value is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated Date.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Date.
+ */
+ public Date getDate(String key, Date defaultValue)
+ {
+ return getDate(key, defaultValue, getDefaultDateFormat());
+ }
+
+ /**
+ * Get a Date associated with the given configuration key. If the property
+ * is a String, it will be parsed with the specified format pattern.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @param format The non-localized {@link java.text.DateFormat} pattern.
+ * @return The associated Date.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Date.
+ */
+ public Date getDate(String key, Date defaultValue, String format)
+ {
+ Object value = resolveContainerStore(key);
+
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ else
+ {
+ try
+ {
+ return PropertyConverter.toDate(interpolate(value), format);
+ }
+ catch (ConversionException e)
+ {
+ throw new ConversionException('\'' + key + "' doesn't map to a Date", e);
+ }
+ }
+ }
+ public List<Date> getDateList(String key)
+ {
+ return getDateList(key, new ArrayList<Date>());
+ }
+
+ /**
+ * Get a list of Dates associated with the given configuration key.
+ * If the property is a list of Strings, they will be parsed with the
+ * specified format pattern. If the key doesn't map to an existing object
+ * an empty list is returned.
+ *
+ * @param key The configuration key.
+ * @param format The non-localized {@link java.text.DateFormat} pattern.
+ * @return The associated Date list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Dates.
+ */
+ public List<Date> getDateList(String key, String format)
+ {
+ return getDateList(key, new ArrayList<Date>(), format);
+ }
+
+ /**
+ * Get a list of Dates associated with the given configuration key.
+ * If the property is a list of Strings, they will be parsed with the
+ * format defined by the user in the {@link #DATE_FORMAT_KEY} property,
+ * or if it's not defined with the {@link #DEFAULT_DATE_FORMAT} pattern.
+ * If the key doesn't map to an existing object, the default value is
+ * returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated Date list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Dates.
+ */
+ public List<Date> getDateList(String key, List<Date> defaultValue)
+ {
+ return getDateList(key, defaultValue, getDefaultDateFormat());
+ }
+
+ /**
+ * Get a list of Dates associated with the given configuration key.
+ * If the property is a list of Strings, they will be parsed with the
+ * specified format pattern. If the key doesn't map to an existing object,
+ * the default value is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @param format The non-localized {@link java.text.DateFormat} pattern.
+ * @return The associated Date list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Dates.
+ */
+ public List<Date> getDateList(String key, List<Date> defaultValue, String format)
+ {
+ Object value = getProperty(key);
+
+ List<Date> list;
+
+ if (value == null || (value instanceof String && StringUtils.isEmpty((String) value)))
+ {
+ list = defaultValue;
+ }
+ else if (value.getClass().isArray())
+ {
+ list = new ArrayList<Date>();
+ int length = Array.getLength(value);
+ for (int i = 0; i < length; i++)
+ {
+ list.add(convert(Date.class, key, interpolate(Array.get(value, i)), new String[] {format}));
+ }
+ }
+ else if (value instanceof Collection)
+ {
+ Collection<?> values = (Collection<?>) value;
+ list = new ArrayList<Date>();
+
+ for (Object o : values)
+ {
+ list.add(convert(Date.class, key, interpolate(o), new String[] {format}));
+ }
+ }
+ else
+ {
+ // attempt to convert a single value
+ list = new ArrayList<Date>();
+ list.add(convert(Date.class, key, interpolate(value), new String[] {format}));
+ }
+
+ return list;
+ }
+
+ /**
+ * Get an array of Dates associated with the given configuration key.
+ * If the property is a list of Strings, they will be parsed with the
+ * format defined by the user in the {@link #DATE_FORMAT_KEY} property,
+ * or if it's not defined with the {@link #DEFAULT_DATE_FORMAT} pattern.
+ * If the key doesn't map to an existing object an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated Date array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Dates.
+ */
+ public Date[] getDateArray(String key)
+ {
+ return getDateArray(key, new Date[0]);
+ }
+
+ /**
+ * Get an array of Dates associated with the given configuration key.
+ * If the property is a list of Strings, they will be parsed with the
+ * specified format pattern. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @param format The non-localized {@link java.text.DateFormat} pattern.
+ * @return The associated Date array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Dates.
+ */
+ public Date[] getDateArray(String key, String format)
+ {
+ return getDateArray(key, new Date[0], format);
+ }
+
+ /**
+ * Get an array of Dates associated with the given configuration key.
+ * If the property is a list of Strings, they will be parsed with the
+ * format defined by the user in the {@link #DATE_FORMAT_KEY} property,
+ * or if it's not defined with the {@link #DEFAULT_DATE_FORMAT} pattern.
+ * If the key doesn't map to an existing object an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue the default value, which will be returned if the property is not found
+ * @return The associated Date array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Dates.
+ */
+ public Date[] getDateArray(String key, Date[] defaultValue)
+ {
+ return getDateArray(key, defaultValue, getDefaultDateFormat());
+ }
+
+ /**
+ * Get an array of Dates associated with the given configuration key.
+ * If the property is a list of Strings, they will be parsed with the
+ * specified format pattern. If the key doesn't map to an existing object,
+ * the default value is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @param format The non-localized {@link java.text.DateFormat} pattern.
+ * @return The associated Date array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Dates.
+ */
+ public Date[] getDateArray(String key, Date[] defaultValue, String format)
+ {
+ List<Date> list = getDateList(key, format);
+ if (list.isEmpty())
+ {
+ return defaultValue;
+ }
+ else
+ {
+ return list.toArray(new Date[list.size()]);
+ }
+ }
+
+ /**
+ * Get a Calendar associated with the given configuration key. If the
+ * property is a String, it will be parsed with the format defined by the
+ * user in the {@link #DATE_FORMAT_KEY} property, or if it's not defined
+ * with the {@link #DEFAULT_DATE_FORMAT} pattern.
+ *
+ * @param key The configuration key.
+ * @return The associated Calendar.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Calendar.
+ */
+ public Calendar getCalendar(String key)
+ {
+ return get(Calendar.class, key);
+ }
+
+ /**
+ * Get a Calendar associated with the given configuration key. If the
+ * property is a String, it will be parsed with the specified format
+ * pattern.
+ *
+ * @param key The configuration key.
+ * @param format The non-localized {@link java.text.DateFormat} pattern.
+ * @return The associated Calendar
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Calendar.
+ */
+ public Calendar getCalendar(String key, String format)
+ {
+ Calendar value = getCalendar(key, null, format);
+ if (value != null)
+ {
+ return value;
+ }
+ else if (isThrowExceptionOnMissing())
+ {
+ throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Get a Calendar associated with the given configuration key. If the
+ * property is a String, it will be parsed with the format defined by the
+ * user in the {@link #DATE_FORMAT_KEY} property, or if it's not defined
+ * with the {@link #DEFAULT_DATE_FORMAT} pattern. If the key doesn't map
+ * to an existing object, the default value is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated Calendar.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Calendar.
+ */
+ public Calendar getCalendar(String key, Calendar defaultValue)
+ {
+ return getCalendar(key, defaultValue, getDefaultDateFormat());
+ }
+
+ /**
+ * Get a Calendar associated with the given configuration key. If the
+ * property is a String, it will be parsed with the specified format
+ * pattern. If the key doesn't map to an existing object, the default
+ * value is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @param format The non-localized {@link java.text.DateFormat} pattern.
+ * @return The associated Calendar.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Calendar.
+ */
+ public Calendar getCalendar(String key, Calendar defaultValue, String format)
+ {
+ Object value = resolveContainerStore(key);
+
+ if (value == null)
+ {
+ return defaultValue;
+ }
+ else
+ {
+ try
+ {
+ return PropertyConverter.toCalendar(interpolate(value), format);
+ }
+ catch (ConversionException e)
+ {
+ throw new ConversionException('\'' + key + "' doesn't map to a Calendar", e);
+ }
+ }
+ }
+
+ /**
+ * Get a list of Calendars associated with the given configuration key.
+ * If the property is a list of Strings, they will be parsed with the
+ * format defined by the user in the {@link #DATE_FORMAT_KEY} property,
+ * or if it's not defined with the {@link #DEFAULT_DATE_FORMAT} pattern.
+ * If the key doesn't map to an existing object an empty list is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated Calendar list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Calendars.
+ */
+ public List<Calendar> getCalendarList(String key)
+ {
+ return getCalendarList(key, new ArrayList<Calendar>());
+ }
+
+ /**
+ * Get a list of Calendars associated with the given configuration key.
+ * If the property is a list of Strings, they will be parsed with the
+ * specified format pattern. If the key doesn't map to an existing object
+ * an empty list is returned.
+ *
+ * @param key The configuration key.
+ * @param format The non-localized {@link java.text.DateFormat} pattern.
+ * @return The associated Calendar list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Calendars.
+ */
+ public List<Calendar> getCalendarList(String key, String format)
+ {
+ return getCalendarList(key, new ArrayList<Calendar>(), format);
+ }
+
+ /**
+ * Get a list of Calendars associated with the given configuration key.
+ * If the property is a list of Strings, they will be parsed with the
+ * format defined by the user in the {@link #DATE_FORMAT_KEY} property,
+ * or if it's not defined with the {@link #DEFAULT_DATE_FORMAT} pattern.
+ * If the key doesn't map to an existing object, the default value is
+ * returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated Calendar list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Calendars.
+ */
+ public List<Calendar> getCalendarList(String key, List<Calendar> defaultValue)
+ {
+ return getCalendarList(key, defaultValue, getDefaultDateFormat());
+ }
+
+ /**
+ * Get a list of Calendars associated with the given configuration key.
+ * If the property is a list of Strings, they will be parsed with the
+ * specified format pattern. If the key doesn't map to an existing object,
+ * the default value is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @param format The non-localized {@link java.text.DateFormat} pattern.
+ * @return The associated Calendar list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Calendars.
+ */
+ public List<Calendar> getCalendarList(String key, List<Calendar> defaultValue, String format)
+ {
+ Object value = getProperty(key);
+
+ List<Calendar> list;
+
+ if (value == null || (value instanceof String && StringUtils.isEmpty((String) value)))
+ {
+ list = defaultValue;
+ }
+ else if (value.getClass().isArray())
+ {
+ list = new ArrayList<Calendar>();
+ int length = Array.getLength(value);
+ for (int i = 0; i < length; i++)
+ {
+ list.add(convert(Calendar.class, key, interpolate(Array.get(value, i)), new String[] {format}));
+ }
+ }
+ else if (value instanceof Collection)
+ {
+ Collection<?> values = (Collection<?>) value;
+ list = new ArrayList<Calendar>();
+
+ for (Object o : values)
+ {
+ list.add(convert(Calendar.class, key, interpolate(o), new String[] {format}));
+ }
+ }
+ else
+ {
+ // attempt to convert a single value
+ list = new ArrayList<Calendar>();
+ list.add(convert(Calendar.class, key, interpolate(value), new String[] {format}));
+ }
+
+ return list;
+ }
+
+ /**
+ * Get an array of Calendars associated with the given configuration key.
+ * If the property is a list of Strings, they will be parsed with the
+ * format defined by the user in the {@link #DATE_FORMAT_KEY} property,
+ * or if it's not defined with the {@link #DEFAULT_DATE_FORMAT} pattern.
+ * If the key doesn't map to an existing object an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated Calendar array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Calendars.
+ */
+ public Calendar[] getCalendarArray(String key)
+ {
+ return getCalendarArray(key, new Calendar[0]);
+ }
+
+ /**
+ * Get an array of Calendars associated with the given configuration key.
+ * If the property is a list of Strings, they will be parsed with the
+ * specified format pattern. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @param format The non-localized {@link java.text.DateFormat} pattern.
+ * @return The associated Calendar array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Calendars.
+ */
+ public Calendar[] getCalendarArray(String key, String format)
+ {
+ return getCalendarArray(key, new Calendar[0], format);
+ }
+
+ /**
+ * Get an array of Calendars associated with the given configuration key.
+ * If the property is a list of Strings, they will be parsed with the
+ * format defined by the user in the {@link #DATE_FORMAT_KEY} property,
+ * or if it's not defined with the {@link #DEFAULT_DATE_FORMAT} pattern.
+ * If the key doesn't map to an existing object an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue the default value, which will be returned if the property is not found
+ * @return The associated Calendar array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Calendars.
+ */
+ public Calendar[] getCalendarArray(String key, Calendar[] defaultValue)
+ {
+ return getCalendarArray(key, defaultValue, getDefaultDateFormat());
+ }
+
+ /**
+ * Get an array of Calendars associated with the given configuration key.
+ * If the property is a list of Strings, they will be parsed with the
+ * specified format pattern. If the key doesn't map to an existing object,
+ * the default value is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @param format The non-localized {@link java.text.DateFormat} pattern.
+ * @return The associated Calendar array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Calendars.
+ */
+ public Calendar[] getCalendarArray(String key, Calendar[] defaultValue, String format)
+ {
+ List<Calendar> list = getCalendarList(key, format);
+ if (list.isEmpty())
+ {
+ return defaultValue;
+ }
+ else
+ {
+ return list.toArray(new Calendar[list.size()]);
+ }
+ }
+
+ /**
+ * Returns the date format specified by the user in the DATE_FORMAT_KEY
+ * property, or the default format otherwise.
+ *
+ * @return the default date format
+ */
+ private String getDefaultDateFormat()
+ {
+ return getString(DATE_FORMAT_KEY, DEFAULT_DATE_FORMAT);
+ }
+
+ /**
+ * Get a Locale associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @return The associated Locale.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Locale.
+ */
+ public Locale getLocale(String key)
+ {
+ return get(Locale.class, key);
+ }
+
+ /**
+ * Get a Locale associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated Locale.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Locale.
+ */
+ public Locale getLocale(String key, Locale defaultValue)
+ {
+ return get(Locale.class, key, defaultValue);
+ }
+
+ /**
+ * Get a list of Locales associated with the given configuration key.
+ * If the key doesn't map to an existing object an empty list is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated Locale list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Locales.
+ */
+ public List<Locale> getLocaleList(String key)
+ {
+ return getLocaleList(key, new ArrayList<Locale>());
+ }
+
+ /**
+ * Get a list of Locales associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value is
+ * returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated List of Locales.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Locales.
+ */
+ public List<Locale> getLocaleList(String key, List<Locale> defaultValue)
+ {
+ return getList(Locale.class, key, defaultValue);
+ }
+
+ /**
+ * Get an array of Locales associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated Locale array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Locales.
+ */
+ public Locale[] getLocaleArray(String key)
+ {
+ return getLocaleArray(key, new Locale[0]);
+ }
+
+ /**
+ * Get an array of Locales associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue the default value, which will be returned if the property is not found
+ * @return The associated Locale array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Locales.
+ */
+ public Locale[] getLocaleArray(String key, Locale[] defaultValue)
+ {
+ return (Locale[]) getArray(Locale.class, key, defaultValue);
+ }
+
+ /**
+ * Get a Color associated with the given configuration key.
+ *
+ * @param key The configuration key.
+ * @return The associated Color.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Color.
+ */
+ public Color getColor(String key)
+ {
+ return get(Color.class, key);
+ }
+
+ /**
+ * Get a Color associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value
+ * is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated Color.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a Color.
+ */
+ public Color getColor(String key, Color defaultValue)
+ {
+ return get(Color.class, key, defaultValue);
+ }
+
+ /**
+ * Get a list of Colors associated with the given configuration key.
+ * If the key doesn't map to an existing object an empty list is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated Color list if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Colors.
+ */
+ public List<Color> getColorList(String key)
+ {
+ return getColorList(key, new ArrayList<Color>());
+ }
+
+ /**
+ * Get a list of Colors associated with the given configuration key.
+ * If the key doesn't map to an existing object, the default value is
+ * returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue The default value.
+ * @return The associated List of Colors.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Colors.
+ */
+ public List<Color> getColorList(String key, List<Color> defaultValue)
+ {
+ return getList(Color.class, key, defaultValue);
+ }
+
+ /**
+ * Get an array of Colors associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @return The associated Color array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Colors.
+ */
+ public Color[] getColorArray(String key)
+ {
+ return getColorArray(key, new Color[0]);
+ }
+
+ /**
+ * Get an array of Colors associated with the given
+ * configuration key. If the key doesn't map to an existing object
+ * an empty array is returned.
+ *
+ * @param key The configuration key.
+ * @param defaultValue the default value, which will be returned if the property is not found
+ * @return The associated Color array if the key is found.
+ *
+ * @throws ConversionException is thrown if the key maps to an
+ * object that is not a list of Colors.
+ */
+ public Color[] getColorArray(String key, Color[] defaultValue)
+ {
+ return (Color[]) getArray(Color.class, key, defaultValue);
+ }
+
+ /**
+ * Helper method for performing a type conversion using the
+ * {@code PropertyConverter} class.
+ *
+ * @param <T> the target type of the conversion
+ * @param cls the target class of the conversion
+ * @param key the configuration key
+ * @param value the value to be converted
+ * @param params additional parameters
+ * @throws ConversionException if the value is not compatible with the
+ * requested type
+ */
+ private static <T> T convert(Class<T> cls, String key, Object value,
+ Object[] params)
+ {
+ try
+ {
+ Object result = PropertyConverter.to(cls, value, params);
+ // Will not throw a ClassCastException because PropertyConverter
+ // would have thrown a ConversionException if conversion had failed.
+ return cls.cast(result);
+ }
+ catch (ConversionException e)
+ {
+ throw new ConversionException('\'' + key + "' doesn't map to a "
+ + cls, e);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/DatabaseConfiguration.java b/src/main/java/org/apache/commons/configuration/DatabaseConfiguration.java
new file mode 100644
index 0000000..28828ec
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/DatabaseConfiguration.java
@@ -0,0 +1,709 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.sql.DataSource;
+
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Configuration stored in a database. The properties are retrieved from a
+ * table containing at least one column for the keys, and one column for the
+ * values. It's possible to store several configurations in the same table by
+ * adding a column containing the name of the configuration. The name of the
+ * table and the columns is specified in the constructor.
+ *
+ * <h4>Example 1 - One configuration per table</h4>
+ *
+ * <pre>
+ * CREATE TABLE myconfig (
+ * `key` VARCHAR NOT NULL PRIMARY KEY,
+ * `value` VARCHAR
+ * );
+ *
+ * INSERT INTO myconfig (key, value) VALUES ('foo', 'bar');
+ *
+ *
+ * Configuration config = new DatabaseConfiguration(datasource, "myconfig", "key", "value");
+ * String value = config.getString("foo");
+ * </pre>
+ *
+ * <h4>Example 2 - Multiple configurations per table</h4>
+ *
+ * <pre>
+ * CREATE TABLE myconfigs (
+ * `name` VARCHAR NOT NULL,
+ * `key` VARCHAR NOT NULL,
+ * `value` VARCHAR,
+ * CONSTRAINT sys_pk_myconfigs PRIMARY KEY (`name`, `key`)
+ * );
+ *
+ * INSERT INTO myconfigs (name, key, value) VALUES ('config1', 'key1', 'value1');
+ * INSERT INTO myconfigs (name, key, value) VALUES ('config2', 'key2', 'value2');
+ *
+ *
+ * Configuration config1 = new DatabaseConfiguration(datasource, "myconfigs", "name", "key", "value", "config1");
+ * String value1 = conf.getString("key1");
+ *
+ * Configuration config2 = new DatabaseConfiguration(datasource, "myconfigs", "name", "key", "value", "config2");
+ * String value2 = conf.getString("key2");
+ * </pre>
+ * The configuration can be instructed to perform commits after database updates.
+ * This is achieved by setting the {@code commits} parameter of the
+ * constructors to <b>true</b>. If commits should not be performed (which is the
+ * default behavior), it should be ensured that the connections returned by the
+ * {@code DataSource} are in auto-commit mode.
+ *
+ * <h1>Note: Like JDBC itself, protection against SQL injection is left to the user.</h1>
+ * @since 1.0
+ *
+ * @author <a href="mailto:ebourg at apache.org">Emmanuel Bourg</a>
+ * @version $Id: DatabaseConfiguration.java 1344442 2012-05-30 20:17:35Z oheger $
+ */
+public class DatabaseConfiguration extends AbstractConfiguration
+{
+ /** The datasource to connect to the database. */
+ private final DataSource datasource;
+
+ /** The name of the table containing the configurations. */
+ private final String table;
+
+ /** The column containing the name of the configuration. */
+ private final String nameColumn;
+
+ /** The column containing the keys. */
+ private final String keyColumn;
+
+ /** The column containing the values. */
+ private final String valueColumn;
+
+ /** The name of the configuration. */
+ private final String name;
+
+ /** A flag whether commits should be performed by this configuration. */
+ private final boolean doCommits;
+
+ /**
+ * Build a configuration from a table containing multiple configurations.
+ * No commits are performed by the new configuration instance.
+ *
+ * @param datasource the datasource to connect to the database
+ * @param table the name of the table containing the configurations
+ * @param nameColumn the column containing the name of the configuration
+ * @param keyColumn the column containing the keys of the configuration
+ * @param valueColumn the column containing the values of the configuration
+ * @param name the name of the configuration
+ */
+ public DatabaseConfiguration(DataSource datasource, String table, String nameColumn,
+ String keyColumn, String valueColumn, String name)
+ {
+ this(datasource, table, nameColumn, keyColumn, valueColumn, name, false);
+ }
+
+ /**
+ * Creates a new instance of {@code DatabaseConfiguration} that operates on
+ * a database table containing multiple configurations.
+ *
+ * @param datasource the {@code DataSource} to connect to the database
+ * @param table the name of the table containing the configurations
+ * @param nameColumn the column containing the name of the configuration
+ * @param keyColumn the column containing the keys of the configuration
+ * @param valueColumn the column containing the values of the configuration
+ * @param name the name of the configuration
+ * @param commits a flag whether the configuration should perform a commit
+ * after a database update
+ */
+ public DatabaseConfiguration(DataSource datasource, String table,
+ String nameColumn, String keyColumn, String valueColumn,
+ String name, boolean commits)
+ {
+ this.datasource = datasource;
+ this.table = table;
+ this.nameColumn = nameColumn;
+ this.keyColumn = keyColumn;
+ this.valueColumn = valueColumn;
+ this.name = name;
+ doCommits = commits;
+ setLogger(LogFactory.getLog(getClass()));
+ addErrorLogListener(); // log errors per default
+ }
+
+ /**
+ * Build a configuration from a table.
+ *
+ * @param datasource the datasource to connect to the database
+ * @param table the name of the table containing the configurations
+ * @param keyColumn the column containing the keys of the configuration
+ * @param valueColumn the column containing the values of the configuration
+ */
+ public DatabaseConfiguration(DataSource datasource, String table, String keyColumn, String valueColumn)
+ {
+ this(datasource, table, null, keyColumn, valueColumn, null);
+ }
+
+ /**
+ * Creates a new instance of {@code DatabaseConfiguration} that
+ * operates on a database table containing a single configuration only.
+ *
+ * @param datasource the {@code DataSource} to connect to the database
+ * @param table the name of the table containing the configurations
+ * @param keyColumn the column containing the keys of the configuration
+ * @param valueColumn the column containing the values of the configuration
+ * @param commits a flag whether the configuration should perform a commit
+ * after a database update
+ */
+ public DatabaseConfiguration(DataSource datasource, String table,
+ String keyColumn, String valueColumn, boolean commits)
+ {
+ this(datasource, table, null, keyColumn, valueColumn, null, commits);
+ }
+
+ /**
+ * Returns a flag whether this configuration performs commits after database
+ * updates.
+ *
+ * @return a flag whether commits are performed
+ */
+ public boolean isDoCommits()
+ {
+ return doCommits;
+ }
+
+ /**
+ * Returns the value of the specified property. If this causes a database
+ * error, an error event will be generated of type
+ * {@code EVENT_READ_PROPERTY} with the causing exception. The
+ * event's {@code propertyName} is set to the passed in property key,
+ * the {@code propertyValue} is undefined.
+ *
+ * @param key the key of the desired property
+ * @return the value of this property
+ */
+ public Object getProperty(String key)
+ {
+ Object result = null;
+
+ // build the query
+ StringBuilder query = new StringBuilder("SELECT * FROM ");
+ query.append(table).append(" WHERE ");
+ query.append(keyColumn).append("=?");
+ if (nameColumn != null)
+ {
+ query.append(" AND " + nameColumn + "=?");
+ }
+
+ Connection conn = null;
+ PreparedStatement pstmt = null;
+ ResultSet rs = null;
+
+ try
+ {
+ conn = getConnection();
+
+ // bind the parameters
+ pstmt = conn.prepareStatement(query.toString());
+ pstmt.setString(1, key);
+ if (nameColumn != null)
+ {
+ pstmt.setString(2, name);
+ }
+
+ rs = pstmt.executeQuery();
+
+ List<Object> results = new ArrayList<Object>();
+ while (rs.next())
+ {
+ Object value = rs.getObject(valueColumn);
+ if (isDelimiterParsingDisabled())
+ {
+ results.add(value);
+ }
+ else
+ {
+ // Split value if it contains the list delimiter
+ Iterator<?> it = PropertyConverter.toIterator(value, getListDelimiter());
+ while (it.hasNext())
+ {
+ results.add(it.next());
+ }
+ }
+ }
+
+ if (!results.isEmpty())
+ {
+ result = (results.size() > 1) ? results : results.get(0);
+ }
+ }
+ catch (SQLException e)
+ {
+ fireError(EVENT_READ_PROPERTY, key, null, e);
+ }
+ finally
+ {
+ close(conn, pstmt, rs);
+ }
+
+ return result;
+ }
+
+ /**
+ * Adds a property to this configuration. If this causes a database error,
+ * an error event will be generated of type {@code EVENT_ADD_PROPERTY}
+ * with the causing exception. The event's {@code propertyName} is
+ * set to the passed in property key, the {@code propertyValue}
+ * points to the passed in value.
+ *
+ * @param key the property key
+ * @param obj the value of the property to add
+ */
+ @Override
+ protected void addPropertyDirect(String key, Object obj)
+ {
+ // build the query
+ StringBuilder query = new StringBuilder("INSERT INTO " + table);
+ if (nameColumn != null)
+ {
+ query.append(" (" + nameColumn + ", " + keyColumn + ", " + valueColumn + ") VALUES (?, ?, ?)");
+ }
+ else
+ {
+ query.append(" (" + keyColumn + ", " + valueColumn + ") VALUES (?, ?)");
+ }
+
+ Connection conn = null;
+ PreparedStatement pstmt = null;
+
+ try
+ {
+ conn = getConnection();
+
+ // bind the parameters
+ pstmt = conn.prepareStatement(query.toString());
+ int index = 1;
+ if (nameColumn != null)
+ {
+ pstmt.setString(index++, name);
+ }
+ pstmt.setString(index++, key);
+ pstmt.setString(index++, String.valueOf(obj));
+
+ pstmt.executeUpdate();
+ commitIfRequired(conn);
+ }
+ catch (SQLException e)
+ {
+ fireError(EVENT_ADD_PROPERTY, key, obj, e);
+ }
+ finally
+ {
+ // clean up
+ close(conn, pstmt, null);
+ }
+ }
+
+ /**
+ * Adds a property to this configuration. This implementation will
+ * temporarily disable list delimiter parsing, so that even if the value
+ * contains the list delimiter, only a single record will be written into
+ * the managed table. The implementation of {@code getProperty()}
+ * will take care about delimiters. So list delimiters are fully supported
+ * by {@code DatabaseConfiguration}, but internally treated a bit
+ * differently.
+ *
+ * @param key the key of the new property
+ * @param value the value to be added
+ */
+ @Override
+ public void addProperty(String key, Object value)
+ {
+ boolean parsingFlag = isDelimiterParsingDisabled();
+ try
+ {
+ if (value instanceof String)
+ {
+ // temporarily disable delimiter parsing
+ setDelimiterParsingDisabled(true);
+ }
+ super.addProperty(key, value);
+ }
+ finally
+ {
+ setDelimiterParsingDisabled(parsingFlag);
+ }
+ }
+
+ /**
+ * Checks if this configuration is empty. If this causes a database error,
+ * an error event will be generated of type {@code EVENT_READ_PROPERTY}
+ * with the causing exception. Both the event's {@code propertyName}
+ * and {@code propertyValue} will be undefined.
+ *
+ * @return a flag whether this configuration is empty.
+ */
+ public boolean isEmpty()
+ {
+ boolean empty = true;
+
+ // build the query
+ StringBuilder query = new StringBuilder("SELECT count(*) FROM " + table);
+ if (nameColumn != null)
+ {
+ query.append(" WHERE " + nameColumn + "=?");
+ }
+
+ Connection conn = null;
+ PreparedStatement pstmt = null;
+ ResultSet rs = null;
+
+ try
+ {
+ conn = getConnection();
+
+ // bind the parameters
+ pstmt = conn.prepareStatement(query.toString());
+ if (nameColumn != null)
+ {
+ pstmt.setString(1, name);
+ }
+
+ rs = pstmt.executeQuery();
+
+ if (rs.next())
+ {
+ empty = rs.getInt(1) == 0;
+ }
+ }
+ catch (SQLException e)
+ {
+ fireError(EVENT_READ_PROPERTY, null, null, e);
+ }
+ finally
+ {
+ // clean up
+ close(conn, pstmt, rs);
+ }
+
+ return empty;
+ }
+
+ /**
+ * Checks whether this configuration contains the specified key. If this
+ * causes a database error, an error event will be generated of type
+ * {@code EVENT_READ_PROPERTY} with the causing exception. The
+ * event's {@code propertyName} will be set to the passed in key, the
+ * {@code propertyValue} will be undefined.
+ *
+ * @param key the key to be checked
+ * @return a flag whether this key is defined
+ */
+ public boolean containsKey(String key)
+ {
+ boolean found = false;
+
+ // build the query
+ StringBuilder query = new StringBuilder("SELECT * FROM " + table + " WHERE " + keyColumn + "=?");
+ if (nameColumn != null)
+ {
+ query.append(" AND " + nameColumn + "=?");
+ }
+
+ Connection conn = null;
+ PreparedStatement pstmt = null;
+ ResultSet rs = null;
+
+ try
+ {
+ conn = getConnection();
+
+ // bind the parameters
+ pstmt = conn.prepareStatement(query.toString());
+ pstmt.setString(1, key);
+ if (nameColumn != null)
+ {
+ pstmt.setString(2, name);
+ }
+
+ rs = pstmt.executeQuery();
+
+ found = rs.next();
+ }
+ catch (SQLException e)
+ {
+ fireError(EVENT_READ_PROPERTY, key, null, e);
+ }
+ finally
+ {
+ // clean up
+ close(conn, pstmt, rs);
+ }
+
+ return found;
+ }
+
+ /**
+ * Removes the specified value from this configuration. If this causes a
+ * database error, an error event will be generated of type
+ * {@code EVENT_CLEAR_PROPERTY} with the causing exception. The
+ * event's {@code propertyName} will be set to the passed in key, the
+ * {@code propertyValue} will be undefined.
+ *
+ * @param key the key of the property to be removed
+ */
+ @Override
+ protected void clearPropertyDirect(String key)
+ {
+ // build the query
+ StringBuilder query = new StringBuilder("DELETE FROM " + table + " WHERE " + keyColumn + "=?");
+ if (nameColumn != null)
+ {
+ query.append(" AND " + nameColumn + "=?");
+ }
+
+ Connection conn = null;
+ PreparedStatement pstmt = null;
+
+ try
+ {
+ conn = getConnection();
+
+ // bind the parameters
+ pstmt = conn.prepareStatement(query.toString());
+ pstmt.setString(1, key);
+ if (nameColumn != null)
+ {
+ pstmt.setString(2, name);
+ }
+
+ pstmt.executeUpdate();
+ commitIfRequired(conn);
+ }
+ catch (SQLException e)
+ {
+ fireError(EVENT_CLEAR_PROPERTY, key, null, e);
+ }
+ finally
+ {
+ // clean up
+ close(conn, pstmt, null);
+ }
+ }
+
+ /**
+ * Removes all entries from this configuration. If this causes a database
+ * error, an error event will be generated of type
+ * {@code EVENT_CLEAR} with the causing exception. Both the
+ * event's {@code propertyName} and the {@code propertyValue}
+ * will be undefined.
+ */
+ @Override
+ public void clear()
+ {
+ fireEvent(EVENT_CLEAR, null, null, true);
+ // build the query
+ StringBuilder query = new StringBuilder("DELETE FROM " + table);
+ if (nameColumn != null)
+ {
+ query.append(" WHERE " + nameColumn + "=?");
+ }
+
+ Connection conn = null;
+ PreparedStatement pstmt = null;
+
+ try
+ {
+ conn = getConnection();
+
+ // bind the parameters
+ pstmt = conn.prepareStatement(query.toString());
+ if (nameColumn != null)
+ {
+ pstmt.setString(1, name);
+ }
+
+ pstmt.executeUpdate();
+ commitIfRequired(conn);
+ }
+ catch (SQLException e)
+ {
+ fireError(EVENT_CLEAR, null, null, e);
+ }
+ finally
+ {
+ // clean up
+ close(conn, pstmt, null);
+ }
+ fireEvent(EVENT_CLEAR, null, null, false);
+ }
+
+ /**
+ * Returns an iterator with the names of all properties contained in this
+ * configuration. If this causes a database
+ * error, an error event will be generated of type
+ * {@code EVENT_READ_PROPERTY} with the causing exception. Both the
+ * event's {@code propertyName} and the {@code propertyValue}
+ * will be undefined.
+ * @return an iterator with the contained keys (an empty iterator in case
+ * of an error)
+ */
+ public Iterator<String> getKeys()
+ {
+ Collection<String> keys = new ArrayList<String>();
+
+ // build the query
+ StringBuilder query = new StringBuilder("SELECT DISTINCT " + keyColumn + " FROM " + table);
+ if (nameColumn != null)
+ {
+ query.append(" WHERE " + nameColumn + "=?");
+ }
+
+ Connection conn = null;
+ PreparedStatement pstmt = null;
+ ResultSet rs = null;
+
+ try
+ {
+ conn = getConnection();
+
+ // bind the parameters
+ pstmt = conn.prepareStatement(query.toString());
+ if (nameColumn != null)
+ {
+ pstmt.setString(1, name);
+ }
+
+ rs = pstmt.executeQuery();
+
+ while (rs.next())
+ {
+ keys.add(rs.getString(1));
+ }
+ }
+ catch (SQLException e)
+ {
+ fireError(EVENT_READ_PROPERTY, null, null, e);
+ }
+ finally
+ {
+ // clean up
+ close(conn, pstmt, rs);
+ }
+
+ return keys.iterator();
+ }
+
+ /**
+ * Returns the used {@code DataSource} object.
+ *
+ * @return the data source
+ * @since 1.4
+ */
+ public DataSource getDatasource()
+ {
+ return datasource;
+ }
+
+ /**
+ * Returns a {@code Connection} object. This method is called when
+ * ever the database is to be accessed. This implementation returns a
+ * connection from the current {@code DataSource}.
+ *
+ * @return the {@code Connection} object to be used
+ * @throws SQLException if an error occurs
+ * @since 1.4
+ * @deprecated Use a custom data source to change the connection used by the
+ * class. To be removed in Commons Configuration 2.0
+ */
+ @Deprecated
+ protected Connection getConnection() throws SQLException
+ {
+ return getDatasource().getConnection();
+ }
+
+ /**
+ * Close the specified database objects.
+ * Avoid closing if null and hide any SQLExceptions that occur.
+ *
+ * @param conn The database connection to close
+ * @param stmt The statement to close
+ * @param rs the result set to close
+ */
+ private void close(Connection conn, Statement stmt, ResultSet rs)
+ {
+ try
+ {
+ if (rs != null)
+ {
+ rs.close();
+ }
+ }
+ catch (SQLException e)
+ {
+ getLogger().error("An error occurred on closing the result set", e);
+ }
+
+ try
+ {
+ if (stmt != null)
+ {
+ stmt.close();
+ }
+ }
+ catch (SQLException e)
+ {
+ getLogger().error("An error occured on closing the statement", e);
+ }
+
+ try
+ {
+ if (conn != null)
+ {
+ conn.close();
+ }
+ }
+ catch (SQLException e)
+ {
+ getLogger().error("An error occured on closing the connection", e);
+ }
+ }
+
+ /**
+ * Performs a commit if needed. This method is called after updates of the
+ * managed database table. If the configuration should perform commits, it
+ * does so now.
+ *
+ * @param conn the active connection
+ * @throws SQLException if an error occurs
+ */
+ private void commitIfRequired(Connection conn) throws SQLException
+ {
+ if (isDoCommits())
+ {
+ conn.commit();
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/DefaultConfigurationBuilder.java b/src/main/java/org/apache/commons/configuration/DefaultConfigurationBuilder.java
new file mode 100644
index 0000000..77e51c3
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/DefaultConfigurationBuilder.java
@@ -0,0 +1,1800 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.configuration.beanutils.BeanDeclaration;
+import org.apache.commons.configuration.beanutils.BeanFactory;
+import org.apache.commons.configuration.beanutils.BeanHelper;
+import org.apache.commons.configuration.beanutils.DefaultBeanFactory;
+import org.apache.commons.configuration.beanutils.XMLBeanDeclaration;
+import org.apache.commons.configuration.event.ConfigurationErrorListener;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.configuration.interpol.ConfigurationInterpolator;
+import org.apache.commons.configuration.resolver.CatalogResolver;
+import org.apache.commons.configuration.resolver.EntityRegistry;
+import org.apache.commons.configuration.resolver.EntityResolverSupport;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.DefaultExpressionEngine;
+import org.apache.commons.configuration.tree.OverrideCombiner;
+import org.apache.commons.configuration.tree.UnionCombiner;
+import org.apache.commons.lang.text.StrLookup;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.xml.sax.EntityResolver;
+
+/**
+ * <p>
+ * A factory class that creates a composite configuration from an XML based
+ * <em>configuration definition file</em>.
+ * </p>
+ * <p>
+ * This class provides an easy and flexible means for loading multiple
+ * configuration sources and combining the results into a single configuration
+ * object. The sources to be loaded are defined in an XML document that can
+ * contain certain tags representing the different supported configuration
+ * classes. If such a tag is found, the corresponding {@code Configuration}
+ * class is instantiated and initialized using the classes of the
+ * {@code beanutils} package (namely
+ * {@link org.apache.commons.configuration.beanutils.XMLBeanDeclaration XMLBeanDeclaration}
+ * will be used to extract the configuration's initialization parameters, which
+ * allows for complex initialization scenarios).
+ * </p>
+ * <p>
+ * It is also possible to add custom tags to the configuration definition file.
+ * For this purpose register your own {@code ConfigurationProvider}
+ * implementation for your tag using the {@code addConfigurationProvider()}
+ * method. This provider will then be called when the corresponding custom tag
+ * is detected. For the default configuration classes providers are already
+ * registered.
+ * </p>
+ * <p>
+ * The configuration definition file has the following basic structure:
+ * </p>
+ * <p>
+ *
+ * <pre>
+ * <configuration systemProperties="properties file name">
+ * <header>
+ * <!-- Optional meta information about the composite configuration -->
+ * </header>
+ * <override>
+ * <!-- Declarations for override configurations -->
+ * </override>
+ * <additional>
+ * <!-- Declarations for union configurations -->
+ * </additional>
+ * </configuration>
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * The name of the root element (here {@code configuration}) is
+ * arbitrary. The optional systemProperties attribute identifies the path to
+ * a property file containing properties that should be added to the system
+ * properties. If specified on the root element, the system properties are
+ * set before the rest of the configuration is processed.
+ * </p>
+ * <p>
+ * There are two sections (both of them are optional) for declaring
+ * <em>override</em> and <em>additional</em> configurations. Configurations
+ * in the former section are evaluated in the order of their declaration, and
+ * properties of configurations declared earlier hide those of configurations
+ * declared later. Configurations in the latter section are combined to a union
+ * configuration, i.e. all of their properties are added to a large hierarchical
+ * configuration. Configuration declarations that occur as direct children of
+ * the root element are treated as override declarations.
+ * </p>
+ * <p>
+ * Each configuration declaration consists of a tag whose name is associated
+ * with a {@code ConfigurationProvider}. This can be one of the
+ * predefined tags like {@code properties}, or {@code xml}, or
+ * a custom tag, for which a configuration provider was registered. Attributes
+ * and sub elements with specific initialization parameters can be added. There
+ * are some reserved attributes with a special meaning that can be used in every
+ * configuration declaration:
+ * </p>
+ * <p>
+ * <table border="1">
+ * <tr>
+ * <th>Attribute</th>
+ * <th>Meaning</th>
+ * </tr>
+ * <tr>
+ * <td valign="top">{@code config-name}</td>
+ * <td>Allows to specify a name for this configuration. This name can be used
+ * to obtain a reference to the configuration from the resulting combined
+ * configuration (see below).</td>
+ * </tr>
+ * <tr>
+ * <td valign="top">{@code config-at}</td>
+ * <td>With this attribute an optional prefix can be specified for the
+ * properties of the corresponding configuration.</td>
+ * </tr>
+ * <tr>
+ * <td valign="top">{@code config-optional}</td>
+ * <td>Declares a configuration as optional. This means that errors that occur
+ * when creating the configuration are ignored. (However
+ * {@link org.apache.commons.configuration.event.ConfigurationErrorListener}s
+ * registered at the builder instance will get notified about this error: they
+ * receive an event of type {@code EVENT_ERR_LOAD_OPTIONAL}. The key
+ * property of this event contains the name of the optional configuration source
+ * that caused this problem.)</td>
+ * </tr>
+ * </table>
+ * </p>
+ * <p>
+ * The optional <em>header</em> section can contain some meta data about the
+ * created configuration itself. For instance, it is possible to set further
+ * properties of the {@code NodeCombiner} objects used for constructing
+ * the resulting configuration.
+ * </p>
+ * <p>
+ * The default configuration object returned by this builder is an instance of the
+ * {@link CombinedConfiguration} class. The return value of the
+ * {@code getConfiguration()} method can be casted to this type, and the
+ * {@code getConfiguration(boolean)} method directly declares
+ * {@code CombinedConfiguration} as return type. This allows for
+ * convenient access to the configuration objects maintained by the combined
+ * configuration (e.g. for updates of single configuration objects). It has also
+ * the advantage that the properties stored in all declared configuration
+ * objects are collected and transformed into a single hierarchical structure,
+ * which can be accessed using different expression engines. The actual CombinedConfiguration
+ * implementation can be overridden by specifying the class in the <em>config-class</em>
+ * attribute of the result element.
+ * </p>
+ * <p>
+ * A custom EntityResolver can be used for all XMLConfigurations by adding
+ * <pre>
+ * <entity-resolver config-class="EntityResolver fully qualified class name">
+ * </pre>
+ * The CatalogResolver can be used for all XMLConfiguration by adding
+ * <pre>
+ * <entity-resolver catalogFiles="comma separated list of catalog files">
+ * </pre>
+ * </p>
+ * <p>
+ * Additional ConfigurationProviders can be added by configuring them in the <em>header</em>
+ * section.
+ * <pre>
+ * <providers>
+ * <provider config-tag="tag name" config-class="provider fully qualified class name"/>
+ * </providers>
+ * </pre>
+ * </p>
+ * <p>
+ * Additional variable resolvers can be added by configuring them in the <em>header</em>
+ * section.
+ * <pre>
+ * <lookups>
+ * <lookup config-prefix="prefix" config-class="StrLookup fully qualified class name"/>
+ * </lookups>
+ * </pre>
+ * </p>
+ * <p>
+ * All declared override configurations are directly added to the resulting
+ * combined configuration. If they are given names (using the
+ * {@code config-name} attribute), they can directly be accessed using
+ * the {@code getConfiguration(String)} method of
+ * {@code CombinedConfiguration}. The additional configurations are
+ * altogether added to another combined configuration, which uses a union
+ * combiner. Then this union configuration is added to the resulting combined
+ * configuration under the name defined by the {@code ADDITIONAL_NAME}
+ * constant.
+ * </p>
+ * <p>
+ * Implementation note: This class is not thread-safe. Especially the
+ * {@code getConfiguration()} methods should be called by a single thread
+ * only.
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: DefaultConfigurationBuilder.java 1366930 2012-07-29 20:05:36Z oheger $
+ */
+public class DefaultConfigurationBuilder extends XMLConfiguration implements
+ ConfigurationBuilder
+{
+ /**
+ * Constant for the name of the additional configuration. If the
+ * configuration definition file contains an {@code additional}
+ * section, a special union configuration is created and added under this
+ * name to the resulting combined configuration.
+ */
+ public static final String ADDITIONAL_NAME = DefaultConfigurationBuilder.class
+ .getName()
+ + "/ADDITIONAL_CONFIG";
+
+ /**
+ * Constant for the type of error events caused by optional configurations
+ * that cannot be loaded.
+ */
+ public static final int EVENT_ERR_LOAD_OPTIONAL = 51;
+
+ /** Constant for the name of the configuration bean factory. */
+ static final String CONFIG_BEAN_FACTORY_NAME = DefaultConfigurationBuilder.class
+ .getName()
+ + ".CONFIG_BEAN_FACTORY_NAME";
+
+ /** Constant for the reserved name attribute. */
+ static final String ATTR_NAME = DefaultExpressionEngine.DEFAULT_ATTRIBUTE_START
+ + XMLBeanDeclaration.RESERVED_PREFIX
+ + "name"
+ + DefaultExpressionEngine.DEFAULT_ATTRIBUTE_END;
+
+ /** Constant for the name of the at attribute. */
+ static final String ATTR_ATNAME = "at";
+
+ /** Constant for the reserved at attribute. */
+ static final String ATTR_AT_RES = DefaultExpressionEngine.DEFAULT_ATTRIBUTE_START
+ + XMLBeanDeclaration.RESERVED_PREFIX
+ + ATTR_ATNAME
+ + DefaultExpressionEngine.DEFAULT_ATTRIBUTE_END;
+
+ /** Constant for the at attribute without the reserved prefix. */
+ static final String ATTR_AT = DefaultExpressionEngine.DEFAULT_ATTRIBUTE_START
+ + ATTR_ATNAME + DefaultExpressionEngine.DEFAULT_ATTRIBUTE_END;
+
+ /** Constant for the name of the optional attribute. */
+ static final String ATTR_OPTIONALNAME = "optional";
+
+ /** Constant for the reserved optional attribute. */
+ static final String ATTR_OPTIONAL_RES = DefaultExpressionEngine.DEFAULT_ATTRIBUTE_START
+ + XMLBeanDeclaration.RESERVED_PREFIX
+ + ATTR_OPTIONALNAME
+ + DefaultExpressionEngine.DEFAULT_ATTRIBUTE_END;
+
+ /** Constant for the optional attribute without the reserved prefix. */
+ static final String ATTR_OPTIONAL = DefaultExpressionEngine.DEFAULT_ATTRIBUTE_START
+ + ATTR_OPTIONALNAME + DefaultExpressionEngine.DEFAULT_ATTRIBUTE_END;
+
+ /** Constant for the file name attribute. */
+ static final String ATTR_FILENAME = DefaultExpressionEngine.DEFAULT_ATTRIBUTE_START
+ + "fileName" + DefaultExpressionEngine.DEFAULT_ATTRIBUTE_END;
+
+ /** Constant for the forceCreate attribute. */
+ static final String ATTR_FORCECREATE = DefaultExpressionEngine.DEFAULT_ATTRIBUTE_START
+ + XMLBeanDeclaration.RESERVED_PREFIX
+ + "forceCreate"
+ + DefaultExpressionEngine.DEFAULT_ATTRIBUTE_END;
+
+ /**
+ * Constant for the tag attribute for providers.
+ */
+ static final String KEY_SYSTEM_PROPS = "[@systemProperties]";
+
+ /** Constant for the name of the header section. */
+ static final String SEC_HEADER = "header";
+
+ /** Constant for an expression that selects the union configurations. */
+ static final String KEY_UNION = "additional";
+
+ /** An array with the names of top level configuration sections.*/
+ static final String[] CONFIG_SECTIONS = {
+ "additional", "override", SEC_HEADER
+ };
+
+ /**
+ * Constant for an expression that selects override configurations in the
+ * override section.
+ */
+ static final String KEY_OVERRIDE = "override";
+
+ /**
+ * Constant for the key that points to the list nodes definition of the
+ * override combiner.
+ */
+ static final String KEY_OVERRIDE_LIST = SEC_HEADER
+ + ".combiner.override.list-nodes.node";
+
+ /**
+ * Constant for the key that points to the list nodes definition of the
+ * additional combiner.
+ */
+ static final String KEY_ADDITIONAL_LIST = SEC_HEADER
+ + ".combiner.additional.list-nodes.node";
+
+ /**
+ * Constant for the key for defining providers in the configuration file.
+ */
+ static final String KEY_CONFIGURATION_PROVIDERS = SEC_HEADER
+ + ".providers.provider";
+
+ /**
+ * Constant for the tag attribute for providers.
+ */
+ static final String KEY_PROVIDER_KEY = XMLBeanDeclaration.ATTR_PREFIX + "tag]";
+
+ /**
+ * Constant for the key for defining variable resolvers
+ */
+ static final String KEY_CONFIGURATION_LOOKUPS = SEC_HEADER
+ + ".lookups.lookup";
+
+ /**
+ * Constant for the key for defining entity resolvers
+ */
+ static final String KEY_ENTITY_RESOLVER = SEC_HEADER + ".entity-resolver";
+
+ /**
+ * Constant for the prefix attribute for lookups.
+ */
+ static final String KEY_LOOKUP_KEY = XMLBeanDeclaration.ATTR_PREFIX + "prefix]";
+
+ /**
+ * Constance for the FileSystem.
+ */
+ static final String FILE_SYSTEM = SEC_HEADER + ".fileSystem";
+
+ /**
+ * Constant for the key of the result declaration. This key can point to a
+ * bean declaration, which defines properties of the resulting combined
+ * configuration.
+ */
+ static final String KEY_RESULT = SEC_HEADER + ".result";
+
+ /** Constant for the key of the combiner in the result declaration.*/
+ static final String KEY_COMBINER = KEY_RESULT + ".nodeCombiner";
+
+ /** Constant for the XML file extension. */
+ static final String EXT_XML = ".xml";
+
+ /** Constant for the provider for properties files. */
+ private static final ConfigurationProvider PROPERTIES_PROVIDER = new FileExtensionConfigurationProvider(
+ XMLPropertiesConfiguration.class, PropertiesConfiguration.class,
+ EXT_XML);
+
+ /** Constant for the provider for XML files. */
+ private static final ConfigurationProvider XML_PROVIDER = new XMLConfigurationProvider();
+
+ /** Constant for the provider for JNDI sources. */
+ private static final ConfigurationProvider JNDI_PROVIDER = new ConfigurationProvider(
+ JNDIConfiguration.class);
+
+ /** Constant for the provider for system properties. */
+ private static final ConfigurationProvider SYSTEM_PROVIDER = new ConfigurationProvider(
+ SystemConfiguration.class);
+
+ /** Constant for the provider for ini files. */
+ private static final ConfigurationProvider INI_PROVIDER =
+ new FileConfigurationProvider(HierarchicalINIConfiguration.class);
+
+ /** Constant for the provider for environment properties. */
+ private static final ConfigurationProvider ENV_PROVIDER =
+ new ConfigurationProvider(EnvironmentConfiguration.class);
+
+ /** Constant for the provider for plist files. */
+ private static final ConfigurationProvider PLIST_PROVIDER = new FileExtensionConfigurationProvider(
+ "org.apache.commons.configuration.plist.XMLPropertyListConfiguration",
+ "org.apache.commons.configuration.plist.PropertyListConfiguration",
+ EXT_XML);
+
+ /** Constant for the provider for configuration definition files.*/
+ private static final ConfigurationProvider BUILDER_PROVIDER = new ConfigurationBuilderProvider();
+
+ /** An array with the names of the default tags. */
+ private static final String[] DEFAULT_TAGS = {
+ "properties", "xml", "hierarchicalXml", "jndi", "system", "plist",
+ "configuration", "ini", "env"
+ };
+
+ /** An array with the providers for the default tags. */
+ private static final ConfigurationProvider[] DEFAULT_PROVIDERS = {
+ PROPERTIES_PROVIDER, XML_PROVIDER, XML_PROVIDER, JNDI_PROVIDER,
+ SYSTEM_PROVIDER, PLIST_PROVIDER, BUILDER_PROVIDER, INI_PROVIDER,
+ ENV_PROVIDER
+ };
+
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = -3113777854714492123L;
+
+ /**
+ * A specialized {@code StrLookup} object which operates on the combined
+ * configuration constructed by this builder. This object is used as
+ * default lookup for {@code ConfigurationInterpolator} objects assigned to
+ * newly created configuration objects.
+ */
+ private final StrLookup combinedConfigLookup = new StrLookup()
+ {
+ @Override
+ public String lookup(String key)
+ {
+ if (constructedConfiguration != null)
+ {
+ Object value =
+ constructedConfiguration.resolveContainerStore(key);
+ return (value != null) ? value.toString() : null;
+ }
+ return null;
+ }
+ };
+
+ /** Stores the configuration that is currently constructed.*/
+ private CombinedConfiguration constructedConfiguration;
+
+ /** Stores a map with the registered configuration providers. */
+ private final Map<String, ConfigurationProvider> providers;
+
+ /** Stores the base path to the configuration sources to load. */
+ private String configurationBasePath;
+
+ /**
+ * Creates a new instance of {@code DefaultConfigurationBuilder}. A
+ * configuration definition file is not yet loaded. Use the diverse setter
+ * methods provided by file based configurations to specify the
+ * configuration definition file.
+ */
+ public DefaultConfigurationBuilder()
+ {
+ super();
+ providers = new HashMap<String, ConfigurationProvider>();
+ registerDefaultProviders();
+ registerBeanFactory();
+ setLogger(LogFactory.getLog(getClass()));
+ addErrorLogListener(); // log errors per default
+ }
+
+ /**
+ * Creates a new instance of {@code DefaultConfigurationBuilder} and
+ * sets the specified configuration definition file.
+ *
+ * @param file the configuration definition file
+ */
+ public DefaultConfigurationBuilder(File file)
+ {
+ this();
+ setFile(file);
+ }
+
+ /**
+ * Creates a new instance of {@code DefaultConfigurationBuilder} and
+ * sets the specified configuration definition file.
+ *
+ * @param fileName the name of the configuration definition file
+ * @throws ConfigurationException if an error occurs when the file is loaded
+ */
+ public DefaultConfigurationBuilder(String fileName)
+ throws ConfigurationException
+ {
+ this();
+ setFileName(fileName);
+ }
+
+ /**
+ * Creates a new instance of {@code DefaultConfigurationBuilder} and
+ * sets the specified configuration definition file.
+ *
+ * @param url the URL to the configuration definition file
+ * @throws ConfigurationException if an error occurs when the file is loaded
+ */
+ public DefaultConfigurationBuilder(URL url) throws ConfigurationException
+ {
+ this();
+ setURL(url);
+ }
+
+ /**
+ * Returns the base path for the configuration sources to load. This path is
+ * used to resolve relative paths in the configuration definition file.
+ *
+ * @return the base path for configuration sources
+ */
+ public String getConfigurationBasePath()
+ {
+ return (configurationBasePath != null) ? configurationBasePath
+ : getBasePath();
+ }
+
+ /**
+ * Sets the base path for the configuration sources to load. Normally a base
+ * path need not to be set because it is determined by the location of the
+ * configuration definition file to load. All relative paths in this file
+ * are resolved relative to this file. Setting a base path makes sense if
+ * such relative paths should be otherwise resolved, e.g. if the
+ * configuration file is loaded from the class path and all sub
+ * configurations it refers to are stored in a special config directory.
+ *
+ * @param configurationBasePath the new base path to set
+ */
+ public void setConfigurationBasePath(String configurationBasePath)
+ {
+ this.configurationBasePath = configurationBasePath;
+ }
+
+ /**
+ * Adds a configuration provider for the specified tag. Whenever this tag is
+ * encountered in the configuration definition file this provider will be
+ * called to create the configuration object.
+ *
+ * @param tagName the name of the tag in the configuration definition file
+ * @param provider the provider for this tag
+ */
+ public void addConfigurationProvider(String tagName,
+ ConfigurationProvider provider)
+ {
+ if (tagName == null)
+ {
+ throw new IllegalArgumentException("Tag name must not be null!");
+ }
+ if (provider == null)
+ {
+ throw new IllegalArgumentException("Provider must not be null!");
+ }
+
+ providers.put(tagName, provider);
+ }
+
+ /**
+ * Removes the configuration provider for the specified tag name.
+ *
+ * @param tagName the tag name
+ * @return the removed configuration provider or <b>null</b> if none was
+ * registered for that tag
+ */
+ public ConfigurationProvider removeConfigurationProvider(String tagName)
+ {
+ return providers.remove(tagName);
+ }
+
+ /**
+ * Returns the configuration provider for the given tag.
+ *
+ * @param tagName the name of the tag
+ * @return the provider that was registered for this tag or <b>null</b> if
+ * there is none
+ */
+ public ConfigurationProvider providerForTag(String tagName)
+ {
+ return providers.get(tagName);
+ }
+
+ /**
+ * Returns the configuration provided by this builder. Loads and parses the
+ * configuration definition file and creates instances for the declared
+ * configurations.
+ *
+ * @return the configuration
+ * @throws ConfigurationException if an error occurs
+ */
+ public Configuration getConfiguration() throws ConfigurationException
+ {
+ return getConfiguration(true);
+ }
+
+ /**
+ * Returns the configuration provided by this builder. If the boolean
+ * parameter is <b>true</b>, the configuration definition file will be
+ * loaded. It will then be parsed, and instances for the declared
+ * configurations will be created.
+ *
+ * @param load a flag whether the configuration definition file should be
+ * loaded; a value of <b>false</b> would make sense if the file has already
+ * been created or its content was manipulated using some of the property
+ * accessor methods
+ * @return the configuration
+ * @throws ConfigurationException if an error occurs
+ */
+ public CombinedConfiguration getConfiguration(boolean load)
+ throws ConfigurationException
+ {
+ if (load)
+ {
+ load();
+ }
+
+ initFileSystem();
+ initSystemProperties();
+ configureEntityResolver();
+ registerConfiguredProviders();
+ registerConfiguredLookups();
+
+ CombinedConfiguration result = createResultConfiguration();
+ constructedConfiguration = result;
+
+ List<SubnodeConfiguration> overrides = fetchTopLevelOverrideConfigs();
+ overrides.addAll(fetchChildConfigs(KEY_OVERRIDE));
+ initCombinedConfiguration(result, overrides, KEY_OVERRIDE_LIST);
+
+ List<SubnodeConfiguration> additionals = fetchChildConfigs(KEY_UNION);
+ if (!additionals.isEmpty())
+ {
+ CombinedConfiguration addConfig = createAdditionalsConfiguration(result);
+ result.addConfiguration(addConfig, ADDITIONAL_NAME);
+ initCombinedConfiguration(addConfig, additionals,
+ KEY_ADDITIONAL_LIST);
+ }
+
+ return result;
+ }
+
+ /**
+ * Creates the resulting combined configuration. This method is called by
+ * {@code getConfiguration()}. It checks whether the
+ * {@code header} section of the configuration definition file
+ * contains a {@code result} element. If this is the case, it will be
+ * used to initialize the properties of the newly created configuration
+ * object.
+ *
+ * @return the resulting configuration object
+ * @throws ConfigurationException if an error occurs
+ */
+ protected CombinedConfiguration createResultConfiguration()
+ throws ConfigurationException
+ {
+ XMLBeanDeclaration decl = new XMLBeanDeclaration(this, KEY_RESULT, true);
+ CombinedConfiguration result = (CombinedConfiguration) BeanHelper
+ .createBean(decl, CombinedConfiguration.class);
+
+ if (getMaxIndex(KEY_COMBINER) < 0)
+ {
+ // No combiner defined => set default
+ result.setNodeCombiner(new OverrideCombiner());
+ }
+
+ return result;
+ }
+
+ /**
+ * Creates the {@code CombinedConfiguration} for the configuration
+ * sources in the <code><additional></code> section. This method is
+ * called when the builder constructs the final configuration. It creates a
+ * new {@code CombinedConfiguration} and initializes some properties
+ * from the result configuration.
+ *
+ * @param resultConfig the result configuration (this is the configuration
+ * that will be returned by the builder)
+ * @return the {@code CombinedConfiguration} for the additional
+ * configuration sources
+ * @since 1.7
+ */
+ protected CombinedConfiguration createAdditionalsConfiguration(
+ CombinedConfiguration resultConfig)
+ {
+ CombinedConfiguration addConfig =
+ new CombinedConfiguration(new UnionCombiner());
+ addConfig.setDelimiterParsingDisabled(resultConfig
+ .isDelimiterParsingDisabled());
+ addConfig.setForceReloadCheck(resultConfig.isForceReloadCheck());
+ addConfig.setIgnoreReloadExceptions(resultConfig
+ .isIgnoreReloadExceptions());
+ return addConfig;
+ }
+
+ /**
+ * Initializes a combined configuration for the configurations of a specific
+ * section. This method is called for the override and for the additional
+ * section (if it exists).
+ *
+ * @param config the configuration to be initialized
+ * @param containedConfigs the list with the declarations of the contained
+ * configurations
+ * @param keyListNodes a list with the declaration of list nodes
+ * @throws ConfigurationException if an error occurs
+ */
+ protected void initCombinedConfiguration(CombinedConfiguration config,
+ List<? extends HierarchicalConfiguration> containedConfigs,
+ String keyListNodes) throws ConfigurationException
+ {
+ List<Object> listNodes = getList(keyListNodes);
+ for (Object listNode : listNodes)
+ {
+ config.getNodeCombiner().addListNode((String) listNode);
+ }
+
+ for (HierarchicalConfiguration conf : containedConfigs)
+ {
+ ConfigurationDeclaration decl = new ConfigurationDeclaration(this,
+ conf);
+ if (getLogger().isDebugEnabled())
+ {
+ getLogger().debug("Creating configuration " + decl.getBeanClassName() + " with name "
+ + decl.getConfiguration().getString(ATTR_NAME));
+ }
+ AbstractConfiguration newConf = createConfigurationAt(decl);
+ if (newConf != null)
+ {
+ config.addConfiguration(newConf, decl.getConfiguration()
+ .getString(ATTR_NAME), decl.getAt());
+ }
+ }
+ }
+
+ /**
+ * Registers the default configuration providers supported by this class.
+ * This method will be called during initialization. It registers
+ * configuration providers for the tags that are supported by default.
+ */
+ protected void registerDefaultProviders()
+ {
+ for (int i = 0; i < DEFAULT_TAGS.length; i++)
+ {
+ addConfigurationProvider(DEFAULT_TAGS[i], DEFAULT_PROVIDERS[i]);
+ }
+ }
+
+ /**
+ * Registers providers defined in the configuration.
+ *
+ * @throws ConfigurationException if an error occurs
+ */
+ protected void registerConfiguredProviders() throws ConfigurationException
+ {
+ List<HierarchicalConfiguration> nodes = configurationsAt(KEY_CONFIGURATION_PROVIDERS);
+ for (HierarchicalConfiguration config : nodes)
+ {
+ XMLBeanDeclaration decl = new XMLBeanDeclaration(config);
+ String key = config.getString(KEY_PROVIDER_KEY);
+ addConfigurationProvider(key, (ConfigurationProvider) BeanHelper
+ .createBean(decl));
+ }
+ }
+
+ /**
+ * Registers StrLookups defined in the configuration.
+ *
+ * @throws ConfigurationException if an error occurs
+ */
+ protected void registerConfiguredLookups() throws ConfigurationException
+ {
+ List<HierarchicalConfiguration> nodes = configurationsAt(KEY_CONFIGURATION_LOOKUPS);
+ for (HierarchicalConfiguration config : nodes)
+ {
+ XMLBeanDeclaration decl = new XMLBeanDeclaration(config);
+ String key = config.getString(KEY_LOOKUP_KEY);
+ StrLookup lookup = (StrLookup) BeanHelper.createBean(decl);
+ BeanHelper.setProperty(lookup, "configuration", this);
+ ConfigurationInterpolator.registerGlobalLookup(key, lookup);
+ this.getInterpolator().registerLookup(key, lookup);
+ }
+ }
+
+ protected void initFileSystem() throws ConfigurationException
+ {
+ if (getMaxIndex(FILE_SYSTEM) == 0)
+ {
+ HierarchicalConfiguration config = configurationAt(FILE_SYSTEM);
+ XMLBeanDeclaration decl = new XMLBeanDeclaration(config);
+ setFileSystem((FileSystem) BeanHelper.createBean(decl));
+ }
+ }
+
+ /**
+ * If a property file is configured add the properties to the System properties.
+ * @throws ConfigurationException if an error occurs.
+ */
+ protected void initSystemProperties() throws ConfigurationException
+ {
+ String fileName = getString(KEY_SYSTEM_PROPS);
+ if (fileName != null)
+ {
+ try
+ {
+ SystemConfiguration.setSystemProperties(getConfigurationBasePath(), fileName);
+ }
+ catch (Exception ex)
+ {
+ throw new ConfigurationException("Error setting system properties from " + fileName, ex);
+ }
+
+ }
+ }
+
+ protected void configureEntityResolver() throws ConfigurationException
+ {
+ if (getMaxIndex(KEY_ENTITY_RESOLVER) == 0)
+ {
+ XMLBeanDeclaration decl = new XMLBeanDeclaration(this, KEY_ENTITY_RESOLVER, true);
+ EntityResolver resolver = (EntityResolver) BeanHelper.createBean(decl, CatalogResolver.class);
+ BeanHelper.setProperty(resolver, "fileSystem", getFileSystem());
+ BeanHelper.setProperty(resolver, "baseDir", getBasePath());
+ BeanHelper.setProperty(resolver, "substitutor", getSubstitutor());
+ setEntityResolver(resolver);
+ }
+ }
+
+ /**
+ * Performs interpolation. This method will not only take this configuration
+ * instance into account (which is the one that loaded the configuration
+ * definition file), but also the so far constructed combined configuration.
+ * So variables can be used that point to properties that are defined in
+ * configuration sources loaded by this builder.
+ *
+ * @param value the value to be interpolated
+ * @return the interpolated value
+ */
+ @Override
+ protected Object interpolate(Object value)
+ {
+ Object result = super.interpolate(value);
+ if (constructedConfiguration != null)
+ {
+ result = constructedConfiguration.interpolate(result);
+ }
+ return result;
+ }
+
+ /**
+ * Creates a configuration object from the specified configuration
+ * declaration.
+ *
+ * @param decl the configuration declaration
+ * @return the new configuration object
+ * @throws ConfigurationException if an error occurs
+ */
+ private AbstractConfiguration createConfigurationAt(
+ ConfigurationDeclaration decl) throws ConfigurationException
+ {
+ try
+ {
+ return (AbstractConfiguration) BeanHelper.createBean(decl);
+ }
+ catch (Exception ex)
+ {
+ // redirect to configuration exceptions
+ throw new ConfigurationException(ex);
+ }
+ }
+
+ /**
+ * Returns a list with {@code SubnodeConfiguration} objects for the
+ * child nodes of the specified configuration node.
+ *
+ * @param node the start node
+ * @return a list with subnode configurations for the node's children
+ */
+ private List<SubnodeConfiguration> fetchChildConfigs(ConfigurationNode node)
+ {
+ List<ConfigurationNode> children = node.getChildren();
+ List<SubnodeConfiguration> result = new ArrayList<SubnodeConfiguration>(children.size());
+ for (ConfigurationNode child : children)
+ {
+ result.add(createSubnodeConfiguration(child));
+ }
+ return result;
+ }
+
+ /**
+ * Returns a list with {@code SubnodeConfiguration} objects for the
+ * child nodes of the node specified by the given key.
+ *
+ * @param key the key (must define exactly one node)
+ * @return a list with subnode configurations for the node's children
+ */
+ private List<SubnodeConfiguration> fetchChildConfigs(String key)
+ {
+ List<ConfigurationNode> nodes = fetchNodeList(key);
+ if (nodes.size() > 0)
+ {
+ return fetchChildConfigs(nodes.get(0));
+ }
+ else
+ {
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Finds the override configurations that are defined as top level elements
+ * in the configuration definition file. This method will fetch the child
+ * elements of the root node and remove the nodes that represent other
+ * configuration sections. The remaining nodes are treated as definitions
+ * for override configurations.
+ *
+ * @return a list with subnode configurations for the top level override
+ * configurations
+ */
+ private List<SubnodeConfiguration> fetchTopLevelOverrideConfigs()
+ {
+ List<SubnodeConfiguration> configs = fetchChildConfigs(getRootNode());
+ for (Iterator<SubnodeConfiguration> it = configs.iterator(); it.hasNext();)
+ {
+ String nodeName = it.next().getRootNode().getName();
+ for (int i = 0; i < CONFIG_SECTIONS.length; i++)
+ {
+ if (CONFIG_SECTIONS[i].equals(nodeName))
+ {
+ it.remove();
+ break;
+ }
+ }
+ }
+ return configs;
+ }
+
+ /**
+ * Registers the bean factory used by this class if necessary. This method
+ * is called by the constructor to ensure that the required bean factory is
+ * available.
+ */
+ private void registerBeanFactory()
+ {
+ synchronized (DefaultConfigurationBuilder.class)
+ {
+ if (!BeanHelper.registeredFactoryNames().contains(
+ CONFIG_BEAN_FACTORY_NAME))
+ {
+ BeanHelper.registerBeanFactory(CONFIG_BEAN_FACTORY_NAME,
+ new ConfigurationBeanFactory());
+ }
+ }
+ }
+
+ /**
+ * <p>
+ * A base class for creating and initializing configuration sources.
+ * </p>
+ * <p>
+ * Concrete sub classes of this base class are responsible for creating
+ * specific {@code Configuration} objects for the tags in the
+ * configuration definition file. The configuration factory will parse the
+ * definition file and try to find a matching
+ * {@code ConfigurationProvider} for each encountered tag. This
+ * provider is then asked to create a corresponding
+ * {@code Configuration} object. It is up to a concrete
+ * implementation how this object is created and initialized.
+ * </p>
+ * <p>
+ * Note that at the moment only configuration classes derived from
+ * {@link AbstractConfiguration} are supported.
+ * </p>
+ */
+ public static class ConfigurationProvider extends DefaultBeanFactory
+ {
+ /** Stores the class of the configuration to be created. */
+ private Class<?> configurationClass;
+
+ /** Stores the name of the configuration class to be created.*/
+ private String configurationClassName;
+
+ /**
+ * Creates a new uninitialized instance of {@code ConfigurationProvider}.
+ */
+ public ConfigurationProvider()
+ {
+ this((Class<?>) null);
+ }
+
+ /**
+ * Creates a new instance of {@code ConfigurationProvider} and
+ * sets the class of the configuration created by this provider.
+ *
+ * @param configClass the configuration class
+ */
+ public ConfigurationProvider(Class<?> configClass)
+ {
+ setConfigurationClass(configClass);
+ }
+
+ /**
+ * Creates a new instance of {@code ConfigurationProvider} and
+ * sets the name of the class of the configuration created by this
+ * provider.
+ *
+ * @param configClassName the name of the configuration class
+ * @since 1.4
+ */
+ public ConfigurationProvider(String configClassName)
+ {
+ setConfigurationClassName(configClassName);
+ }
+
+ /**
+ * Returns the class of the configuration returned by this provider.
+ *
+ * @return the class of the provided configuration
+ */
+ public Class<?> getConfigurationClass()
+ {
+ return configurationClass;
+ }
+
+ /**
+ * Sets the class of the configuration returned by this provider.
+ *
+ * @param configurationClass the configuration class
+ */
+ public void setConfigurationClass(Class<?> configurationClass)
+ {
+ this.configurationClass = configurationClass;
+ }
+
+ /**
+ * Returns the name of the configuration class returned by this
+ * provider.
+ *
+ * @return the configuration class name
+ * @since 1.4
+ */
+ public String getConfigurationClassName()
+ {
+ return configurationClassName;
+ }
+
+ /**
+ * Sets the name of the configuration class returned by this provider.
+ *
+ * @param configurationClassName the name of the configuration class
+ * @since 1.4
+ */
+ public void setConfigurationClassName(String configurationClassName)
+ {
+ this.configurationClassName = configurationClassName;
+ }
+
+ /**
+ * Returns the configuration. This method is called to fetch the
+ * configuration from the provider. This implementation will call the
+ * inherited {@link
+ * org.apache.commons.configuration.beanutils.DefaultBeanFactory#createBean(Class, BeanDeclaration, Object)
+ * createBean()} method to create a new instance of the
+ * configuration class.
+ *
+ * @param decl the bean declaration with initialization parameters for
+ * the configuration
+ * @return the new configuration object
+ * @throws Exception if an error occurs
+ */
+ public AbstractConfiguration getConfiguration(
+ ConfigurationDeclaration decl) throws Exception
+ {
+ return (AbstractConfiguration) createBean(fetchConfigurationClass(),
+ decl, null);
+ }
+
+ /**
+ * Returns an uninitialized configuration of the represented type. This
+ * method will be called for optional configurations when the
+ * {@code getConfiguration()} method caused an error and the
+ * {@code forceCreate} attribute is set. A concrete sub class can
+ * here try to create an uninitialized, empty configuration, which may
+ * be possible if the error was created during initialization. This base
+ * implementation just returns <b>null</b>.
+ *
+ * @param decl the bean declaration with initialization parameters for
+ * the configuration
+ * @return the new configuration object
+ * @throws Exception if an error occurs
+ * @since 1.4
+ */
+ public AbstractConfiguration getEmptyConfiguration(
+ ConfigurationDeclaration decl) throws Exception
+ {
+ return null;
+ }
+
+ /**
+ * Returns the configuration class supported by this provider. If a
+ * class object was set, it is returned. Otherwise the method tries to
+ * resolve the class name.
+ *
+ * @return the class of the configuration to be created
+ * @since 1.4
+ */
+ protected synchronized Class<?> fetchConfigurationClass() throws Exception
+ {
+ if (getConfigurationClass() == null)
+ {
+ setConfigurationClass(loadClass(getConfigurationClassName()));
+ }
+ return getConfigurationClass();
+ }
+
+ /**
+ * Loads the class with the specified name dynamically. If the class's
+ * name is <b>null</b>, <b>null</b> will also be returned.
+ *
+ * @param className the name of the class to be loaded
+ * @return the class object
+ * @throws ClassNotFoundException if class loading fails
+ * @since 1.4
+ */
+ protected Class<?> loadClass(String className)
+ throws ClassNotFoundException
+ {
+ return (className != null) ? Class.forName(className, true,
+ getClass().getClassLoader()) : null;
+ }
+ }
+
+ /**
+ * <p>
+ * A specialized {@code BeanDeclaration} implementation that
+ * represents the declaration of a configuration source.
+ * </p>
+ * <p>
+ * Instances of this class are able to extract all information about a
+ * configuration source from the configuration definition file. The
+ * declaration of a configuration source is very similar to a bean
+ * declaration processed by {@code XMLBeanDeclaration}. There are
+ * very few differences, e.g. some reserved attributes like
+ * {@code optional} and {@code at} and the fact that a bean
+ * factory is never needed.
+ * </p>
+ */
+ public static class ConfigurationDeclaration extends XMLBeanDeclaration
+ {
+ /** Stores a reference to the associated configuration builder. */
+ private DefaultConfigurationBuilder configurationBuilder;
+
+ /**
+ * Creates a new instance of {@code ConfigurationDeclaration} and
+ * initializes it.
+ *
+ * @param builder the associated configuration builder
+ * @param config the configuration this declaration is based onto
+ */
+ public ConfigurationDeclaration(DefaultConfigurationBuilder builder,
+ HierarchicalConfiguration config)
+ {
+ super(config);
+ configurationBuilder = builder;
+ }
+
+ /**
+ * Returns the associated configuration builder.
+ *
+ * @return the configuration builder
+ */
+ public DefaultConfigurationBuilder getConfigurationBuilder()
+ {
+ return configurationBuilder;
+ }
+
+ /**
+ * Returns the value of the {@code at} attribute.
+ *
+ * @return the value of the {@code at} attribute (can be <b>null</b>)
+ */
+ public String getAt()
+ {
+ String result = this.getConfiguration().getString(ATTR_AT_RES);
+ return (result == null) ? this.getConfiguration().getString(ATTR_AT)
+ : result;
+ }
+
+ /**
+ * Returns a flag whether this is an optional configuration.
+ *
+ * @return a flag if this declaration points to an optional
+ * configuration
+ */
+ public boolean isOptional()
+ {
+ Boolean value = this.getConfiguration().getBoolean(ATTR_OPTIONAL_RES,
+ null);
+ if (value == null)
+ {
+ value = this.getConfiguration().getBoolean(ATTR_OPTIONAL,
+ Boolean.FALSE);
+ }
+ return value.booleanValue();
+ }
+
+ /**
+ * Returns a flag whether this configuration should always be created
+ * and added to the resulting combined configuration. This flag is
+ * evaluated only for optional configurations whose normal creation has
+ * caused an error. If for such a configuration the
+ * {@code forceCreate} attribute is set and the corresponding
+ * configuration provider supports this mode, an empty configuration
+ * will be created and added to the resulting combined configuration.
+ *
+ * @return the value of the {@code forceCreate} attribute
+ * @since 1.4
+ */
+ public boolean isForceCreate()
+ {
+ return this.getConfiguration().getBoolean(ATTR_FORCECREATE, false);
+ }
+
+ /**
+ * Returns the name of the bean factory. For configuration source
+ * declarations always a reserved factory is used. This factory's name
+ * is returned by this implementation.
+ *
+ * @return the name of the bean factory
+ */
+ @Override
+ public String getBeanFactoryName()
+ {
+ return CONFIG_BEAN_FACTORY_NAME;
+ }
+
+ /**
+ * Returns the bean's class name. This implementation will always return
+ * <b>null</b>.
+ *
+ * @return the name of the bean's class
+ */
+ @Override
+ public String getBeanClassName()
+ {
+ return null;
+ }
+
+ /**
+ * Checks whether the given node is reserved. This method will take
+ * further reserved attributes into account
+ *
+ * @param nd the node
+ * @return a flag whether this node is reserved
+ */
+ @Override
+ protected boolean isReservedNode(ConfigurationNode nd)
+ {
+ if (super.isReservedNode(nd))
+ {
+ return true;
+ }
+
+ return nd.isAttribute()
+ && ((ATTR_ATNAME.equals(nd.getName()) && nd.getParentNode()
+ .getAttributeCount(RESERVED_PREFIX + ATTR_ATNAME) == 0) || (ATTR_OPTIONALNAME
+ .equals(nd.getName()) && nd.getParentNode()
+ .getAttributeCount(RESERVED_PREFIX + ATTR_OPTIONALNAME) == 0));
+ }
+
+ /**
+ * Performs interpolation. This implementation will delegate
+ * interpolation to the configuration builder, which takes care that the
+ * currently constructed configuration is taken into account, too.
+ *
+ * @param value the value to be interpolated
+ * @return the interpolated value
+ */
+ @Override
+ protected Object interpolate(Object value)
+ {
+ return getConfigurationBuilder().interpolate(value);
+ }
+ }
+
+ /**
+ * A specialized {@code BeanFactory} implementation that handles
+ * configuration declarations. This class will retrieve the correct
+ * configuration provider and delegate the task of creating the
+ * configuration to this object.
+ */
+ static class ConfigurationBeanFactory implements BeanFactory
+ {
+ /** The logger. */
+ private Log logger = LogFactory.getLog(DefaultConfigurationBuilder.class);
+
+ /**
+ * Creates an instance of a bean class. This implementation expects that
+ * the passed in bean declaration is a declaration for a configuration.
+ * It will determine the responsible configuration provider and delegate
+ * the call to this instance. If creation of the configuration fails
+ * and the {@code optional} attribute is set, the exception will
+ * be ignored. If the {@code forceCreate} attribute is set, too,
+ * the provider is asked to create an empty configuration. A return
+ * value of <b>null</b> means that no configuration could be created.
+ *
+ * @param beanClass the bean class (will be ignored)
+ * @param data the declaration
+ * @param param an additional parameter (will be ignored)
+ * @return the newly created configuration
+ * @throws Exception if an error occurs
+ */
+ public Object createBean(Class<?> beanClass, BeanDeclaration data,
+ Object param) throws Exception
+ {
+ ConfigurationDeclaration decl = (ConfigurationDeclaration) data;
+ String tagName = decl.getNode().getName();
+ ConfigurationProvider provider = decl.getConfigurationBuilder()
+ .providerForTag(tagName);
+ if (provider == null)
+ {
+ throw new ConfigurationRuntimeException(
+ "No ConfigurationProvider registered for tag "
+ + tagName);
+ }
+
+ try
+ {
+ AbstractConfiguration config = provider.getConfiguration(decl);
+ installInterpolator(decl, config);
+ return config;
+ }
+ catch (Exception ex)
+ {
+ // If this is an optional configuration, ignore the exception
+ if (!decl.isOptional())
+ {
+ throw ex;
+ }
+ else
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Load failed for optional configuration " + tagName + ": "
+ + ex.getMessage());
+ }
+ // Notify registered error listeners
+ decl.getConfigurationBuilder().fireError(
+ EVENT_ERR_LOAD_OPTIONAL,
+ decl.getConfiguration().getString(ATTR_NAME), null,
+ ex);
+
+ if (decl.isForceCreate())
+ {
+ try
+ {
+ return provider.getEmptyConfiguration(decl);
+ }
+ catch (Exception ex2)
+ {
+ // Ignore exception, return null in this case
+ logger.warn("Could not create instance of optional configuration "
+ + tagName, ex2);
+ }
+ }
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Returns the default class for this bean factory.
+ *
+ * @return the default class
+ */
+ public Class<?> getDefaultBeanClass()
+ {
+ // Here some valid class must be returned, otherwise BeanHelper
+ // will complain that the bean's class cannot be determined
+ return Configuration.class;
+ }
+
+ /**
+ * Installs a specialized {@code ConfigurationInterpolator} on a newly
+ * created configuration which also takes the combined configuration
+ * created by the builder into account. With this
+ * {@code ConfigurationInterpolator} the interpolation facilities of
+ * this child configuration are extended to include all other
+ * configurations created by this builder.
+ *
+ * @param decl the {@code ConfigurationDeclaration}
+ * @param config the newly created configuration instance
+ */
+ private void installInterpolator(ConfigurationDeclaration decl,
+ AbstractConfiguration config)
+ {
+ ConfigurationInterpolator parent = new ConfigurationInterpolator();
+ parent.setDefaultLookup(decl.getConfigurationBuilder().combinedConfigLookup);
+ config.getInterpolator().setParentInterpolator(parent);
+ }
+ }
+
+ /**
+ * A specialized provider implementation that deals with file based
+ * configurations. Ensures that the base path is correctly set and that the
+ * load() method gets called.
+ */
+ public static class FileConfigurationProvider extends ConfigurationProvider
+ {
+ /**
+ * Creates a new instance of {@code FileConfigurationProvider}.
+ */
+ public FileConfigurationProvider()
+ {
+ super();
+ }
+
+ /**
+ * Creates a new instance of {@code FileConfigurationProvider}
+ * and sets the configuration class.
+ *
+ * @param configClass the class for the configurations to be created
+ */
+ public FileConfigurationProvider(Class<?> configClass)
+ {
+ super(configClass);
+ }
+
+ /**
+ * Creates a new instance of {@code FileConfigurationProvider}
+ * and sets the configuration class name.
+ *
+ * @param configClassName the name of the configuration to be created
+ * @since 1.4
+ */
+ public FileConfigurationProvider(String configClassName)
+ {
+ super(configClassName);
+ }
+
+ /**
+ * Creates the configuration. After that {@code load()} will be
+ * called. If this configuration is marked as optional, exceptions will
+ * be ignored.
+ *
+ * @param decl the declaration
+ * @return the new configuration
+ * @throws Exception if an error occurs
+ */
+ @Override
+ public AbstractConfiguration getConfiguration(
+ ConfigurationDeclaration decl) throws Exception
+ {
+ AbstractConfiguration result = getEmptyConfiguration(decl);
+ if (result instanceof FileSystemBased)
+ {
+ DefaultConfigurationBuilder builder = decl.getConfigurationBuilder();
+ if (builder.getFileSystem() != null)
+ {
+ ((FileSystemBased) result).setFileSystem(builder.getFileSystem());
+ }
+ }
+ ((FileConfiguration) result).load();
+ return result;
+ }
+
+ /**
+ * Returns an uninitialized file configuration. This method will be
+ * called for optional configurations when the
+ * {@code getConfiguration()} method caused an error and the
+ * {@code forceCreate} attribute is set. It will create the
+ * configuration of the represented type, but the {@code load()}
+ * method won't be called. This way non-existing configuration files can
+ * be handled gracefully: If loading a the file fails, an empty
+ * configuration will be created that is already configured with the
+ * correct file name.
+ *
+ * @param decl the bean declaration with initialization parameters for
+ * the configuration
+ * @return the new configuration object
+ * @throws Exception if an error occurs
+ * @since 1.4
+ */
+ @Override
+ public AbstractConfiguration getEmptyConfiguration(
+ ConfigurationDeclaration decl) throws Exception
+ {
+ AbstractConfiguration config = super.getConfiguration(decl);
+
+ /**
+ * Some wrapper classes may need to pass the EntityResolver to XMLConfigurations
+ * they construct buy may not be an XMLConfiguration.
+ */
+ if (config instanceof EntityResolverSupport)
+ {
+ DefaultConfigurationBuilder builder = decl.getConfigurationBuilder();
+ EntityResolver resolver = builder.getEntityResolver();
+ ((EntityResolverSupport) config).setEntityResolver(resolver);
+ }
+
+ return config;
+ }
+
+ /**
+ * Initializes the bean instance. Ensures that the file configuration's
+ * base path will be initialized with the base path of the factory so
+ * that relative path names can be correctly resolved.
+ *
+ * @param bean the bean to be initialized
+ * @param data the declaration
+ * @throws Exception if an error occurs
+ */
+ @Override
+ protected void initBeanInstance(Object bean, BeanDeclaration data)
+ throws Exception
+ {
+ FileConfiguration config = (FileConfiguration) bean;
+ config.setBasePath(((ConfigurationDeclaration) data)
+ .getConfigurationBuilder().getConfigurationBasePath());
+ super.initBeanInstance(bean, data);
+ }
+ }
+
+ /**
+ * A specialized configuration provider for XML configurations. This
+ * implementation acts like a {@code FileConfigurationProvider}, but
+ * it will copy all entity IDs that have been registered for the
+ * configuration builder to the new XML configuration before it is loaded.
+ *
+ * @since 1.6
+ */
+ public static class XMLConfigurationProvider extends FileConfigurationProvider
+ {
+ /**
+ * Creates a new instance of {@code XMLConfigurationProvider}.
+ */
+ public XMLConfigurationProvider()
+ {
+ super(XMLConfiguration.class);
+ }
+
+ /**
+ * Returns a new empty configuration instance. This implementation
+ * performs some additional initialization specific to XML
+ * configurations.
+ *
+ * @param decl the configuration declaration
+ * @return the new configuration
+ * @throws Exception if an error occurs
+ */
+ @Override
+ public AbstractConfiguration getEmptyConfiguration(
+ ConfigurationDeclaration decl) throws Exception
+ {
+ XMLConfiguration config = (XMLConfiguration) super
+ .getEmptyConfiguration(decl);
+
+ DefaultConfigurationBuilder builder = decl
+ .getConfigurationBuilder();
+ EntityResolver resolver = builder.getEntityResolver();
+ if (resolver instanceof EntityRegistry)
+ {
+ // copy the registered entities
+ config.getRegisteredEntities().putAll(
+ builder.getRegisteredEntities());
+ }
+ else
+ {
+ config.setEntityResolver(resolver);
+ }
+ return config;
+ }
+ }
+
+ /**
+ * A specialized configuration provider for file based configurations that
+ * can handle configuration sources whose concrete type depends on the
+ * extension of the file to be loaded. One example is the
+ * {@code properties} tag: if the file ends with ".xml" a
+ * XMLPropertiesConfiguration object must be created, otherwise a
+ * PropertiesConfiguration object.
+ */
+ static class FileExtensionConfigurationProvider extends
+ FileConfigurationProvider
+ {
+ /**
+ * Stores the class to be created when the file extension matches.
+ */
+ private Class<?> matchingClass;
+
+ /**
+ * Stores the name of the class to be created when the file extension
+ * matches.
+ */
+ private String matchingClassName;
+
+ /**
+ * Stores the class to be created when the file extension does not
+ * match.
+ */
+ private Class<?> defaultClass;
+
+ /**
+ * Stores the name of the class to be created when the file extension
+ * does not match.
+ */
+ private String defaultClassName;
+
+ /** Stores the file extension to be checked against. */
+ private String fileExtension;
+
+ /**
+ * Creates a new instance of
+ * {@code FileExtensionConfigurationProvider} and initializes it.
+ *
+ * @param matchingClass the class to be created when the file extension
+ * matches
+ * @param defaultClass the class to be created when the file extension
+ * does not match
+ * @param extension the file extension to be checked against
+ */
+ public FileExtensionConfigurationProvider(Class<?> matchingClass,
+ Class<?> defaultClass, String extension)
+ {
+ this.matchingClass = matchingClass;
+ this.defaultClass = defaultClass;
+ fileExtension = extension;
+ }
+
+ /**
+ * Creates a new instance of
+ * {@code FileExtensionConfigurationProvider} and initializes it
+ * with the names of the classes to be created.
+ *
+ * @param matchingClassName the name of the class to be created when the
+ * file extension matches
+ * @param defaultClassName the name of the class to be created when the
+ * file extension does not match
+ * @param extension the file extension to be checked against
+ * @since 1.4
+ */
+ public FileExtensionConfigurationProvider(String matchingClassName,
+ String defaultClassName, String extension)
+ {
+ this.matchingClassName = matchingClassName;
+ this.defaultClassName = defaultClassName;
+ fileExtension = extension;
+ }
+
+ /**
+ * Returns the matching class object, no matter whether it was defined
+ * as a class or as a class name.
+ *
+ * @return the matching class object
+ * @throws Exception if an error occurs
+ * @since 1.4
+ */
+ protected synchronized Class<?> fetchMatchingClass() throws Exception
+ {
+ if (matchingClass == null)
+ {
+ matchingClass = loadClass(matchingClassName);
+ }
+ return matchingClass;
+ }
+
+ /**
+ * Returns the default class object, no matter whether it was defined as
+ * a class or as a class name.
+ *
+ * @return the default class object
+ * @throws Exception if an error occurs
+ * @since 1.4
+ */
+ protected synchronized Class<?> fetchDefaultClass() throws Exception
+ {
+ if (defaultClass == null)
+ {
+ defaultClass = loadClass(defaultClassName);
+ }
+ return defaultClass;
+ }
+
+ /**
+ * Creates the configuration object. The class is determined by the file
+ * name's extension.
+ *
+ * @param beanClass the class
+ * @param data the bean declaration
+ * @return the new bean
+ * @throws Exception if an error occurs
+ */
+ @Override
+ protected Object createBeanInstance(Class<?> beanClass,
+ BeanDeclaration data) throws Exception
+ {
+ String fileName = ((ConfigurationDeclaration) data)
+ .getConfiguration().getString(ATTR_FILENAME);
+ if (fileName != null
+ && fileName.toLowerCase().trim().endsWith(fileExtension))
+ {
+ return super.createBeanInstance(fetchMatchingClass(), data);
+ }
+ else
+ {
+ return super.createBeanInstance(fetchDefaultClass(), data);
+ }
+ }
+ }
+
+ /**
+ * A specialized configuration provider class that allows to include other
+ * configuration definition files.
+ */
+ static class ConfigurationBuilderProvider extends ConfigurationProvider
+ {
+ /**
+ * Creates a new instance of {@code ConfigurationBuilderProvider}.
+ */
+ public ConfigurationBuilderProvider()
+ {
+ super(DefaultConfigurationBuilder.class);
+ }
+
+ /**
+ * Creates the configuration. First creates a configuration builder
+ * object. Then returns the configuration created by this builder.
+ *
+ * @param decl the configuration declaration
+ * @return the configuration
+ * @exception Exception if an error occurs
+ */
+ @Override
+ public AbstractConfiguration getConfiguration(
+ ConfigurationDeclaration decl) throws Exception
+ {
+ DefaultConfigurationBuilder builder = (DefaultConfigurationBuilder) super
+ .getConfiguration(decl);
+ return builder.getConfiguration(true);
+ }
+
+ /**
+ * Returns an empty configuration in case of an optional configuration
+ * could not be created. This implementation returns an empty combined
+ * configuration.
+ *
+ * @param decl the configuration declaration
+ * @return the configuration
+ * @exception Exception if an error occurs
+ * @since 1.4
+ */
+ @Override
+ public AbstractConfiguration getEmptyConfiguration(
+ ConfigurationDeclaration decl) throws Exception
+ {
+ return new CombinedConfiguration();
+ }
+
+ /**
+ * {@inheritDoc} This implementation ensures that the configuration
+ * builder created by this provider inherits the properties from the
+ * current configuration builder.
+ */
+ @Override
+ protected void initBeanInstance(Object bean, BeanDeclaration data)
+ throws Exception
+ {
+ ConfigurationDeclaration decl = (ConfigurationDeclaration) data;
+ initChildBuilder(decl.getConfigurationBuilder(),
+ (DefaultConfigurationBuilder) bean);
+ super.initBeanInstance(bean, data);
+ }
+
+ /**
+ * Initializes the given child configuration builder from its parent
+ * builder. This method copies the values of some properties from the
+ * parent builder to the child builder so that the child inherits
+ * properties from its parent.
+ *
+ * @param parent the parent builder
+ * @param child the child builder
+ */
+ private static void initChildBuilder(
+ DefaultConfigurationBuilder parent,
+ DefaultConfigurationBuilder child)
+ {
+ child.setAttributeSplittingDisabled(parent
+ .isAttributeSplittingDisabled());
+ child.setBasePath(parent.getBasePath());
+ child.setDelimiterParsingDisabled(parent
+ .isDelimiterParsingDisabled());
+ child.setListDelimiter(parent.getListDelimiter());
+ child.setThrowExceptionOnMissing(parent.isThrowExceptionOnMissing());
+ child.setLogger(parent.getLogger());
+
+ child.clearConfigurationListeners();
+ for (ConfigurationListener l : parent.getConfigurationListeners())
+ {
+ child.addConfigurationListener(l);
+ }
+ child.clearErrorListeners();
+ for (ConfigurationErrorListener l : parent.getErrorListeners())
+ {
+ child.addErrorListener(l);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/DefaultFileSystem.java b/src/main/java/org/apache/commons/configuration/DefaultFileSystem.java
new file mode 100644
index 0000000..ac6e8ff
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/DefaultFileSystem.java
@@ -0,0 +1,373 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * FileSystem that uses java.io.File or HttpClient
+ * @since 1.7
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons Configuration team</a>
+ */
+public class DefaultFileSystem extends FileSystem
+{
+ /**
+ * The Log for diagnostic messages.
+ */
+ private Log log = LogFactory.getLog(DefaultFileSystem.class);
+
+ @Override
+ public InputStream getInputStream(String basePath, String fileName)
+ throws ConfigurationException
+ {
+ try
+ {
+ URL url = ConfigurationUtils.locate(this, basePath, fileName);
+
+ if (url == null)
+ {
+ throw new ConfigurationException("Cannot locate configuration source " + fileName);
+ }
+ return getInputStream(url);
+ }
+ catch (ConfigurationException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new ConfigurationException("Unable to load the configuration file " + fileName, e);
+ }
+ }
+
+ @Override
+ public InputStream getInputStream(URL url) throws ConfigurationException
+ {
+ // throw an exception if the target URL is a directory
+ File file = ConfigurationUtils.fileFromURL(url);
+ if (file != null && file.isDirectory())
+ {
+ throw new ConfigurationException("Cannot load a configuration from a directory");
+ }
+
+ try
+ {
+ return url.openStream();
+ }
+ catch (Exception e)
+ {
+ throw new ConfigurationException("Unable to load the configuration from the URL " + url, e);
+ }
+ }
+
+ @Override
+ public OutputStream getOutputStream(URL url) throws ConfigurationException
+ {
+ // file URLs have to be converted to Files since FileURLConnection is
+ // read only (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4191800)
+ File file = ConfigurationUtils.fileFromURL(url);
+ if (file != null)
+ {
+ return getOutputStream(file);
+ }
+ else
+ {
+ // for non file URLs save through an URLConnection
+ OutputStream out;
+ try
+ {
+ URLConnection connection = url.openConnection();
+ connection.setDoOutput(true);
+
+ // use the PUT method for http URLs
+ if (connection instanceof HttpURLConnection)
+ {
+ HttpURLConnection conn = (HttpURLConnection) connection;
+ conn.setRequestMethod("PUT");
+ }
+
+ out = connection.getOutputStream();
+
+ // check the response code for http URLs and throw an exception if an error occured
+ if (connection instanceof HttpURLConnection)
+ {
+ out = new HttpOutputStream(out, (HttpURLConnection) connection);
+ }
+ return out;
+ }
+ catch (IOException e)
+ {
+ throw new ConfigurationException("Could not save to URL " + url, e);
+ }
+ }
+ }
+
+ @Override
+ public OutputStream getOutputStream(File file) throws ConfigurationException
+ {
+ try
+ {
+ // create the file if necessary
+ createPath(file);
+ return new FileOutputStream(file);
+ }
+ catch (FileNotFoundException e)
+ {
+ throw new ConfigurationException("Unable to save to file " + file, e);
+ }
+ }
+
+ @Override
+ public String getPath(File file, URL url, String basePath, String fileName)
+ {
+ String path = null;
+ // if resource was loaded from jar file may be null
+ if (file != null)
+ {
+ path = file.getAbsolutePath();
+ }
+
+ // try to see if file was loaded from a jar
+ if (path == null)
+ {
+ if (url != null)
+ {
+ path = url.getPath();
+ }
+ else
+ {
+ try
+ {
+ path = getURL(basePath, fileName).getPath();
+ }
+ catch (Exception e)
+ {
+ // simply ignore it and return null
+ if (log.isDebugEnabled())
+ {
+ log.debug(String.format("Could not determine URL for "
+ + "basePath = %s, fileName = %s.", basePath,
+ fileName), e);
+ }
+ }
+ }
+ }
+
+ return path;
+ }
+
+ @Override
+ public String getBasePath(String path)
+ {
+ URL url;
+ try
+ {
+ url = getURL(null, path);
+ return ConfigurationUtils.getBasePath(url);
+ }
+ catch (Exception e)
+ {
+ return null;
+ }
+ }
+
+ @Override
+ public String getFileName(String path)
+ {
+ URL url;
+ try
+ {
+ url = getURL(null, path);
+ return ConfigurationUtils.getFileName(url);
+ }
+ catch (Exception e)
+ {
+ return null;
+ }
+ }
+
+
+ @Override
+ public URL getURL(String basePath, String file) throws MalformedURLException
+ {
+ File f = new File(file);
+ if (f.isAbsolute()) // already absolute?
+ {
+ return ConfigurationUtils.toURL(f);
+ }
+
+ try
+ {
+ if (basePath == null)
+ {
+ return new URL(file);
+ }
+ else
+ {
+ URL base = new URL(basePath);
+ return new URL(base, file);
+ }
+ }
+ catch (MalformedURLException uex)
+ {
+ return ConfigurationUtils.toURL(ConfigurationUtils.constructFile(basePath, file));
+ }
+ }
+
+
+ @Override
+ public URL locateFromURL(String basePath, String fileName)
+ {
+ try
+ {
+ URL url;
+ if (basePath == null)
+ {
+ return new URL(fileName);
+ //url = new URL(name);
+ }
+ else
+ {
+ URL baseURL = new URL(basePath);
+ url = new URL(baseURL, fileName);
+
+ // check if the file exists
+ InputStream in = null;
+ try
+ {
+ in = url.openStream();
+ }
+ finally
+ {
+ if (in != null)
+ {
+ in.close();
+ }
+ }
+ return url;
+ }
+ }
+ catch (IOException e)
+ {
+ if (log.isDebugEnabled())
+ {
+ log.debug("Could not locate file " + fileName + " at " + basePath + ": " + e.getMessage());
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Create the path to the specified file.
+ *
+ * @param file the target file
+ */
+ private void createPath(File file)
+ {
+ if (file != null)
+ {
+ // create the path to the file if the file doesn't exist
+ if (!file.exists())
+ {
+ File parent = file.getParentFile();
+ if (parent != null && !parent.exists())
+ {
+ parent.mkdirs();
+ }
+ }
+ }
+ }
+ /**
+ * Wraps the output stream so errors can be detected in the HTTP response.
+ * @since 1.7
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons Configuration team</a>
+ */
+ private static class HttpOutputStream extends VerifiableOutputStream
+ {
+ /** The wrapped OutputStream */
+ private final OutputStream stream;
+
+ /** The HttpURLConnection */
+ private final HttpURLConnection connection;
+
+ public HttpOutputStream(OutputStream stream, HttpURLConnection connection)
+ {
+ this.stream = stream;
+ this.connection = connection;
+ }
+
+ @Override
+ public void write(byte[] bytes) throws IOException
+ {
+ stream.write(bytes);
+ }
+
+ @Override
+ public void write(byte[] bytes, int i, int i1) throws IOException
+ {
+ stream.write(bytes, i, i1);
+ }
+
+ @Override
+ public void flush() throws IOException
+ {
+ stream.flush();
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ stream.close();
+ }
+
+ @Override
+ public void write(int i) throws IOException
+ {
+ stream.write(i);
+ }
+
+ @Override
+ public String toString()
+ {
+ return stream.toString();
+ }
+
+ @Override
+ public void verify() throws IOException
+ {
+ if (connection.getResponseCode() >= HttpURLConnection.HTTP_BAD_REQUEST)
+ {
+ throw new IOException("HTTP Error " + connection.getResponseCode()
+ + " " + connection.getResponseMessage());
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/DynamicCombinedConfiguration.java b/src/main/java/org/apache/commons/configuration/DynamicCombinedConfiguration.java
new file mode 100644
index 0000000..5a92918
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/DynamicCombinedConfiguration.java
@@ -0,0 +1,935 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.commons.configuration.event.ConfigurationErrorListener;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.configuration.interpol.ConfigurationInterpolator;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.ExpressionEngine;
+import org.apache.commons.configuration.tree.NodeCombiner;
+import org.apache.commons.lang.text.StrSubstitutor;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * DynamicCombinedConfiguration allows a set of CombinedConfigurations to be used. Each CombinedConfiguration
+ * is referenced by a key that is dynamically constructed from a key pattern on each call. The key pattern
+ * will be resolved using the configured ConfigurationInterpolator.
+ * @since 1.6
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: DynamicCombinedConfiguration.java 1534064 2013-10-21 08:44:33Z henning $
+ */
+public class DynamicCombinedConfiguration extends CombinedConfiguration
+{
+ /**
+ * Prevent recursion while resolving unprefixed properties.
+ */
+ private static ThreadLocal<Boolean> recursive = new ThreadLocal<Boolean>()
+ {
+ @Override
+ protected synchronized Boolean initialValue()
+ {
+ return Boolean.FALSE;
+ }
+ };
+
+ /** The CombinedConfigurations */
+ private final ConcurrentMap<String, CombinedConfiguration> configs =
+ new ConcurrentHashMap<String, CombinedConfiguration>();
+
+ /** Stores a list with the contained configurations. */
+ private List<ConfigData> configurations = new ArrayList<ConfigData>();
+
+ /** Stores a map with the named configurations. */
+ private Map<String, AbstractConfiguration> namedConfigurations =
+ new HashMap<String, AbstractConfiguration>();
+
+ /** The key pattern for the CombinedConfiguration map */
+ private String keyPattern;
+
+ /** Stores the combiner. */
+ private NodeCombiner nodeCombiner;
+
+ /** The name of the logger to use for each CombinedConfiguration */
+ private String loggerName = DynamicCombinedConfiguration.class.getName();
+
+ /** The object for handling variable substitution in key patterns. */
+ private StrSubstitutor localSubst = new StrSubstitutor(new ConfigurationInterpolator());
+
+ /**
+ * Creates a new instance of {@code DynamicCombinedConfiguration} and
+ * initializes the combiner to be used.
+ *
+ * @param comb the node combiner (can be <b>null</b>, then a union combiner
+ * is used as default)
+ */
+ public DynamicCombinedConfiguration(NodeCombiner comb)
+ {
+ super();
+ setNodeCombiner(comb);
+ setIgnoreReloadExceptions(false);
+ setLogger(LogFactory.getLog(DynamicCombinedConfiguration.class));
+ }
+
+ /**
+ * Creates a new instance of {@code DynamicCombinedConfiguration} that uses
+ * a union combiner.
+ *
+ * @see org.apache.commons.configuration.tree.UnionCombiner
+ */
+ public DynamicCombinedConfiguration()
+ {
+ super();
+ setIgnoreReloadExceptions(false);
+ setLogger(LogFactory.getLog(DynamicCombinedConfiguration.class));
+ }
+
+ public void setKeyPattern(String pattern)
+ {
+ this.keyPattern = pattern;
+ }
+
+ public String getKeyPattern()
+ {
+ return this.keyPattern;
+ }
+
+ /**
+ * Set the name of the Logger to use on each CombinedConfiguration.
+ * @param name The Logger name.
+ */
+ public void setLoggerName(String name)
+ {
+ this.loggerName = name;
+ }
+
+ /**
+ * Returns the node combiner that is used for creating the combined node
+ * structure.
+ *
+ * @return the node combiner
+ */
+ @Override
+ public NodeCombiner getNodeCombiner()
+ {
+ return nodeCombiner;
+ }
+
+ /**
+ * Sets the node combiner. This object will be used when the combined node
+ * structure is to be constructed. It must not be <b>null</b>, otherwise an
+ * {@code IllegalArgumentException} exception is thrown. Changing the
+ * node combiner causes an invalidation of this combined configuration, so
+ * that the new combiner immediately takes effect.
+ *
+ * @param nodeCombiner the node combiner
+ */
+ @Override
+ public void setNodeCombiner(NodeCombiner nodeCombiner)
+ {
+ if (nodeCombiner == null)
+ {
+ throw new IllegalArgumentException(
+ "Node combiner must not be null!");
+ }
+ this.nodeCombiner = nodeCombiner;
+ invalidateAll();
+ }
+ /**
+ * Adds a new configuration to this combined configuration. It is possible
+ * (but not mandatory) to give the new configuration a name. This name must
+ * be unique, otherwise a {@code ConfigurationRuntimeException} will
+ * be thrown. With the optional {@code at} argument you can specify
+ * where in the resulting node structure the content of the added
+ * configuration should appear. This is a string that uses dots as property
+ * delimiters (independent on the current expression engine). For instance
+ * if you pass in the string {@code "database.tables"},
+ * all properties of the added configuration will occur in this branch.
+ *
+ * @param config the configuration to add (must not be <b>null</b>)
+ * @param name the name of this configuration (can be <b>null</b>)
+ * @param at the position of this configuration in the combined tree (can be
+ * <b>null</b>)
+ */
+ @Override
+ public void addConfiguration(AbstractConfiguration config, String name,
+ String at)
+ {
+ ConfigData cd = new ConfigData(config, name, at);
+ configurations.add(cd);
+ if (name != null)
+ {
+ namedConfigurations.put(name, config);
+ }
+ }
+ /**
+ * Returns the number of configurations that are contained in this combined
+ * configuration.
+ *
+ * @return the number of contained configurations
+ */
+ @Override
+ public int getNumberOfConfigurations()
+ {
+ return configurations.size();
+ }
+
+ /**
+ * Returns the configuration at the specified index. The contained
+ * configurations are numbered in the order they were added to this combined
+ * configuration. The index of the first configuration is 0.
+ *
+ * @param index the index
+ * @return the configuration at this index
+ */
+ @Override
+ public Configuration getConfiguration(int index)
+ {
+ ConfigData cd = configurations.get(index);
+ return cd.getConfiguration();
+ }
+
+ /**
+ * Returns the configuration with the given name. This can be <b>null</b>
+ * if no such configuration exists.
+ *
+ * @param name the name of the configuration
+ * @return the configuration with this name
+ */
+ @Override
+ public Configuration getConfiguration(String name)
+ {
+ return namedConfigurations.get(name);
+ }
+
+ /**
+ * Returns a set with the names of all configurations contained in this
+ * combined configuration. Of course here are only these configurations
+ * listed, for which a name was specified when they were added.
+ *
+ * @return a set with the names of the contained configurations (never
+ * <b>null</b>)
+ */
+ @Override
+ public Set<String> getConfigurationNames()
+ {
+ return namedConfigurations.keySet();
+ }
+
+ /**
+ * Removes the configuration with the specified name.
+ *
+ * @param name the name of the configuration to be removed
+ * @return the removed configuration (<b>null</b> if this configuration
+ * was not found)
+ */
+ @Override
+ public Configuration removeConfiguration(String name)
+ {
+ Configuration conf = getConfiguration(name);
+ if (conf != null)
+ {
+ removeConfiguration(conf);
+ }
+ return conf;
+ }
+
+ /**
+ * Removes the specified configuration from this combined configuration.
+ *
+ * @param config the configuration to be removed
+ * @return a flag whether this configuration was found and could be removed
+ */
+ @Override
+ public boolean removeConfiguration(Configuration config)
+ {
+ for (int index = 0; index < getNumberOfConfigurations(); index++)
+ {
+ if (configurations.get(index).getConfiguration() == config)
+ {
+ removeConfigurationAt(index);
+
+ }
+ }
+
+ return super.removeConfiguration(config);
+ }
+
+ /**
+ * Removes the configuration at the specified index.
+ *
+ * @param index the index
+ * @return the removed configuration
+ */
+ @Override
+ public Configuration removeConfigurationAt(int index)
+ {
+ ConfigData cd = configurations.remove(index);
+ if (cd.getName() != null)
+ {
+ namedConfigurations.remove(cd.getName());
+ }
+ return super.removeConfigurationAt(index);
+ }
+ /**
+ * Returns the configuration root node of this combined configuration. This
+ * method will construct a combined node structure using the current node
+ * combiner if necessary.
+ *
+ * @return the combined root node
+ */
+ @Override
+ public ConfigurationNode getRootNode()
+ {
+ return getCurrentConfig().getRootNode();
+ }
+
+ @Override
+ public void setRootNode(ConfigurationNode rootNode)
+ {
+ if (configs != null)
+ {
+ this.getCurrentConfig().setRootNode(rootNode);
+ }
+ else
+ {
+ super.setRootNode(rootNode);
+ }
+ }
+
+ @Override
+ public void addProperty(String key, Object value)
+ {
+ this.getCurrentConfig().addProperty(key, value);
+ }
+
+ @Override
+ public void clear()
+ {
+ if (configs != null)
+ {
+ this.getCurrentConfig().clear();
+ }
+ }
+
+ @Override
+ public void clearProperty(String key)
+ {
+ this.getCurrentConfig().clearProperty(key);
+ }
+
+ @Override
+ public boolean containsKey(String key)
+ {
+ return this.getCurrentConfig().containsKey(key);
+ }
+
+ @Override
+ public BigDecimal getBigDecimal(String key, BigDecimal defaultValue)
+ {
+ return this.getCurrentConfig().getBigDecimal(key, defaultValue);
+ }
+
+ @Override
+ public BigDecimal getBigDecimal(String key)
+ {
+ return this.getCurrentConfig().getBigDecimal(key);
+ }
+
+ @Override
+ public BigInteger getBigInteger(String key, BigInteger defaultValue)
+ {
+ return this.getCurrentConfig().getBigInteger(key, defaultValue);
+ }
+
+ @Override
+ public BigInteger getBigInteger(String key)
+ {
+ return this.getCurrentConfig().getBigInteger(key);
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean defaultValue)
+ {
+ return this.getCurrentConfig().getBoolean(key, defaultValue);
+ }
+
+ @Override
+ public Boolean getBoolean(String key, Boolean defaultValue)
+ {
+ return this.getCurrentConfig().getBoolean(key, defaultValue);
+ }
+
+ @Override
+ public boolean getBoolean(String key)
+ {
+ return this.getCurrentConfig().getBoolean(key);
+ }
+
+ @Override
+ public byte getByte(String key, byte defaultValue)
+ {
+ return this.getCurrentConfig().getByte(key, defaultValue);
+ }
+
+ @Override
+ public Byte getByte(String key, Byte defaultValue)
+ {
+ return this.getCurrentConfig().getByte(key, defaultValue);
+ }
+
+ @Override
+ public byte getByte(String key)
+ {
+ return this.getCurrentConfig().getByte(key);
+ }
+
+ @Override
+ public double getDouble(String key, double defaultValue)
+ {
+ return this.getCurrentConfig().getDouble(key, defaultValue);
+ }
+
+ @Override
+ public Double getDouble(String key, Double defaultValue)
+ {
+ return this.getCurrentConfig().getDouble(key, defaultValue);
+ }
+
+ @Override
+ public double getDouble(String key)
+ {
+ return this.getCurrentConfig().getDouble(key);
+ }
+
+ @Override
+ public float getFloat(String key, float defaultValue)
+ {
+ return this.getCurrentConfig().getFloat(key, defaultValue);
+ }
+
+ @Override
+ public Float getFloat(String key, Float defaultValue)
+ {
+ return this.getCurrentConfig().getFloat(key, defaultValue);
+ }
+
+ @Override
+ public float getFloat(String key)
+ {
+ return this.getCurrentConfig().getFloat(key);
+ }
+
+ @Override
+ public int getInt(String key, int defaultValue)
+ {
+ return this.getCurrentConfig().getInt(key, defaultValue);
+ }
+
+ @Override
+ public int getInt(String key)
+ {
+ return this.getCurrentConfig().getInt(key);
+ }
+
+ @Override
+ public Integer getInteger(String key, Integer defaultValue)
+ {
+ return this.getCurrentConfig().getInteger(key, defaultValue);
+ }
+
+ @Override
+ public Iterator<String> getKeys()
+ {
+ return this.getCurrentConfig().getKeys();
+ }
+
+ @Override
+ public Iterator<String> getKeys(String prefix)
+ {
+ return this.getCurrentConfig().getKeys(prefix);
+ }
+
+ @Override
+ public List<Object> getList(String key, List<?> defaultValue)
+ {
+ return this.getCurrentConfig().getList(key, defaultValue);
+ }
+
+ @Override
+ public List<Object> getList(String key)
+ {
+ return this.getCurrentConfig().getList(key);
+ }
+
+ @Override
+ public long getLong(String key, long defaultValue)
+ {
+ return this.getCurrentConfig().getLong(key, defaultValue);
+ }
+
+ @Override
+ public Long getLong(String key, Long defaultValue)
+ {
+ return this.getCurrentConfig().getLong(key, defaultValue);
+ }
+
+ @Override
+ public long getLong(String key)
+ {
+ return this.getCurrentConfig().getLong(key);
+ }
+
+ @Override
+ public Properties getProperties(String key)
+ {
+ return this.getCurrentConfig().getProperties(key);
+ }
+
+ @Override
+ public Object getProperty(String key)
+ {
+ return this.getCurrentConfig().getProperty(key);
+ }
+
+ @Override
+ public short getShort(String key, short defaultValue)
+ {
+ return this.getCurrentConfig().getShort(key, defaultValue);
+ }
+
+ @Override
+ public Short getShort(String key, Short defaultValue)
+ {
+ return this.getCurrentConfig().getShort(key, defaultValue);
+ }
+
+ @Override
+ public short getShort(String key)
+ {
+ return this.getCurrentConfig().getShort(key);
+ }
+
+ @Override
+ public String getString(String key, String defaultValue)
+ {
+ return this.getCurrentConfig().getString(key, defaultValue);
+ }
+
+ @Override
+ public String getString(String key)
+ {
+ return this.getCurrentConfig().getString(key);
+ }
+
+ @Override
+ public String[] getStringArray(String key)
+ {
+ return this.getCurrentConfig().getStringArray(key);
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ return this.getCurrentConfig().isEmpty();
+ }
+
+ @Override
+ public void setProperty(String key, Object value)
+ {
+ if (configs != null)
+ {
+ this.getCurrentConfig().setProperty(key, value);
+ }
+ }
+
+ @Override
+ public Configuration subset(String prefix)
+ {
+ return this.getCurrentConfig().subset(prefix);
+ }
+
+ @Override
+ public Node getRoot()
+ {
+ return this.getCurrentConfig().getRoot();
+ }
+
+ @Override
+ public void setRoot(Node node)
+ {
+ if (configs != null)
+ {
+ this.getCurrentConfig().setRoot(node);
+ }
+ else
+ {
+ super.setRoot(node);
+ }
+ }
+
+ @Override
+ public ExpressionEngine getExpressionEngine()
+ {
+ return super.getExpressionEngine();
+ }
+
+ @Override
+ public void setExpressionEngine(ExpressionEngine expressionEngine)
+ {
+ super.setExpressionEngine(expressionEngine);
+ }
+
+ @Override
+ public void addNodes(String key, Collection<? extends ConfigurationNode> nodes)
+ {
+ this.getCurrentConfig().addNodes(key, nodes);
+ }
+
+ @Override
+ public SubnodeConfiguration configurationAt(String key, boolean supportUpdates)
+ {
+ return this.getCurrentConfig().configurationAt(key, supportUpdates);
+ }
+
+ @Override
+ public SubnodeConfiguration configurationAt(String key)
+ {
+ return this.getCurrentConfig().configurationAt(key);
+ }
+
+ @Override
+ public List<HierarchicalConfiguration> configurationsAt(String key)
+ {
+ return this.getCurrentConfig().configurationsAt(key);
+ }
+
+ @Override
+ public void clearTree(String key)
+ {
+ this.getCurrentConfig().clearTree(key);
+ }
+
+ @Override
+ public int getMaxIndex(String key)
+ {
+ return this.getCurrentConfig().getMaxIndex(key);
+ }
+
+ @Override
+ public Configuration interpolatedConfiguration()
+ {
+ return this.getCurrentConfig().interpolatedConfiguration();
+ }
+
+
+ /**
+ * Returns the configuration source, in which the specified key is defined.
+ * This method will determine the configuration node that is identified by
+ * the given key. The following constellations are possible:
+ * <ul>
+ * <li>If no node object is found for this key, <b>null</b> is returned.</li>
+ * <li>If the key maps to multiple nodes belonging to different
+ * configuration sources, a {@code IllegalArgumentException} is
+ * thrown (in this case no unique source can be determined).</li>
+ * <li>If exactly one node is found for the key, the (child) configuration
+ * object, to which the node belongs is determined and returned.</li>
+ * <li>For keys that have been added directly to this combined
+ * configuration and that do not belong to the namespaces defined by
+ * existing child configurations this configuration will be returned.</li>
+ * </ul>
+ *
+ * @param key the key of a configuration property
+ * @return the configuration, to which this property belongs or <b>null</b>
+ * if the key cannot be resolved
+ * @throws IllegalArgumentException if the key maps to multiple properties
+ * and the source cannot be determined, or if the key is <b>null</b>
+ */
+ @Override
+ public Configuration getSource(String key)
+ {
+ if (key == null)
+ {
+ throw new IllegalArgumentException("Key must not be null!");
+ }
+ return getCurrentConfig().getSource(key);
+ }
+
+ @Override
+ public void addConfigurationListener(ConfigurationListener l)
+ {
+ super.addConfigurationListener(l);
+
+ for (CombinedConfiguration cc : configs.values())
+ {
+ cc.addConfigurationListener(l);
+ }
+ }
+
+ @Override
+ public boolean removeConfigurationListener(ConfigurationListener l)
+ {
+ for (CombinedConfiguration cc : configs.values())
+ {
+ cc.removeConfigurationListener(l);
+ }
+ return super.removeConfigurationListener(l);
+ }
+
+ @Override
+ public Collection<ConfigurationListener> getConfigurationListeners()
+ {
+ return super.getConfigurationListeners();
+ }
+
+ @Override
+ public void clearConfigurationListeners()
+ {
+ for (CombinedConfiguration cc : configs.values())
+ {
+ cc.clearConfigurationListeners();
+ }
+ super.clearConfigurationListeners();
+ }
+
+ @Override
+ public void addErrorListener(ConfigurationErrorListener l)
+ {
+ for (CombinedConfiguration cc : configs.values())
+ {
+ cc.addErrorListener(l);
+ }
+ super.addErrorListener(l);
+ }
+
+ @Override
+ public boolean removeErrorListener(ConfigurationErrorListener l)
+ {
+ for (CombinedConfiguration cc : configs.values())
+ {
+ cc.removeErrorListener(l);
+ }
+ return super.removeErrorListener(l);
+ }
+
+ @Override
+ public void clearErrorListeners()
+ {
+ for (CombinedConfiguration cc : configs.values())
+ {
+ cc.clearErrorListeners();
+ }
+ super.clearErrorListeners();
+ }
+
+ @Override
+ public Collection<ConfigurationErrorListener> getErrorListeners()
+ {
+ return super.getErrorListeners();
+ }
+
+ /**
+ * Returns a copy of this object. This implementation performs a deep clone,
+ * i.e. all contained configurations will be cloned, too. For this to work,
+ * all contained configurations must be cloneable. Registered event
+ * listeners won't be cloned. The clone will use the same node combiner than
+ * the original.
+ *
+ * @return the copied object
+ */
+ @Override
+ public Object clone()
+ {
+ return super.clone();
+ }
+
+ /**
+ * Invalidates the current combined configuration. This means that the next time a
+ * property is accessed the combined node structure must be re-constructed.
+ * Invalidation of a combined configuration also means that an event of type
+ * {@code EVENT_COMBINED_INVALIDATE} is fired. Note that while other
+ * events most times appear twice (once before and once after an update),
+ * this event is only fired once (after update).
+ */
+ @Override
+ public void invalidate()
+ {
+ getCurrentConfig().invalidate();
+ }
+
+ public void invalidateAll()
+ {
+ if (configs == null)
+ {
+ return;
+ }
+ for (CombinedConfiguration cc : configs.values())
+ {
+ cc.invalidate();
+ }
+ }
+
+ /*
+ * Don't allow resolveContainerStore to be called recursively.
+ * @param key The key to resolve.
+ * @return The value of the key.
+ */
+ @Override
+ protected Object resolveContainerStore(String key)
+ {
+ if (recursive.get().booleanValue())
+ {
+ return null;
+ }
+ recursive.set(Boolean.TRUE);
+ try
+ {
+ return super.resolveContainerStore(key);
+ }
+ finally
+ {
+ recursive.set(Boolean.FALSE);
+ }
+ }
+
+ private CombinedConfiguration getCurrentConfig()
+ {
+ String key = localSubst.replace(keyPattern);
+ CombinedConfiguration config = configs.get(key);
+ // The double-checked works here due to the Thread guarantees of ConcurrentMap.
+ if (config == null)
+ {
+ synchronized (configs)
+ {
+ config = configs.get(key);
+ if (config == null)
+ {
+ config = new CombinedConfiguration(getNodeCombiner());
+ if (loggerName != null)
+ {
+ Log log = LogFactory.getLog(loggerName);
+ if (log != null)
+ {
+ config.setLogger(log);
+ }
+ }
+ config.setIgnoreReloadExceptions(isIgnoreReloadExceptions());
+ config.setExpressionEngine(this.getExpressionEngine());
+ config.setDelimiterParsingDisabled(isDelimiterParsingDisabled());
+ config.setConversionExpressionEngine(getConversionExpressionEngine());
+ config.setListDelimiter(getListDelimiter());
+ for (ConfigurationErrorListener listener : getErrorListeners())
+ {
+ config.addErrorListener(listener);
+ }
+ for (ConfigurationListener listener : getConfigurationListeners())
+ {
+ config.addConfigurationListener(listener);
+ }
+ config.setForceReloadCheck(isForceReloadCheck());
+ for (ConfigData data : configurations)
+ {
+ config.addConfiguration(data.getConfiguration(), data.getName(), data.getAt());
+ }
+ configs.put(key, config);
+ }
+ }
+ }
+ if (getLogger().isDebugEnabled())
+ {
+ getLogger().debug("Returning config for " + key + ": " + config);
+ }
+ return config;
+ }
+
+ /**
+ * Internal class that identifies each Configuration.
+ */
+ static class ConfigData
+ {
+ /** Stores a reference to the configuration. */
+ private AbstractConfiguration configuration;
+
+ /** Stores the name under which the configuration is stored. */
+ private String name;
+
+ /** Stores the at string.*/
+ private String at;
+
+ /**
+ * Creates a new instance of {@code ConfigData} and initializes
+ * it.
+ *
+ * @param config the configuration
+ * @param n the name
+ * @param at the at position
+ */
+ public ConfigData(AbstractConfiguration config, String n, String at)
+ {
+ configuration = config;
+ name = n;
+ this.at = at;
+ }
+
+ /**
+ * Returns the stored configuration.
+ *
+ * @return the configuration
+ */
+ public AbstractConfiguration getConfiguration()
+ {
+ return configuration;
+ }
+
+ /**
+ * Returns the configuration's name.
+ *
+ * @return the name
+ */
+ public String getName()
+ {
+ return name;
+ }
+
+ /**
+ * Returns the at position of this configuration.
+ *
+ * @return the at position
+ */
+ public String getAt()
+ {
+ return at;
+ }
+
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/EnvironmentConfiguration.java b/src/main/java/org/apache/commons/configuration/EnvironmentConfiguration.java
new file mode 100644
index 0000000..e6c03db
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/EnvironmentConfiguration.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.util.HashMap;
+
+/**
+ * <p>A Configuration implementation that reads the platform specific
+ * environment variables using the map returned by {@link System#getenv()}.</p>
+ *
+ * <p>This configuration implementation is read-only. It allows read access to the
+ * defined OS environment variables, but their values cannot be changed. Any
+ * attempts to add or remove a property will throw an
+ * {@link UnsupportedOperationException}</p>
+ *
+ * <p>Usage of this class is easy: After an instance has been created the get
+ * methods provided by the {@code Configuration} interface can be used
+ * for querying environment variables, e.g.:</p>
+ *
+ * <pre>
+ * Configuration envConfig = new EnvironmentConfiguration();
+ * System.out.println("JAVA_HOME=" + envConfig.getString("JAVA_HOME");
+ * </pre>
+ *
+ * @author <a href="mailto:nicolas.deloof at gmail.com">Nicolas De Loof</a>
+ * @version $Id: EnvironmentConfiguration.java 1210171 2011-12-04 18:32:07Z oheger $
+ * @since 1.5
+ */
+public class EnvironmentConfiguration extends MapConfiguration
+{
+ /**
+ * Create a Configuration based on the environment variables.
+ *
+ * @see System#getenv()
+ */
+ public EnvironmentConfiguration()
+ {
+ super(new HashMap<String, Object>(System.getenv()));
+ }
+
+ /**
+ * Adds a property to this configuration. Because this configuration is
+ * read-only, this operation is not allowed and will cause an exception.
+ *
+ * @param key the key of the property to be added
+ * @param value the property value
+ */
+ @Override
+ protected void addPropertyDirect(String key, Object value)
+ {
+ throw new UnsupportedOperationException("EnvironmentConfiguration is read-only!");
+ }
+
+ /**
+ * Removes a property from this configuration. Because this configuration is
+ * read-only, this operation is not allowed and will cause an exception.
+ *
+ * @param key the key of the property to be removed
+ */
+ @Override
+ public void clearProperty(String key)
+ {
+ throw new UnsupportedOperationException("EnvironmentConfiguration is read-only!");
+ }
+
+ /**
+ * Removes all properties from this configuration. Because this
+ * configuration is read-only, this operation is not allowed and will cause
+ * an exception.
+ */
+ @Override
+ public void clear()
+ {
+ throw new UnsupportedOperationException("EnvironmentConfiguration is read-only!");
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/FileConfiguration.java b/src/main/java/org/apache/commons/configuration/FileConfiguration.java
new file mode 100644
index 0000000..8ebf8e1
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/FileConfiguration.java
@@ -0,0 +1,293 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URL;
+
+import org.apache.commons.configuration.reloading.ReloadingStrategy;
+
+/**
+ * A persistent configuration loaded and saved to a file.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: FileConfiguration.java 1209883 2011-12-03 10:56:57Z oheger $
+ * @since 1.0-rc2
+ */
+public interface FileConfiguration extends Configuration
+{
+ /**
+ * Load the configuration from the underlying URL. If the URL is not
+ * specified, it attempts to locate the specified file name.
+ *
+ * @throws ConfigurationException if an error occurs during the load operation
+ */
+ void load() throws ConfigurationException;
+
+ /**
+ * Locate the specified file and load the configuration.
+ *
+ * @param fileName the name of the file loaded
+ *
+ * @throws ConfigurationException if an error occurs during the load operation
+ */
+ void load(String fileName) throws ConfigurationException;
+
+ /**
+ * Load the configuration from the specified file.
+ *
+ * @param file the loaded file
+ *
+ * @throws ConfigurationException if an error occurs during the load operation
+ */
+ void load(File file) throws ConfigurationException;
+
+ /**
+ * Load the configuration from the specified URL.
+ *
+ * @param url the URL of the file loaded
+ *
+ * @throws ConfigurationException if an error occurs during the load operation
+ */
+ void load(URL url) throws ConfigurationException;
+
+ /**
+ * Load the configuration from the specified stream, using the encoding
+ * returned by {@link #getEncoding()}.
+ *
+ * @param in the input stream
+ *
+ * @throws ConfigurationException if an error occurs during the load operation
+ */
+ void load(InputStream in) throws ConfigurationException;
+
+ /**
+ * Load the configuration from the specified stream, using the specified
+ * encoding. If the encoding is null the default encoding is used.
+ *
+ * @param in the input stream
+ * @param encoding the encoding used. {@code null} to use the default encoding
+ *
+ * @throws ConfigurationException if an error occurs during the load operation
+ */
+ void load(InputStream in, String encoding) throws ConfigurationException;
+
+ /**
+ * Load the configuration from the specified reader.
+ *
+ * @param in the reader
+ *
+ * @throws ConfigurationException if an error occurs during the load operation
+ */
+ void load(Reader in) throws ConfigurationException;
+
+ /**
+ * Save the configuration.
+ *
+ * @throws ConfigurationException if an error occurs during the save operation
+ */
+ void save() throws ConfigurationException;
+
+ /**
+ * Save the configuration to the specified file.
+ *
+ * @param fileName the name of the file to be saved
+ *
+ * @throws ConfigurationException if an error occurs during the save operation
+ */
+ void save(String fileName) throws ConfigurationException;
+
+ /**
+ * Save the configuration to the specified file.
+ *
+ * @param file specifies the file to be saved
+ *
+ * @throws ConfigurationException if an error occurs during the save operation
+ */
+ void save(File file) throws ConfigurationException;
+
+ /**
+ * Save the configuration to the specified URL.
+ *
+ * @param url the URL
+ *
+ * @throws ConfigurationException if an error occurs during the save operation
+ */
+ void save(URL url) throws ConfigurationException;
+
+ /**
+ * Save the configuration to the specified stream, using the encoding
+ * returned by {@link #getEncoding()}.
+ *
+ * @param out the output stream
+ *
+ * @throws ConfigurationException if an error occurs during the save operation
+ */
+ void save(OutputStream out) throws ConfigurationException;
+
+ /**
+ * Save the configuration to the specified stream, using the specified
+ * encoding. If the encoding is null the default encoding is used.
+ *
+ * @param out the output stream
+ * @param encoding the encoding to be used
+ * @throws ConfigurationException if an error occurs during the save operation
+ */
+ void save(OutputStream out, String encoding) throws ConfigurationException;
+
+ /**
+ * Save the configuration to the specified writer.
+ *
+ * @param out the writer
+ *
+ * @throws ConfigurationException if an error occurs during the save operation
+ */
+ void save(Writer out) throws ConfigurationException;
+
+ /**
+ * Return the name of the file.
+ *
+ * @return the file name
+ */
+ String getFileName();
+
+ /**
+ * Set the name of the file.
+ *
+ * @param fileName the name of the file
+ */
+ void setFileName(String fileName);
+
+ /**
+ * Returns the base path. One way to specify the location of a configuration
+ * source is by setting its base path and its file name. This method returns
+ * this base path. The concrete value returned by this method depends on the
+ * way the location of the configuration file was set. If methods like
+ * {@code setFile()} or {@code setURL()} were used, the base
+ * path typically points to the parent directory of the configuration file
+ * (e.g. for the URL {@code file:/temp/test.properties} the base path
+ * will be {@code file:/temp/}). If the base path was explicitly set
+ * using {@code setBasePath()}, this method will return the exact
+ * value specified here without further modifications.
+ *
+ * @return the base path
+ * @see AbstractFileConfiguration#setBasePath(String)
+ */
+ String getBasePath();
+
+ /**
+ * Sets the base path. The methods {@code setBasePath()} and
+ * {@code setFileName()} can be used together to specify the location
+ * of the configuration file to be loaded. If relative file names are to
+ * be resolved (e.g. for the include files supported by
+ * {@code PropertiesConfiguration}), this base path will be used.
+ *
+ * @param basePath the base path.
+ */
+ void setBasePath(String basePath);
+
+ /**
+ * Return the file where the configuration is stored.
+ *
+ * @return the configuration file
+ */
+ File getFile();
+
+ /**
+ * Set the file where the configuration is stored.
+ *
+ * @param file the file
+ */
+ void setFile(File file);
+
+ /**
+ * Return the URL where the configuration is stored.
+ *
+ * @return the URL of the configuration
+ */
+ URL getURL();
+
+ /**
+ * The URL where the configuration is stored.
+ *
+ * @param url the URL
+ */
+ void setURL(URL url);
+
+ /**
+ * Enable or disable the automatically saving of modified properties to the disk.
+ *
+ * @param autoSave {@code true} to enable, {@code false} to disable
+ * @since 1.1
+ */
+ void setAutoSave(boolean autoSave);
+
+ /**
+ * Tells if properties are automatically saved to the disk.
+ *
+ * @return {@code true} if auto-saving is enabled, {@code false} otherwise
+ * @since 1.1
+ */
+ boolean isAutoSave();
+
+ /**
+ * Return the reloading strategy.
+ *
+ * @return the reloading strategy currently used
+ * @since 1.1
+ */
+ ReloadingStrategy getReloadingStrategy();
+
+ /**
+ * Set the reloading strategy.
+ *
+ * @param strategy the reloading strategy to use
+ * @since 1.1
+ */
+ void setReloadingStrategy(ReloadingStrategy strategy);
+
+ /**
+ * Reload the configuration.
+ *
+ * @since 1.1
+ */
+ void reload();
+
+ /**
+ * Return the encoding used to store the configuration file. If the value
+ * is null the default encoding is used.
+ *
+ * @return the current encoding
+ * @since 1.1
+ */
+ String getEncoding();
+
+ /**
+ * Set the encoding used to store the configuration file. Set the encoding
+ * to null to use the default encoding.
+ *
+ * @param encoding the encoding to use
+ * @since 1.1
+ */
+ void setEncoding(String encoding);
+
+}
diff --git a/src/main/java/org/apache/commons/configuration/FileOptionsProvider.java b/src/main/java/org/apache/commons/configuration/FileOptionsProvider.java
new file mode 100644
index 0000000..d4a789e
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/FileOptionsProvider.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.util.Map;
+
+/**
+ * Some FileSystems allow options to be passed on File operations. Users of commons
+ * configuration can implement this interface and register it with the FileSystem.
+ * @since 1.7
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons Configuration team</a>
+ * @version $Id: FileOptionsProvider.java 1209884 2011-12-03 10:57:38Z oheger $
+ */
+public interface FileOptionsProvider
+{
+ /**
+ * Key used to identify the user to be associated with the current file operations.
+ * The value associated with this key is a String identifying the current user.
+ */
+ String CURRENT_USER = "currentUser";
+
+ /**
+ * Key used to indicate whether Webdav versioning support should be enabled.
+ * The value associated with this key is a Boolean where True indicates versioning should
+ * be enabled.
+ */
+ String VERSIONING = "versioning";
+
+ /**
+ * Key used to identify the proxy host to connect through.
+ * The value associated with this key is a String identifying the host name of the proxy.
+ */
+ String PROXY_HOST = "proxyHost";
+
+ /**
+ * Key used to identify the proxy port to connect through.
+ * The value associated with this key is an Integer identifying the port on the proxy.
+ */
+ String PROXY_PORT = "proxyPort";
+
+ /**
+ * Key used to identify the maximum number of connections allowed to a single host.
+ * The value associated with this key is an Integer identifying the maximum number of
+ * connections allowed to a single host.
+ */
+ String MAX_HOST_CONNECTIONS = "maxHostConnections";
+
+ /**
+ * Key used to identify the maximum number of connections allowed to all hosts.
+ * The value associated with this key is an Integer identifying the maximum number of
+ * connections allowed to all hosts.
+ */
+ String MAX_TOTAL_CONNECTIONS = "maxTotalConnections";
+
+ /**
+ *
+ * @return Options to be used for this file.
+ */
+ Map<String, Object> getOptions();
+}
diff --git a/src/main/java/org/apache/commons/configuration/FileSystem.java b/src/main/java/org/apache/commons/configuration/FileSystem.java
new file mode 100644
index 0000000..4740807
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/FileSystem.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.logging.impl.NoOpLog;
+
+/**
+ * Abstract layer to allow various types of file systems.
+ * @since 1.7
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons Configuration team</a>
+ * @version $Id: FileSystem.java 1209996 2011-12-03 20:24:21Z oheger $
+ */
+public abstract class FileSystem
+{
+ /** The name of the system property that can be used to set the file system class name */
+ private static final String FILE_SYSTEM = "org.apache.commons.configuration.filesystem";
+
+ /** The default file system */
+ private static FileSystem fileSystem;
+
+ /** The Logger */
+ private Log log;
+
+ /** FileSystem options provider */
+ private FileOptionsProvider optionsProvider;
+
+ public FileSystem()
+ {
+ setLogger(null);
+ }
+
+ /**
+ * Returns the logger used by this FileSystem.
+ *
+ * @return the logger
+ */
+ public Log getLogger()
+ {
+ return log;
+ }
+
+ /**
+ * Allows to set the logger to be used by this FileSystem. This
+ * method makes it possible for clients to exactly control logging behavior.
+ * Per default a logger is set that will ignore all log messages. Derived
+ * classes that want to enable logging should call this method during their
+ * initialization with the logger to be used.
+ *
+ * @param log the new logger
+ */
+ public void setLogger(Log log)
+ {
+ this.log = (log != null) ? log : new NoOpLog();
+ }
+
+ static
+ {
+ String fsClassName = System.getProperty(FILE_SYSTEM);
+ if (fsClassName != null)
+ {
+ Log log = LogFactory.getLog(FileSystem.class);
+
+ try
+ {
+ Class<?> clazz = Class.forName(fsClassName);
+ if (FileSystem.class.isAssignableFrom(clazz))
+ {
+ fileSystem = (FileSystem) clazz.newInstance();
+ if (log.isDebugEnabled())
+ {
+ log.debug("Using " + fsClassName);
+ }
+ }
+ }
+ catch (InstantiationException ex)
+ {
+ log.error("Unable to create " + fsClassName, ex);
+ }
+ catch (IllegalAccessException ex)
+ {
+ log.error("Unable to create " + fsClassName, ex);
+ }
+ catch (ClassNotFoundException ex)
+ {
+ log.error("Unable to create " + fsClassName, ex);
+ }
+ }
+
+ if (fileSystem == null)
+ {
+ fileSystem = new DefaultFileSystem();
+ }
+ }
+
+ /**
+ * Set the FileSystem to use.
+ * @param fs The FileSystem
+ * @throws NullPointerException if the FileSystem parameter is null.
+ */
+ public static void setDefaultFileSystem(FileSystem fs) throws NullPointerException
+ {
+ if (fs == null)
+ {
+ throw new NullPointerException("A FileSystem implementation is required");
+ }
+ fileSystem = fs;
+ }
+
+ /**
+ * Reset the FileSystem to the default.
+ */
+ public static void resetDefaultFileSystem()
+ {
+ fileSystem = new DefaultFileSystem();
+ }
+
+ /**
+ * Retrieve the FileSystem being used.
+ * @return The FileSystem.
+ */
+ public static FileSystem getDefaultFileSystem()
+ {
+ return fileSystem;
+ }
+
+ /**
+ * Set the FileOptionsProvider
+ * @param provider The FileOptionsProvider
+ */
+ public void setFileOptionsProvider(FileOptionsProvider provider)
+ {
+ this.optionsProvider = provider;
+ }
+
+ public FileOptionsProvider getFileOptionsProvider()
+ {
+ return this.optionsProvider;
+ }
+
+ public abstract InputStream getInputStream(String basePath, String fileName)
+ throws ConfigurationException;
+
+ public abstract InputStream getInputStream(URL url) throws ConfigurationException;
+
+ public abstract OutputStream getOutputStream(URL url) throws ConfigurationException;
+
+ public abstract OutputStream getOutputStream(File file) throws ConfigurationException;
+
+ public abstract String getPath(File file, URL url, String basePath, String fileName);
+
+ public abstract String getBasePath(String path);
+
+ public abstract String getFileName(String path);
+
+ public abstract URL locateFromURL(String basePath, String fileName);
+
+ public abstract URL getURL(String basePath, String fileName) throws MalformedURLException;
+}
diff --git a/src/main/java/org/apache/commons/configuration/FileSystemBased.java b/src/main/java/org/apache/commons/configuration/FileSystemBased.java
new file mode 100644
index 0000000..a304a16
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/FileSystemBased.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+/**
+ * Interface used to install or locate the FileSystem
+ * @since 1.7
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons Configuration team</a>
+ */
+public interface FileSystemBased
+{
+ void setFileSystem(FileSystem fileSystem);
+
+ void resetFileSystem();
+
+ FileSystem getFileSystem();
+}
diff --git a/src/main/java/org/apache/commons/configuration/HierarchicalConfiguration.java b/src/main/java/org/apache/commons/configuration/HierarchicalConfiguration.java
new file mode 100644
index 0000000..a125ca9
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/HierarchicalConfiguration.java
@@ -0,0 +1,1769 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.Stack;
+
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.ConfigurationNodeVisitorAdapter;
+import org.apache.commons.configuration.tree.DefaultConfigurationNode;
+import org.apache.commons.configuration.tree.DefaultExpressionEngine;
+import org.apache.commons.configuration.tree.ExpressionEngine;
+import org.apache.commons.configuration.tree.NodeAddData;
+import org.apache.commons.configuration.tree.ViewNode;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * <p>A specialized configuration class that extends its base class by the
+ * ability of keeping more structure in the stored properties.</p><p>There
+ * are some sources of configuration data that cannot be stored very well in a
+ * {@code BaseConfiguration} object because then their structure is lost.
+ * This is especially true for XML documents. This class can deal with such
+ * structured configuration sources by storing the properties in a tree-like
+ * organization.</p><p>The internal used storage form allows for a more
+ * sophisticated access to single properties. As an example consider the
+ * following XML document:</p><p>
+ *
+ * <pre>
+ * <database>
+ * <tables>
+ * <table>
+ * <name>users</name>
+ * <fields>
+ * <field>
+ * <name>lid</name>
+ * <type>long</name>
+ * </field>
+ * <field>
+ * <name>usrName</name>
+ * <type>java.lang.String</type>
+ * </field>
+ * ...
+ * </fields>
+ * </table>
+ * <table>
+ * <name>documents</name>
+ * <fields>
+ * <field>
+ * <name>docid</name>
+ * <type>long</type>
+ * </field>
+ * ...
+ * </fields>
+ * </table>
+ * ...
+ * </tables>
+ * </database>
+ * </pre>
+ *
+ * </p><p>If this document is parsed and stored in a
+ * {@code HierarchicalConfiguration} object (which can be done by one of
+ * the sub classes), there are enhanced possibilities of accessing properties.
+ * The keys for querying information can contain indices that select a certain
+ * element if there are multiple hits.</p><p>For instance the key
+ * {@code tables.table(0).name} can be used to find out the name of the
+ * first table. In opposite {@code tables.table.name} would return a
+ * collection with the names of all available tables. Similarly the key
+ * {@code tables.table(1).fields.field.name} returns a collection with
+ * the names of all fields of the second table. If another index is added after
+ * the {@code field} element, a single field can be accessed:
+ * {@code tables.table(1).fields.field(0).name}.</p><p>There is a
+ * {@code getMaxIndex()} method that returns the maximum allowed index
+ * that can be added to a given property key. This method can be used to iterate
+ * over all values defined for a certain property.</p>
+ * <p>Since the 1.3 release of <em>Commons Configuration</em> hierarchical
+ * configurations support an <em>expression engine</em>. This expression engine
+ * is responsible for evaluating the passed in configuration keys and map them
+ * to the stored properties. The examples above are valid for the default
+ * expression engine, which is used when a new {@code HierarchicalConfiguration}
+ * instance is created. With the {@code setExpressionEngine()} method a
+ * different expression engine can be set. For instance with
+ * {@link org.apache.commons.configuration.tree.xpath.XPathExpressionEngine}
+ * there is an expression engine available that supports configuration keys in
+ * XPATH syntax.</p>
+ * <p>In addition to the events common for all configuration classes hierarchical
+ * configurations support some more events that correspond to some specific
+ * methods and features:
+ * <dl><dt><em>EVENT_ADD_NODES</em></dt><dd>The {@code addNodes()} method
+ * was called; the event object contains the key, to which the nodes were added,
+ * and a collection with the new nodes as value.</dd>
+ * <dt><em>EVENT_CLEAR_TREE</em></dt><dd>The {@code clearTree()} method was
+ * called; the event object stores the key of the removed sub tree.</dd>
+ * <dt><em>EVENT_SUBNODE_CHANGED</em></dt><dd>A {@code SubnodeConfiguration}
+ * that was created from this configuration has been changed. The value property
+ * of the event object contains the original event object as it was sent by the
+ * subnode configuration.</dd></dl></p>
+ * <p><em>Note:</em>Configuration objects of this type can be read concurrently
+ * by multiple threads. However if one of these threads modifies the object,
+ * synchronization has to be performed manually.</p>
+ *
+ * @author Oliver Heger
+ * @version $Id: HierarchicalConfiguration.java 1330666 2012-04-26 06:12:30Z oheger $
+ */
+public class HierarchicalConfiguration extends AbstractConfiguration implements Serializable, Cloneable
+{
+ /**
+ * Constant for the clear tree event.
+ * @since 1.3
+ */
+ public static final int EVENT_CLEAR_TREE = 10;
+
+ /**
+ * Constant for the add nodes event.
+ * @since 1.3
+ */
+ public static final int EVENT_ADD_NODES = 11;
+
+ /**
+ * Constant for the subnode configuration modified event.
+ * @since 1.5
+ */
+ public static final int EVENT_SUBNODE_CHANGED = 12;
+
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = 3373812230395363192L;
+
+ /** Stores the default expression engine to be used for new objects.*/
+ private static ExpressionEngine defaultExpressionEngine;
+
+ /** Stores the root node of this configuration. This field is required for
+ * backwards compatibility only.
+ */
+ private Node root;
+
+ /** Stores the root configuration node.*/
+ private ConfigurationNode rootNode;
+
+ /** Stores the expression engine for this instance.*/
+ private transient ExpressionEngine expressionEngine;
+
+ /**
+ * Creates a new instance of {@code HierarchicalConfiguration}.
+ */
+ public HierarchicalConfiguration()
+ {
+ setRootNode(new Node());
+ }
+
+ /**
+ * Creates a new instance of {@code HierarchicalConfiguration} and
+ * copies all data contained in the specified configuration into the new
+ * one.
+ *
+ * @param c the configuration that is to be copied (if <b>null</b>, this
+ * constructor will behave like the standard constructor)
+ * @since 1.4
+ */
+ public HierarchicalConfiguration(HierarchicalConfiguration c)
+ {
+ this();
+ if (c != null)
+ {
+ CloneVisitor visitor = new CloneVisitor();
+ c.getRootNode().visit(visitor);
+ setRootNode(visitor.getClone());
+ }
+ }
+
+ /**
+ * Returns the object to synchronize on a reload. This class is not
+ * reloadable so this object isn't important
+ *
+ * @return the lock object
+ */
+ public Object getReloadLock()
+ {
+ return this;
+ }
+
+ /**
+ * Returns the root node of this hierarchical configuration. This method
+ * exists for backwards compatibility only. New code should use the
+ * {@link #getRootNode()} method instead, which operates on
+ * the preferred data type {@code ConfigurationNode}.
+ *
+ * @return the root node
+ */
+ public Node getRoot()
+ {
+ if (root == null && rootNode != null)
+ {
+ // Dynamically create a snapshot of the root node
+ return new Node(rootNode);
+ }
+
+ return root;
+ }
+
+ /**
+ * Sets the root node of this hierarchical configuration. This method
+ * exists for backwards compatibility only. New code should use the
+ * {@link #setRootNode(ConfigurationNode)} method instead,
+ * which operates on the preferred data type {@code ConfigurationNode}.
+ *
+ * @param node the root node
+ */
+ public void setRoot(Node node)
+ {
+ if (node == null)
+ {
+ throw new IllegalArgumentException("Root node must not be null!");
+ }
+ root = node;
+ rootNode = null;
+ }
+
+ /**
+ * Returns the root node of this hierarchical configuration.
+ *
+ * @return the root node
+ * @since 1.3
+ */
+ public ConfigurationNode getRootNode()
+ {
+ return (rootNode != null) ? rootNode : root;
+ }
+
+ /**
+ * Sets the root node of this hierarchical configuration.
+ *
+ * @param rootNode the root node
+ * @since 1.3
+ */
+ public void setRootNode(ConfigurationNode rootNode)
+ {
+ if (rootNode == null)
+ {
+ throw new IllegalArgumentException("Root node must not be null!");
+ }
+ this.rootNode = rootNode;
+
+ // For backward compatibility also set the old root field.
+ root = (rootNode instanceof Node) ? (Node) rootNode : null;
+ }
+
+ /**
+ * Returns the default expression engine.
+ *
+ * @return the default expression engine
+ * @since 1.3
+ */
+ public static synchronized ExpressionEngine getDefaultExpressionEngine()
+ {
+ if (defaultExpressionEngine == null)
+ {
+ defaultExpressionEngine = new DefaultExpressionEngine();
+ }
+ return defaultExpressionEngine;
+ }
+
+ /**
+ * Sets the default expression engine. This expression engine will be used
+ * if no specific engine was set for an instance. It is shared between all
+ * hierarchical configuration instances. So modifying its properties will
+ * impact all instances, for which no specific engine is set.
+ *
+ * @param engine the new default expression engine
+ * @since 1.3
+ */
+ public static synchronized void setDefaultExpressionEngine(ExpressionEngine engine)
+ {
+ if (engine == null)
+ {
+ throw new IllegalArgumentException(
+ "Default expression engine must not be null!");
+ }
+ defaultExpressionEngine = engine;
+ }
+
+ /**
+ * Returns the expression engine used by this configuration. This method
+ * will never return <b>null</b>; if no specific expression engine was set,
+ * the default expression engine will be returned.
+ *
+ * @return the current expression engine
+ * @since 1.3
+ */
+ public ExpressionEngine getExpressionEngine()
+ {
+ return (expressionEngine != null) ? expressionEngine
+ : getDefaultExpressionEngine();
+ }
+
+ /**
+ * Sets the expression engine to be used by this configuration. All property
+ * keys this configuration has to deal with will be interpreted by this
+ * engine.
+ *
+ * @param expressionEngine the new expression engine; can be <b>null</b>,
+ * then the default expression engine will be used
+ * @since 1.3
+ */
+ public void setExpressionEngine(ExpressionEngine expressionEngine)
+ {
+ this.expressionEngine = expressionEngine;
+ }
+
+ /**
+ * Fetches the specified property. This task is delegated to the associated
+ * expression engine.
+ *
+ * @param key the key to be looked up
+ * @return the found value
+ */
+ public Object getProperty(String key)
+ {
+ List<ConfigurationNode> nodes = fetchNodeList(key);
+
+ if (nodes.size() == 0)
+ {
+ return null;
+ }
+ else
+ {
+ List<Object> list = new ArrayList<Object>();
+ for (ConfigurationNode node : nodes)
+ {
+ if (node.getValue() != null)
+ {
+ list.add(node.getValue());
+ }
+ }
+
+ if (list.size() < 1)
+ {
+ return null;
+ }
+ else
+ {
+ return (list.size() == 1) ? list.get(0) : list;
+ }
+ }
+ }
+
+ /**
+ * Adds the property with the specified key. This task will be delegated to
+ * the associated {@code ExpressionEngine}, so the passed in key
+ * must match the requirements of this implementation.
+ *
+ * @param key the key of the new property
+ * @param obj the value of the new property
+ */
+ @Override
+ protected void addPropertyDirect(String key, Object obj)
+ {
+ NodeAddData data = getExpressionEngine().prepareAdd(getRootNode(), key);
+ ConfigurationNode node = processNodeAddData(data);
+ node.setValue(obj);
+ }
+
+ /**
+ * Adds a collection of nodes at the specified position of the configuration
+ * tree. This method works similar to {@code addProperty()}, but
+ * instead of a single property a whole collection of nodes can be added -
+ * and thus complete configuration sub trees. E.g. with this method it is
+ * possible to add parts of another {@code HierarchicalConfiguration}
+ * object to this object. (However be aware that a
+ * {@code ConfigurationNode} object can only belong to a single
+ * configuration. So if nodes from one configuration are directly added to
+ * another one using this method, the structure of the source configuration
+ * will be broken. In this case you should clone the nodes to be added
+ * before calling {@code addNodes()}.) If the passed in key refers to
+ * an existing and unique node, the new nodes are added to this node.
+ * Otherwise a new node will be created at the specified position in the
+ * hierarchy.
+ *
+ * @param key the key where the nodes are to be added; can be <b>null </b>,
+ * then they are added to the root node
+ * @param nodes a collection with the {@code Node} objects to be
+ * added
+ */
+ public void addNodes(String key, Collection<? extends ConfigurationNode> nodes)
+ {
+ if (nodes == null || nodes.isEmpty())
+ {
+ return;
+ }
+
+ fireEvent(EVENT_ADD_NODES, key, nodes, true);
+ ConfigurationNode parent;
+ List<ConfigurationNode> target = fetchNodeList(key);
+ if (target.size() == 1)
+ {
+ // existing unique key
+ parent = target.get(0);
+ }
+ else
+ {
+ // otherwise perform an add operation
+ parent = processNodeAddData(getExpressionEngine().prepareAdd(
+ getRootNode(), key));
+ }
+
+ if (parent.isAttribute())
+ {
+ throw new IllegalArgumentException(
+ "Cannot add nodes to an attribute node!");
+ }
+
+ for (ConfigurationNode child : nodes)
+ {
+ if (child.isAttribute())
+ {
+ parent.addAttribute(child);
+ }
+ else
+ {
+ parent.addChild(child);
+ }
+ clearReferences(child);
+ }
+ fireEvent(EVENT_ADD_NODES, key, nodes, false);
+ }
+
+ /**
+ * Checks if this configuration is empty. Empty means that there are no keys
+ * with any values, though there can be some (empty) nodes.
+ *
+ * @return a flag if this configuration is empty
+ */
+ public boolean isEmpty()
+ {
+ return !nodeDefined(getRootNode());
+ }
+
+ /**
+ * Creates a new {@code Configuration} object containing all keys
+ * that start with the specified prefix. This implementation will return a
+ * {@code HierarchicalConfiguration} object so that the structure of
+ * the keys will be saved. The nodes selected by the prefix (it is possible
+ * that multiple nodes are selected) are mapped to the root node of the
+ * returned configuration, i.e. their children and attributes will become
+ * children and attributes of the new root node. However a value of the root
+ * node is only set if exactly one of the selected nodes contain a value (if
+ * multiple nodes have a value, there is simply no way to decide how these
+ * values are merged together). Note that the returned
+ * {@code Configuration} object is not connected to its source
+ * configuration: updates on the source configuration are not reflected in
+ * the subset and vice versa.
+ *
+ * @param prefix the prefix of the keys for the subset
+ * @return a new configuration object representing the selected subset
+ */
+ @Override
+ public Configuration subset(String prefix)
+ {
+ Collection<ConfigurationNode> nodes = fetchNodeList(prefix);
+ if (nodes.isEmpty())
+ {
+ return new HierarchicalConfiguration();
+ }
+
+ final HierarchicalConfiguration parent = this;
+ HierarchicalConfiguration result = new HierarchicalConfiguration()
+ {
+ // Override interpolate to always interpolate on the parent
+ @Override
+ protected Object interpolate(Object value)
+ {
+ return parent.interpolate(value);
+ }
+ };
+ CloneVisitor visitor = new CloneVisitor();
+
+ // Initialize the new root node
+ Object value = null;
+ int valueCount = 0;
+ for (ConfigurationNode nd : nodes)
+ {
+ if (nd.getValue() != null)
+ {
+ value = nd.getValue();
+ valueCount++;
+ }
+ nd.visit(visitor);
+
+ for (ConfigurationNode c : visitor.getClone().getChildren())
+ {
+ result.getRootNode().addChild(c);
+ }
+ for (ConfigurationNode attr : visitor.getClone().getAttributes())
+ {
+ result.getRootNode().addAttribute(attr);
+ }
+ }
+
+ // Determine the value of the new root
+ if (valueCount == 1)
+ {
+ result.getRootNode().setValue(value);
+ }
+ return (result.isEmpty()) ? new HierarchicalConfiguration() : result;
+ }
+
+ /**
+ * <p>
+ * Returns a hierarchical subnode configuration object that wraps the
+ * configuration node specified by the given key. This method provides an
+ * easy means of accessing sub trees of a hierarchical configuration. In the
+ * returned configuration the sub tree can directly be accessed, it becomes
+ * the root node of this configuration. Because of this the passed in key
+ * must select exactly one configuration node; otherwise an
+ * {@code IllegalArgumentException} will be thrown.
+ * </p>
+ * <p>
+ * The difference between this method and the
+ * {@link #subset(String)} method is that
+ * {@code subset()} supports arbitrary subsets of configuration nodes
+ * while {@code configurationAt()} only returns a single sub tree.
+ * Please refer to the documentation of the
+ * {@code SubnodeConfiguration} class to obtain further information
+ * about subnode configurations and when they should be used.
+ * </p>
+ * <p>
+ * With the {@code supportUpdate} flag the behavior of the returned
+ * {@code SubnodeConfiguration} regarding updates of its parent
+ * configuration can be determined. A subnode configuration operates on the
+ * same nodes as its parent, so changes at one configuration are normally
+ * directly visible for the other configuration. There are however changes
+ * of the parent configuration, which are not recognized by the subnode
+ * configuration per default. An example for this is a reload operation (for
+ * file-based configurations): Here the complete node set of the parent
+ * configuration is replaced, but the subnode configuration still references
+ * the old nodes. If such changes should be detected by the subnode
+ * configuration, the {@code supportUpdates} flag must be set to
+ * <b>true</b>. This causes the subnode configuration to reevaluate the key
+ * used for its creation each time it is accessed. This guarantees that the
+ * subnode configuration always stays in sync with its key, even if the
+ * parent configuration's data significantly changes. If such a change
+ * makes the key invalid - because it now no longer points to exactly one
+ * node -, the subnode configuration is not reconstructed, but keeps its
+ * old data. It is then quasi detached from its parent.
+ * </p>
+ *
+ * @param key the key that selects the sub tree
+ * @param supportUpdates a flag whether the returned subnode configuration
+ * should be able to handle updates of its parent
+ * @return a hierarchical configuration that contains this sub tree
+ * @see SubnodeConfiguration
+ * @since 1.5
+ */
+ public SubnodeConfiguration configurationAt(String key,
+ boolean supportUpdates)
+ {
+ List<ConfigurationNode> nodes = fetchNodeList(key);
+ if (nodes.size() != 1)
+ {
+ throw new IllegalArgumentException(
+ "Passed in key must select exactly one node: " + key);
+ }
+ return supportUpdates ? createSubnodeConfiguration(
+ nodes.get(0), key)
+ : createSubnodeConfiguration(nodes.get(0));
+ }
+
+ /**
+ * Returns a hierarchical subnode configuration for the node specified by
+ * the given key. This is a short form for {@code configurationAt(key,
+ * <b>false</b>)}.
+ *
+ * @param key the key that selects the sub tree
+ * @return a hierarchical configuration that contains this sub tree
+ * @see SubnodeConfiguration
+ * @since 1.3
+ */
+ public SubnodeConfiguration configurationAt(String key)
+ {
+ return configurationAt(key, false);
+ }
+
+ /**
+ * Returns a list of sub configurations for all configuration nodes selected
+ * by the given key. This method will evaluate the passed in key (using the
+ * current {@code ExpressionEngine}) and then create a subnode
+ * configuration for each returned node (like
+ * {@link #configurationAt(String)}}). This is especially
+ * useful when dealing with list-like structures. As an example consider the
+ * configuration that contains data about database tables and their fields.
+ * If you need access to all fields of a certain table, you can simply do
+ *
+ * <pre>
+ * List fields = config.configurationsAt("tables.table(0).fields.field");
+ * for(Iterator it = fields.iterator(); it.hasNext();)
+ * {
+ * HierarchicalConfiguration sub = (HierarchicalConfiguration) it.next();
+ * // now the children and attributes of the field node can be
+ * // directly accessed
+ * String fieldName = sub.getString("name");
+ * String fieldType = sub.getString("type");
+ * ...
+ * </pre>
+ *
+ * @param key the key for selecting the desired nodes
+ * @return a list with hierarchical configuration objects; each
+ * configuration represents one of the nodes selected by the passed in key
+ * @since 1.3
+ */
+ public List<HierarchicalConfiguration> configurationsAt(String key)
+ {
+ List<ConfigurationNode> nodes = fetchNodeList(key);
+ List<HierarchicalConfiguration> configs = new ArrayList<HierarchicalConfiguration>(nodes.size());
+ for (ConfigurationNode node : nodes)
+ {
+ configs.add(createSubnodeConfiguration(node));
+ }
+ return configs;
+ }
+
+ /**
+ * Creates a subnode configuration for the specified node. This method is
+ * called by {@code configurationAt()} and
+ * {@code configurationsAt()}.
+ *
+ * @param node the node, for which a subnode configuration is to be created
+ * @return the configuration for the given node
+ * @since 1.3
+ */
+ protected SubnodeConfiguration createSubnodeConfiguration(ConfigurationNode node)
+ {
+ SubnodeConfiguration result = new SubnodeConfiguration(this, node);
+ registerSubnodeConfiguration(result);
+ return result;
+ }
+
+ /**
+ * Creates a new subnode configuration for the specified node and sets its
+ * construction key. A subnode configuration created this way will be aware
+ * of structural changes of its parent.
+ *
+ * @param node the node, for which a subnode configuration is to be created
+ * @param subnodeKey the key used to construct the configuration
+ * @return the configuration for the given node
+ * @since 1.5
+ */
+ protected SubnodeConfiguration createSubnodeConfiguration(
+ ConfigurationNode node, String subnodeKey)
+ {
+ SubnodeConfiguration result = createSubnodeConfiguration(node);
+ result.setSubnodeKey(subnodeKey);
+ return result;
+ }
+
+ /**
+ * This method is always called when a subnode configuration created from
+ * this configuration has been modified. This implementation transforms the
+ * received event into an event of type {@code EVENT_SUBNODE_CHANGED}
+ * and notifies the registered listeners.
+ *
+ * @param event the event describing the change
+ * @since 1.5
+ */
+ protected void subnodeConfigurationChanged(ConfigurationEvent event)
+ {
+ fireEvent(EVENT_SUBNODE_CHANGED, null, event, event.isBeforeUpdate());
+ }
+
+ /**
+ * Registers this instance at the given subnode configuration. This
+ * implementation will register a change listener, so that modifications of
+ * the subnode configuration can be tracked.
+ *
+ * @param config the subnode configuration
+ * @since 1.5
+ */
+ void registerSubnodeConfiguration(SubnodeConfiguration config)
+ {
+ config.addConfigurationListener(new ConfigurationListener()
+ {
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ subnodeConfigurationChanged(event);
+ }
+ });
+ }
+
+ /**
+ * Checks if the specified key is contained in this configuration. Note that
+ * for this configuration the term "contained" means that the key
+ * has an associated value. If there is a node for this key that has no
+ * value but children (either defined or undefined), this method will still
+ * return <b>false </b>.
+ *
+ * @param key the key to be chekced
+ * @return a flag if this key is contained in this configuration
+ */
+ public boolean containsKey(String key)
+ {
+ return getProperty(key) != null;
+ }
+
+ /**
+ * Sets the value of the specified property.
+ *
+ * @param key the key of the property to set
+ * @param value the new value of this property
+ */
+ @Override
+ public void setProperty(String key, Object value)
+ {
+ fireEvent(EVENT_SET_PROPERTY, key, value, true);
+
+ // Update the existing nodes for this property
+ Iterator<ConfigurationNode> itNodes = fetchNodeList(key).iterator();
+ Iterator<?> itValues;
+ if (!isDelimiterParsingDisabled() || !(value instanceof String))
+ {
+ itValues = PropertyConverter.toIterator(value, getListDelimiter());
+ }
+ else
+ {
+ itValues = Collections.singleton(value).iterator();
+ }
+
+ while (itNodes.hasNext() && itValues.hasNext())
+ {
+ itNodes.next().setValue(itValues.next());
+ }
+
+ // Add additional nodes if necessary
+ while (itValues.hasNext())
+ {
+ addPropertyDirect(key, itValues.next());
+ }
+
+ // Remove remaining nodes
+ while (itNodes.hasNext())
+ {
+ clearNode(itNodes.next());
+ }
+
+ fireEvent(EVENT_SET_PROPERTY, key, value, false);
+ }
+
+ /**
+ * Clears this configuration. This is a more efficient implementation than
+ * the one inherited from the base class. It directly removes all data from
+ * the root node.
+ */
+ @Override
+ public void clear()
+ {
+ fireEvent(EVENT_CLEAR, null, null, true);
+ getRootNode().removeAttributes();
+ getRootNode().removeChildren();
+ getRootNode().setValue(null);
+ fireEvent(EVENT_CLEAR, null, null, false);
+ }
+
+ /**
+ * Removes all values of the property with the given name and of keys that
+ * start with this name. So if there is a property with the key
+ * "foo" and a property with the key "foo.bar", a call
+ * of {@code clearTree("foo")} would remove both properties.
+ *
+ * @param key the key of the property to be removed
+ */
+ public void clearTree(String key)
+ {
+ fireEvent(EVENT_CLEAR_TREE, key, null, true);
+ List<ConfigurationNode> nodes = fetchNodeList(key);
+
+ for (ConfigurationNode node : nodes)
+ {
+ removeNode(node);
+ }
+ fireEvent(EVENT_CLEAR_TREE, key, nodes, false);
+ }
+
+ /**
+ * Removes the property with the given key. Properties with names that start
+ * with the given key (i.e. properties below the specified key in the
+ * hierarchy) won't be affected.
+ *
+ * @param key the key of the property to be removed
+ */
+ @Override
+ public void clearProperty(String key)
+ {
+ fireEvent(EVENT_CLEAR_PROPERTY, key, null, true);
+ List<ConfigurationNode> nodes = fetchNodeList(key);
+
+ for (ConfigurationNode node : nodes)
+ {
+ clearNode(node);
+ }
+
+ fireEvent(EVENT_CLEAR_PROPERTY, key, null, false);
+ }
+
+ /**
+ * Returns an iterator with all keys defined in this configuration.
+ * Note that the keys returned by this method will not contain any
+ * indices. This means that some structure will be lost.</p>
+ *
+ * @return an iterator with the defined keys in this configuration
+ */
+ public Iterator<String> getKeys()
+ {
+ DefinedKeysVisitor visitor = new DefinedKeysVisitor();
+ getRootNode().visit(visitor);
+
+ return visitor.getKeyList().iterator();
+ }
+
+ /**
+ * Returns an iterator with all keys defined in this configuration that
+ * start with the given prefix. The returned keys will not contain any
+ * indices. This implementation tries to locate a node whose key is the same
+ * as the passed in prefix. Then the subtree of this node is traversed, and
+ * the keys of all nodes encountered (including attributes) are added to the
+ * result set.
+ *
+ * @param prefix the prefix of the keys to start with
+ * @return an iterator with the found keys
+ */
+ @Override
+ public Iterator<String> getKeys(String prefix)
+ {
+ DefinedKeysVisitor visitor = new DefinedKeysVisitor(prefix);
+ if (containsKey(prefix))
+ {
+ // explicitly add the prefix
+ visitor.getKeyList().add(prefix);
+ }
+
+ List<ConfigurationNode> nodes = fetchNodeList(prefix);
+
+ for (ConfigurationNode node : nodes)
+ {
+ for (ConfigurationNode c : node.getChildren())
+ {
+ c.visit(visitor);
+ }
+ for (ConfigurationNode attr : node.getAttributes())
+ {
+ attr.visit(visitor);
+ }
+ }
+
+ return visitor.getKeyList().iterator();
+ }
+
+ /**
+ * Returns the maximum defined index for the given key. This is useful if
+ * there are multiple values for this key. They can then be addressed
+ * separately by specifying indices from 0 to the return value of this
+ * method.
+ *
+ * @param key the key to be checked
+ * @return the maximum defined index for this key
+ */
+ public int getMaxIndex(String key)
+ {
+ return fetchNodeList(key).size() - 1;
+ }
+
+ /**
+ * Creates a copy of this object. This new configuration object will contain
+ * copies of all nodes in the same structure. Registered event listeners
+ * won't be cloned; so they are not registered at the returned copy.
+ *
+ * @return the copy
+ * @since 1.2
+ */
+ @Override
+ public Object clone()
+ {
+ try
+ {
+ HierarchicalConfiguration copy = (HierarchicalConfiguration) super
+ .clone();
+
+ // clone the nodes, too
+ CloneVisitor v = new CloneVisitor();
+ getRootNode().visit(v);
+ copy.setRootNode(v.getClone());
+
+ return copy;
+ }
+ catch (CloneNotSupportedException cex)
+ {
+ // should not happen
+ throw new ConfigurationRuntimeException(cex);
+ }
+ }
+
+ /**
+ * Returns a configuration with the same content as this configuration, but
+ * with all variables replaced by their actual values. This implementation
+ * is specific for hierarchical configurations. It clones the current
+ * configuration and runs a specialized visitor on the clone, which performs
+ * interpolation on the single configuration nodes.
+ *
+ * @return a configuration with all variables interpolated
+ * @since 1.5
+ */
+ @Override
+ public Configuration interpolatedConfiguration()
+ {
+ HierarchicalConfiguration c = (HierarchicalConfiguration) clone();
+ c.getRootNode().visit(new ConfigurationNodeVisitorAdapter()
+ {
+ @Override
+ public void visitAfterChildren(ConfigurationNode node)
+ {
+ node.setValue(interpolate(node.getValue()));
+ }
+ });
+ return c;
+ }
+
+ /**
+ * Helper method for fetching a list of all nodes that are addressed by the
+ * specified key.
+ *
+ * @param key the key
+ * @return a list with all affected nodes (never <b>null </b>)
+ */
+ protected List<ConfigurationNode> fetchNodeList(String key)
+ {
+ return getExpressionEngine().query(getRootNode(), key);
+ }
+
+ /**
+ * Recursive helper method for fetching a property. This method processes
+ * all facets of a configuration key, traverses the tree of properties and
+ * fetches the the nodes of all matching properties.
+ *
+ * @param keyPart the configuration key iterator
+ * @param node the actual node
+ * @param nodes here the found nodes are stored
+ * @deprecated Property keys are now evaluated by the expression engine
+ * associated with the configuration; this method will no longer be called.
+ * If you want to modify the way properties are looked up, consider
+ * implementing you own {@code ExpressionEngine} implementation.
+ */
+ @Deprecated
+ protected void findPropertyNodes(ConfigurationKey.KeyIterator keyPart,
+ Node node, Collection<ConfigurationNode> nodes)
+ {
+ }
+
+ /**
+ * Checks if the specified node is defined.
+ *
+ * @param node the node to be checked
+ * @return a flag if this node is defined
+ * @deprecated Use the method {@link #nodeDefined(ConfigurationNode)}
+ * instead.
+ */
+ @Deprecated
+ protected boolean nodeDefined(Node node)
+ {
+ return nodeDefined((ConfigurationNode) node);
+ }
+
+ /**
+ * Checks if the specified node is defined.
+ *
+ * @param node the node to be checked
+ * @return a flag if this node is defined
+ */
+ protected boolean nodeDefined(ConfigurationNode node)
+ {
+ DefinedVisitor visitor = new DefinedVisitor();
+ node.visit(visitor);
+ return visitor.isDefined();
+ }
+
+ /**
+ * Removes the specified node from this configuration. This method ensures
+ * that parent nodes that become undefined by this operation are also
+ * removed.
+ *
+ * @param node the node to be removed
+ * @deprecated Use the method {@link #removeNode(ConfigurationNode)}
+ * instead.
+ */
+ @Deprecated
+ protected void removeNode(Node node)
+ {
+ removeNode((ConfigurationNode) node);
+ }
+
+ /**
+ * Removes the specified node from this configuration. This method ensures
+ * that parent nodes that become undefined by this operation are also
+ * removed.
+ *
+ * @param node the node to be removed
+ */
+ protected void removeNode(ConfigurationNode node)
+ {
+ ConfigurationNode parent = node.getParentNode();
+ if (parent != null)
+ {
+ parent.removeChild(node);
+ if (!nodeDefined(parent))
+ {
+ removeNode(parent);
+ }
+ }
+ }
+
+ /**
+ * Clears the value of the specified node. If the node becomes undefined by
+ * this operation, it is removed from the hierarchy.
+ *
+ * @param node the node to be cleared
+ * @deprecated Use the method {@link #clearNode(ConfigurationNode)}
+ * instead
+ */
+ @Deprecated
+ protected void clearNode(Node node)
+ {
+ clearNode((ConfigurationNode) node);
+ }
+
+ /**
+ * Clears the value of the specified node. If the node becomes undefined by
+ * this operation, it is removed from the hierarchy.
+ *
+ * @param node the node to be cleared
+ */
+ protected void clearNode(ConfigurationNode node)
+ {
+ node.setValue(null);
+ if (!nodeDefined(node))
+ {
+ removeNode(node);
+ }
+ }
+
+ /**
+ * Returns a reference to the parent node of an add operation. Nodes for new
+ * properties can be added as children of this node. If the path for the
+ * specified key does not exist so far, it is created now.
+ *
+ * @param keyIt the iterator for the key of the new property
+ * @param startNode the node to start the search with
+ * @return the parent node for the add operation
+ * @deprecated Adding new properties is now to a major part delegated to the
+ * {@code ExpressionEngine} associated with this configuration instance.
+ * This method will no longer be called. Developers who want to modify the
+ * process of adding new properties should consider implementing their own
+ * expression engine.
+ */
+ @Deprecated
+ protected Node fetchAddNode(ConfigurationKey.KeyIterator keyIt, Node startNode)
+ {
+ return null;
+ }
+
+ /**
+ * Finds the last existing node for an add operation. This method traverses
+ * the configuration tree along the specified key. The last existing node on
+ * this path is returned.
+ *
+ * @param keyIt the key iterator
+ * @param node the actual node
+ * @return the last existing node on the given path
+ * @deprecated Adding new properties is now to a major part delegated to the
+ * {@code ExpressionEngine} associated with this configuration instance.
+ * This method will no longer be called. Developers who want to modify the
+ * process of adding new properties should consider implementing their own
+ * expression engine.
+ */
+ @Deprecated
+ protected Node findLastPathNode(ConfigurationKey.KeyIterator keyIt, Node node)
+ {
+ return null;
+ }
+
+ /**
+ * Creates the missing nodes for adding a new property. This method ensures
+ * that there are corresponding nodes for all components of the specified
+ * configuration key.
+ *
+ * @param keyIt the key iterator
+ * @param root the base node of the path to be created
+ * @return the last node of the path
+ * @deprecated Adding new properties is now to a major part delegated to the
+ * {@code ExpressionEngine} associated with this configuration instance.
+ * This method will no longer be called. Developers who want to modify the
+ * process of adding new properties should consider implementing their own
+ * expression engine.
+ */
+ @Deprecated
+ protected Node createAddPath(ConfigurationKey.KeyIterator keyIt, Node root)
+ {
+ return null;
+ }
+
+ /**
+ * Creates a new {@code Node} object with the specified name. This
+ * method can be overloaded in derived classes if a specific node type is
+ * needed. This base implementation always returns a new object of the
+ * {@code Node} class.
+ *
+ * @param name the name of the new node
+ * @return the new node
+ */
+ protected Node createNode(String name)
+ {
+ return new Node(name);
+ }
+
+ /**
+ * Helper method for processing a node add data object obtained from the
+ * expression engine. This method will create all new nodes.
+ *
+ * @param data the data object
+ * @return the new node
+ * @since 1.3
+ */
+ private ConfigurationNode processNodeAddData(NodeAddData data)
+ {
+ ConfigurationNode node = data.getParent();
+
+ // Create missing nodes on the path
+ for (String name : data.getPathNodes())
+ {
+ ConfigurationNode child = createNode(name);
+ node.addChild(child);
+ node = child;
+ }
+
+ // Add new target node
+ ConfigurationNode child = createNode(data.getNewNodeName());
+ if (data.isAttribute())
+ {
+ node.addAttribute(child);
+ }
+ else
+ {
+ node.addChild(child);
+ }
+ return child;
+ }
+
+ /**
+ * Clears all reference fields in a node structure. A configuration node can
+ * store a so-called "reference". The meaning of this data is
+ * determined by a concrete sub class. Typically such references are
+ * specific for a configuration instance. If this instance is cloned or
+ * copied, they must be cleared. This can be done using this method.
+ *
+ * @param node the root node of the node hierarchy, in which the references
+ * are to be cleared
+ * @since 1.4
+ */
+ protected static void clearReferences(ConfigurationNode node)
+ {
+ node.visit(new ConfigurationNodeVisitorAdapter()
+ {
+ @Override
+ public void visitBeforeChildren(ConfigurationNode node)
+ {
+ node.setReference(null);
+ }
+ });
+ }
+
+ /**
+ * Transforms the specified object into a Node. This method treats view
+ * nodes in a special way. This is necessary because ViewNode does not
+ * extend HierarchicalConfiguration.Node; thus the API for the node visitor
+ * is slightly different. Therefore a view node is transformed into a
+ * special compatibility Node object.
+ *
+ * @param obj the original node object
+ * @return the node to be used
+ */
+ private static Node getNodeFor(Object obj)
+ {
+ Node nd;
+ if (obj instanceof ViewNode)
+ {
+ final ViewNode viewNode = (ViewNode) obj;
+ nd = new Node(viewNode)
+ {
+ @Override
+ public void setReference(Object reference)
+ {
+ super.setReference(reference);
+ // also set the reference at the original node
+ viewNode.setReference(reference);
+ }
+ };
+ }
+ else
+ {
+ nd = (Node) obj;
+ }
+ return nd;
+ }
+
+ /**
+ * A data class for storing (hierarchical) property information. A property
+ * can have a value and an arbitrary number of child properties. From
+ * version 1.3 on this class is only a thin wrapper over the
+ * {@link org.apache.commons.configuration.tree.DefaultConfigurationNode DefaultconfigurationNode}
+ * class that exists mainly for the purpose of backwards compatibility.
+ */
+ public static class Node extends DefaultConfigurationNode implements Serializable
+ {
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = -6357500633536941775L;
+
+ /**
+ * Creates a new instance of {@code Node}.
+ */
+ public Node()
+ {
+ super();
+ }
+
+ /**
+ * Creates a new instance of {@code Node} and sets the name.
+ *
+ * @param name the node's name
+ */
+ public Node(String name)
+ {
+ super(name);
+ }
+
+ /**
+ * Creates a new instance of {@code Node} and sets the name and the value.
+ *
+ * @param name the node's name
+ * @param value the value
+ */
+ public Node(String name, Object value)
+ {
+ super(name, value);
+ }
+
+ /**
+ * Creates a new instance of {@code Node} based on the given
+ * source node. All properties of the source node, including its
+ * children and attributes, will be copied.
+ *
+ * @param src the node to be copied
+ */
+ public Node(ConfigurationNode src)
+ {
+ this(src.getName(), src.getValue());
+ setReference(src.getReference());
+ for (ConfigurationNode nd : src.getChildren())
+ {
+ // Don't change the parent node
+ ConfigurationNode parent = nd.getParentNode();
+ addChild(nd);
+ nd.setParentNode(parent);
+ }
+
+ for (ConfigurationNode nd : src.getAttributes())
+ {
+ // Don't change the parent node
+ ConfigurationNode parent = nd.getParentNode();
+ addAttribute(nd);
+ nd.setParentNode(parent);
+ }
+ }
+
+ /**
+ * Returns the parent of this node.
+ *
+ * @return this node's parent (can be <b>null</b>)
+ */
+ public Node getParent()
+ {
+ return (Node) getParentNode();
+ }
+
+ /**
+ * Sets the parent of this node.
+ *
+ * @param node the parent node
+ */
+ public void setParent(Node node)
+ {
+ setParentNode(node);
+ }
+
+ /**
+ * Adds the given node to the children of this node.
+ *
+ * @param node the child to be added
+ */
+ public void addChild(Node node)
+ {
+ addChild((ConfigurationNode) node);
+ }
+
+ /**
+ * Returns a flag whether this node has child elements.
+ *
+ * @return <b>true</b> if there is a child node, <b>false</b> otherwise
+ */
+ public boolean hasChildren()
+ {
+ return getChildrenCount() > 0 || getAttributeCount() > 0;
+ }
+
+ /**
+ * Removes the specified child from this node.
+ *
+ * @param child the child node to be removed
+ * @return a flag if the child could be found
+ */
+ public boolean remove(Node child)
+ {
+ return child.isAttribute() ? removeAttribute(child) : removeChild(child);
+ }
+
+ /**
+ * Removes all children with the given name.
+ *
+ * @param name the name of the children to be removed
+ * @return a flag if children with this name existed
+ */
+ public boolean remove(String name)
+ {
+ boolean childrenRemoved = removeChild(name);
+ boolean attrsRemoved = removeAttribute(name);
+ return childrenRemoved || attrsRemoved;
+ }
+
+ /**
+ * A generic method for traversing this node and all of its children.
+ * This method sends the passed in visitor to this node and all of its
+ * children.
+ *
+ * @param visitor the visitor
+ * @param key here a configuration key with the name of the root node of
+ * the iteration can be passed; if this key is not <b>null </b>, the
+ * full paths to the visited nodes are builded and passed to the
+ * visitor's {@code visit()} methods
+ */
+ public void visit(NodeVisitor visitor, ConfigurationKey key)
+ {
+ int length = 0;
+ if (key != null)
+ {
+ length = key.length();
+ if (getName() != null)
+ {
+ key
+ .append(StringUtils
+ .replace(
+ isAttribute() ? ConfigurationKey
+ .constructAttributeKey(getName())
+ : getName(),
+ String
+ .valueOf(ConfigurationKey.PROPERTY_DELIMITER),
+ ConfigurationKey.ESCAPED_DELIMITER));
+ }
+ }
+
+ visitor.visitBeforeChildren(this, key);
+
+ for (Iterator<ConfigurationNode> it = getChildren().iterator(); it.hasNext()
+ && !visitor.terminate();)
+ {
+ Object obj = it.next();
+ getNodeFor(obj).visit(visitor, key);
+ }
+ for (Iterator<ConfigurationNode> it = getAttributes().iterator(); it.hasNext()
+ && !visitor.terminate();)
+ {
+ Object obj = it.next();
+ getNodeFor(obj).visit(visitor, key);
+ }
+
+ visitor.visitAfterChildren(this, key);
+ if (key != null)
+ {
+ key.setLength(length);
+ }
+ }
+ }
+
+ /**
+ * <p>Definition of a visitor class for traversing a node and all of its
+ * children.</p><p>This class defines the interface of a visitor for
+ * {@code Node} objects and provides a default implementation. The
+ * method {@code visit()} of {@code Node} implements a generic
+ * iteration algorithm based on the <em>Visitor</em> pattern. By providing
+ * different implementations of visitors it is possible to collect different
+ * data during the iteration process.</p>
+ *
+ */
+ public static class NodeVisitor
+ {
+ /**
+ * Visits the specified node. This method is called during iteration for
+ * each node before its children have been visited.
+ *
+ * @param node the actual node
+ * @param key the key of this node (may be <b>null </b>)
+ */
+ public void visitBeforeChildren(Node node, ConfigurationKey key)
+ {
+ }
+
+ /**
+ * Visits the specified node after its children have been processed.
+ * This gives a visitor the opportunity of collecting additional data
+ * after the child nodes have been visited.
+ *
+ * @param node the node to be visited
+ * @param key the key of this node (may be <b>null </b>)
+ */
+ public void visitAfterChildren(Node node, ConfigurationKey key)
+ {
+ }
+
+ /**
+ * Returns a flag that indicates if iteration should be stopped. This
+ * method is called after each visited node. It can be useful for
+ * visitors that search a specific node. If this node is found, the
+ * whole process can be stopped. This base implementation always returns
+ * <b>false </b>.
+ *
+ * @return a flag if iteration should be stopped
+ */
+ public boolean terminate()
+ {
+ return false;
+ }
+ }
+
+ /**
+ * A specialized visitor that checks if a node is defined.
+ * "Defined" in this terms means that the node or at least one of
+ * its sub nodes is associated with a value.
+ *
+ */
+ static class DefinedVisitor extends ConfigurationNodeVisitorAdapter
+ {
+ /** Stores the defined flag. */
+ private boolean defined;
+
+ /**
+ * Checks if iteration should be stopped. This can be done if the first
+ * defined node is found.
+ *
+ * @return a flag if iteration should be stopped
+ */
+ @Override
+ public boolean terminate()
+ {
+ return isDefined();
+ }
+
+ /**
+ * Visits the node. Checks if a value is defined.
+ *
+ * @param node the actual node
+ */
+ @Override
+ public void visitBeforeChildren(ConfigurationNode node)
+ {
+ defined = node.getValue() != null;
+ }
+
+ /**
+ * Returns the defined flag.
+ *
+ * @return the defined flag
+ */
+ public boolean isDefined()
+ {
+ return defined;
+ }
+ }
+
+ /**
+ * A specialized visitor that fills a list with keys that are defined in a
+ * node hierarchy.
+ */
+ class DefinedKeysVisitor extends ConfigurationNodeVisitorAdapter
+ {
+ /** Stores the list to be filled. */
+ private Set<String> keyList;
+
+ /** A stack with the keys of the already processed nodes. */
+ private Stack<String> parentKeys;
+
+ /**
+ * Default constructor.
+ */
+ public DefinedKeysVisitor()
+ {
+ keyList = new LinkedHashSet<String>();
+ parentKeys = new Stack<String>();
+ }
+
+ /**
+ * Creates a new {@code DefinedKeysVisitor} instance and sets the
+ * prefix for the keys to fetch.
+ *
+ * @param prefix the prefix
+ */
+ public DefinedKeysVisitor(String prefix)
+ {
+ this();
+ parentKeys.push(prefix);
+ }
+
+ /**
+ * Returns the list with all defined keys.
+ *
+ * @return the list with the defined keys
+ */
+ public Set<String> getKeyList()
+ {
+ return keyList;
+ }
+
+ /**
+ * Visits the node after its children has been processed. Removes this
+ * node's key from the stack.
+ *
+ * @param node the node
+ */
+ @Override
+ public void visitAfterChildren(ConfigurationNode node)
+ {
+ parentKeys.pop();
+ }
+
+ /**
+ * Visits the specified node. If this node has a value, its key is added
+ * to the internal list.
+ *
+ * @param node the node to be visited
+ */
+ @Override
+ public void visitBeforeChildren(ConfigurationNode node)
+ {
+ String parentKey = parentKeys.isEmpty() ? null
+ : (String) parentKeys.peek();
+ String key = getExpressionEngine().nodeKey(node, parentKey);
+ parentKeys.push(key);
+ if (node.getValue() != null)
+ {
+ keyList.add(key);
+ }
+ }
+ }
+
+ /**
+ * A specialized visitor that is able to create a deep copy of a node
+ * hierarchy.
+ */
+ static class CloneVisitor extends ConfigurationNodeVisitorAdapter
+ {
+ /** A stack with the actual object to be copied. */
+ private Stack<ConfigurationNode> copyStack;
+
+ /** Stores the result of the clone process. */
+ private ConfigurationNode result;
+
+ /**
+ * Creates a new instance of {@code CloneVisitor}.
+ */
+ public CloneVisitor()
+ {
+ copyStack = new Stack<ConfigurationNode>();
+ }
+
+ /**
+ * Visits the specified node after its children have been processed.
+ *
+ * @param node the node
+ */
+ @Override
+ public void visitAfterChildren(ConfigurationNode node)
+ {
+ ConfigurationNode copy = copyStack.pop();
+ if (copyStack.isEmpty())
+ {
+ result = copy;
+ }
+ }
+
+ /**
+ * Visits and copies the specified node.
+ *
+ * @param node the node
+ */
+ @Override
+ public void visitBeforeChildren(ConfigurationNode node)
+ {
+ ConfigurationNode copy = (ConfigurationNode) node.clone();
+ copy.setParentNode(null);
+
+ if (!copyStack.isEmpty())
+ {
+ if (node.isAttribute())
+ {
+ copyStack.peek().addAttribute(copy);
+ }
+ else
+ {
+ copyStack.peek().addChild(copy);
+ }
+ }
+
+ copyStack.push(copy);
+ }
+
+ /**
+ * Returns the result of the clone process. This is the root node of the
+ * cloned node hierarchy.
+ *
+ * @return the cloned root node
+ */
+ public ConfigurationNode getClone()
+ {
+ return result;
+ }
+ }
+
+ /**
+ * A specialized visitor base class that can be used for storing the tree of
+ * configuration nodes. The basic idea is that each node can be associated
+ * with a reference object. This reference object has a concrete meaning in
+ * a derived class, e.g. an entry in a JNDI context or an XML element. When
+ * the configuration tree is set up, the {@code load()} method is
+ * responsible for setting the reference objects. When the configuration
+ * tree is later modified, new nodes do not have a defined reference object.
+ * This visitor class processes all nodes and finds the ones without a
+ * defined reference object. For those nodes the {@code insert()}
+ * method is called, which must be defined in concrete sub classes. This
+ * method can perform all steps to integrate the new node into the original
+ * structure.
+ *
+ */
+ protected abstract static class BuilderVisitor extends NodeVisitor
+ {
+ /**
+ * Visits the specified node before its children have been traversed.
+ *
+ * @param node the node to visit
+ * @param key the current key
+ */
+ @Override
+ public void visitBeforeChildren(Node node, ConfigurationKey key)
+ {
+ Collection<ConfigurationNode> subNodes = new LinkedList<ConfigurationNode>(node.getChildren());
+ subNodes.addAll(node.getAttributes());
+ Iterator<ConfigurationNode> children = subNodes.iterator();
+ Node sibling1 = null;
+ Node nd = null;
+
+ while (children.hasNext())
+ {
+ // find the next new node
+ do
+ {
+ sibling1 = nd;
+ Object obj = children.next();
+ nd = getNodeFor(obj);
+ } while (nd.getReference() != null && children.hasNext());
+
+ if (nd.getReference() == null)
+ {
+ // find all following new nodes
+ List<Node> newNodes = new LinkedList<Node>();
+ newNodes.add(nd);
+ while (children.hasNext())
+ {
+ Object obj = children.next();
+ nd = getNodeFor(obj);
+ if (nd.getReference() == null)
+ {
+ newNodes.add(nd);
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ // Insert all new nodes
+ Node sibling2 = (nd.getReference() == null) ? null : nd;
+ for (Node insertNode : newNodes)
+ {
+ if (insertNode.getReference() == null)
+ {
+ Object ref = insert(insertNode, node, sibling1, sibling2);
+ if (ref != null)
+ {
+ insertNode.setReference(ref);
+ }
+ sibling1 = insertNode;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Inserts a new node into the structure constructed by this builder.
+ * This method is called for each node that has been added to the
+ * configuration tree after the configuration has been loaded from its
+ * source. These new nodes have to be inserted into the original
+ * structure. The passed in nodes define the position of the node to be
+ * inserted: its parent and the siblings between to insert. The return
+ * value is interpreted as the new reference of the affected
+ * {@code Node} object; if it is not <b>null </b>, it is passed
+ * to the node's {@code setReference()} method.
+ *
+ * @param newNode the node to be inserted
+ * @param parent the parent node
+ * @param sibling1 the sibling after which the node is to be inserted;
+ * can be <b>null </b> if the new node is going to be the first child
+ * node
+ * @param sibling2 the sibling before which the node is to be inserted;
+ * can be <b>null </b> if the new node is going to be the last child
+ * node
+ * @return the reference object for the node to be inserted
+ */
+ protected abstract Object insert(Node newNode, Node parent, Node sibling1, Node sibling2);
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/HierarchicalConfigurationConverter.java b/src/main/java/org/apache/commons/configuration/HierarchicalConfigurationConverter.java
new file mode 100644
index 0000000..de04775
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/HierarchicalConfigurationConverter.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * <p>A base class for converters that transform a normal configuration
+ * object into a hierarchical configuration.</p>
+ * <p>This class provides a default mechanism for iterating over the keys in a
+ * configuration and to throw corresponding element start and end events. By
+ * handling these events a hierarchy can be constructed that is equivalent to
+ * the keys in the original configuration.</p>
+ * <p>Concrete sub classes will implement event handlers that generate SAX
+ * events for XML processing or construct a
+ * {@code HierarchicalConfiguration} root node. All in all with this class
+ * it is possible to treat a default configuration as if it was a hierarchical
+ * configuration, which can be sometimes useful.</p>
+ * @see HierarchicalConfiguration
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons Configuration team</a>
+ * @version $Id: HierarchicalConfigurationConverter.java 1234985 2012-01-23 21:09:09Z oheger $
+ */
+abstract class HierarchicalConfigurationConverter
+{
+ /**
+ * Processes the specified configuration object. This method implements
+ * the iteration over the configuration's keys. All defined keys are
+ * translated into a set of element start and end events represented by
+ * calls to the {@code elementStart()} and
+ * {@code elementEnd()} methods.
+ *
+ * @param config the configuration to be processed
+ */
+ public void process(Configuration config)
+ {
+ if (config != null)
+ {
+ ConfigurationKey keyEmpty = new ConfigurationKey();
+ ConfigurationKey keyLast = keyEmpty;
+ Set<String> keySet = new HashSet<String>();
+
+ for (Iterator<String> it = config.getKeys(); it.hasNext();)
+ {
+ String key = it.next();
+ if (keySet.contains(key))
+ {
+ // this key has already been processed by openElements
+ continue;
+ }
+ ConfigurationKey keyAct = new ConfigurationKey(key);
+ closeElements(keyLast, keyAct);
+ String elem = openElements(keyLast, keyAct, config, keySet);
+ fireValue(elem, config.getProperty(key));
+ keyLast = keyAct;
+ }
+
+ // close all open
+ closeElements(keyLast, keyEmpty);
+ }
+ }
+
+ /**
+ * An event handler method that is called when an element starts.
+ * Concrete sub classes must implement it to perform a proper event
+ * handling.
+ *
+ * @param name the name of the new element
+ * @param value the element's value; can be <b>null</b> if the element
+ * does not have any value
+ */
+ protected abstract void elementStart(String name, Object value);
+
+ /**
+ * An event handler method that is called when an element ends. For each
+ * call of {@code elementStart()} there will be a corresponding call
+ * of this method. Concrete sub classes must implement it to perform a
+ * proper event handling.
+ *
+ * @param name the name of the ending element
+ */
+ protected abstract void elementEnd(String name);
+
+ /**
+ * Fires all necessary element end events for the specified keys. This
+ * method is called for each key obtained from the configuration to be
+ * converted. It calculates the common part of the actual and the last
+ * processed key and thus determines how many elements must be
+ * closed.
+ *
+ * @param keyLast the last processed key
+ * @param keyAct the actual key
+ */
+ protected void closeElements(ConfigurationKey keyLast, ConfigurationKey keyAct)
+ {
+ ConfigurationKey keyDiff = keyAct.differenceKey(keyLast);
+ Iterator<String> it = reverseIterator(keyDiff);
+ if (it.hasNext())
+ {
+ // Skip first because it has already been closed by fireValue()
+ it.next();
+ }
+
+ while (it.hasNext())
+ {
+ elementEnd(it.next());
+ }
+ }
+
+ /**
+ * Helper method for determining a reverse iterator for the specified key.
+ * This implementation returns an iterator that returns the parts of the
+ * given key in reverse order, ignoring indices.
+ *
+ * @param key the key
+ * @return a reverse iterator for the parts of this key
+ */
+ protected Iterator<String> reverseIterator(ConfigurationKey key)
+ {
+ List<String> list = new ArrayList<String>();
+ for (ConfigurationKey.KeyIterator it = key.iterator(); it.hasNext();)
+ {
+ list.add(it.nextKey());
+ }
+
+ Collections.reverse(list);
+ return list.iterator();
+ }
+
+ /**
+ * Fires all necessary element start events for the specified key. This
+ * method is called for each key obtained from the configuration to be
+ * converted. It ensures that all elements "between" the last key and the
+ * actual key are opened and their values are set.
+ *
+ * @param keyLast the last processed key
+ * @param keyAct the actual key
+ * @param config the configuration to process
+ * @param keySet the set with the processed keys
+ * @return the name of the last element on the path
+ */
+ protected String openElements(ConfigurationKey keyLast, ConfigurationKey keyAct,
+ Configuration config, Set<String> keySet)
+ {
+ ConfigurationKey.KeyIterator it = keyLast.differenceKey(keyAct).iterator();
+ ConfigurationKey k = keyLast.commonKey(keyAct);
+ for (it.nextKey(); it.hasNext(); it.nextKey())
+ {
+ k.append(it.currentKey(true));
+ elementStart(it.currentKey(true), config.getProperty(k.toString()));
+ keySet.add(k.toString());
+ }
+ return it.currentKey(true);
+ }
+
+ /**
+ * Fires all necessary element start events with the actual element values.
+ * This method is called for each key obtained from the configuration to be
+ * processed with the last part of the key as argument. The value can be
+ * either a single value or a collection.
+ *
+ * @param name the name of the actual element
+ * @param value the element's value
+ */
+ protected void fireValue(String name, Object value)
+ {
+ if (value instanceof Collection)
+ {
+ Collection<?> valueCol = (Collection<?>) value;
+ for (Object v : valueCol)
+ {
+ fireValue(name, v);
+ }
+ }
+ else
+ {
+ elementStart(name, value);
+ elementEnd(name);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/HierarchicalConfigurationXMLReader.java b/src/main/java/org/apache/commons/configuration/HierarchicalConfigurationXMLReader.java
new file mode 100644
index 0000000..adb2e3e
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/HierarchicalConfigurationXMLReader.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import org.apache.commons.configuration.HierarchicalConfiguration.Node;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.xml.sax.Attributes;
+import org.xml.sax.helpers.AttributesImpl;
+
+/**
+ * <p>A specialized SAX2 XML parser that "parses" hierarchical
+ * configuration objects.</p>
+ * <p>This class mimics to be a SAX conform XML parser. Instead of parsing
+ * XML documents it processes a {@code Configuration} object and
+ * generates SAX events for the single properties defined there. This enables
+ * the whole world of XML processing for configuration objects.</p>
+ * <p>The {@code HierarchicalConfiguration} object to be parsed can be
+ * specified using a constructor or the {@code setConfiguration()} method.
+ * This object will be processed by the {@code parse()} methods. Note
+ * that these methods ignore their argument.</p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons Configuration team</a>
+ * @version $Id: HierarchicalConfigurationXMLReader.java 1209998 2011-12-03 20:31:16Z oheger $
+ */
+public class HierarchicalConfigurationXMLReader extends ConfigurationXMLReader
+{
+ /** Stores the configuration object to be parsed.*/
+ private HierarchicalConfiguration configuration;
+
+ /**
+ * Creates a new instance of {@code HierarchicalConfigurationXMLReader}.
+ */
+ public HierarchicalConfigurationXMLReader()
+ {
+ super();
+ }
+
+ /**
+ * Creates a new instance of {@code HierarchicalConfigurationXMLReader} and
+ * sets the configuration to be parsed.
+ *
+ * @param config the configuration object
+ */
+ public HierarchicalConfigurationXMLReader(HierarchicalConfiguration config)
+ {
+ this();
+ setConfiguration(config);
+ }
+
+ /**
+ * Returns the configuration object to be parsed.
+ *
+ * @return the configuration object to be parsed
+ */
+ public HierarchicalConfiguration getConfiguration()
+ {
+ return configuration;
+ }
+
+ /**
+ * Sets the configuration object to be parsed.
+ *
+ * @param config the configuration object to be parsed
+ */
+ public void setConfiguration(HierarchicalConfiguration config)
+ {
+ configuration = config;
+ }
+
+ /**
+ * Returns the configuration object to be processed.
+ *
+ * @return the actual configuration object
+ */
+ @Override
+ public Configuration getParsedConfiguration()
+ {
+ return getConfiguration();
+ }
+
+ /**
+ * Processes the actual configuration object to generate SAX parsing events.
+ */
+ @Override
+ protected void processKeys()
+ {
+ getConfiguration().getRoot().visit(new SAXVisitor(), null);
+ }
+
+ /**
+ * A specialized visitor class for generating SAX events for a
+ * hierarchical node structure.
+ *
+ */
+ class SAXVisitor extends HierarchicalConfiguration.NodeVisitor
+ {
+ /** Constant for the attribute type.*/
+ private static final String ATTR_TYPE = "CDATA";
+
+ /**
+ * Visits the specified node after its children have been processed.
+ *
+ * @param node the actual node
+ * @param key the key of this node
+ */
+ @Override
+ public void visitAfterChildren(Node node, ConfigurationKey key)
+ {
+ if (!isAttributeNode(node))
+ {
+ fireElementEnd(nodeName(node));
+ }
+ }
+
+ /**
+ * Visits the specified node.
+ *
+ * @param node the actual node
+ * @param key the key of this node
+ */
+ @Override
+ public void visitBeforeChildren(Node node, ConfigurationKey key)
+ {
+ if (!isAttributeNode(node))
+ {
+ fireElementStart(nodeName(node), fetchAttributes(node));
+
+ if (node.getValue() != null)
+ {
+ fireCharacters(node.getValue().toString());
+ }
+ }
+ }
+
+ /**
+ * Checks if iteration should be terminated. This implementation stops
+ * iteration after an exception has occurred.
+ *
+ * @return a flag if iteration should be stopped
+ */
+ @Override
+ public boolean terminate()
+ {
+ return getException() != null;
+ }
+
+ /**
+ * Returns an object with all attributes for the specified node.
+ *
+ * @param node the actual node
+ * @return an object with all attributes of this node
+ */
+ protected Attributes fetchAttributes(Node node)
+ {
+ AttributesImpl attrs = new AttributesImpl();
+
+ for (ConfigurationNode child : node.getAttributes())
+ {
+ if (child.getValue() != null)
+ {
+ String attr = child.getName();
+ attrs.addAttribute(NS_URI, attr, attr, ATTR_TYPE, child.getValue().toString());
+ }
+ }
+
+ return attrs;
+ }
+
+ /**
+ * Helper method for determining the name of a node. If a node has no
+ * name (which is true for the root node), the specified default name
+ * will be used.
+ *
+ * @param node the node to be checked
+ * @return the name for this node
+ */
+ private String nodeName(Node node)
+ {
+ return (node.getName() == null) ? getRootName() : node.getName();
+ }
+
+ /**
+ * Checks if the specified node is an attribute node. In the node
+ * hierarchy attributes are stored as normal child nodes, but with
+ * special names.
+ *
+ * @param node the node to be checked
+ * @return a flag if this is an attribute node
+ */
+ private boolean isAttributeNode(Node node)
+ {
+ return node.isAttribute();
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/HierarchicalINIConfiguration.java b/src/main/java/org/apache/commons/configuration/HierarchicalINIConfiguration.java
new file mode 100644
index 0000000..3a47b00
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/HierarchicalINIConfiguration.java
@@ -0,0 +1,884 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.ViewNode;
+
+/**
+ * <p>
+ * A specialized hierarchical configuration implementation for parsing ini
+ * files.
+ * </p>
+ * <p>
+ * An initialization or ini file is a configuration file typically found on
+ * Microsoft's Windows operating system and contains data for Windows based
+ * applications.
+ * </p>
+ * <p>
+ * Although popularized by Windows, ini files can be used on any system or
+ * platform due to the fact that they are merely text files that can easily be
+ * parsed and modified by both humans and computers.
+ * </p>
+ * <p>
+ * A typical ini file could look something like:
+ * </p>
+ * <pre>
+ * [section1]
+ * ; this is a comment!
+ * var1 = foo
+ * var2 = bar
+ *
+ * [section2]
+ * var1 = doo
+ * </pre>
+ * <p>
+ * The format of ini files is fairly straight forward and is composed of three
+ * components:<br>
+ * <ul>
+ * <li><b>Sections:</b> Ini files are split into sections, each section starting
+ * with a section declaration. A section declaration starts with a '[' and ends
+ * with a ']'. Sections occur on one line only.</li>
+ * <li><b>Parameters:</b> Items in a section are known as parameters. Parameters
+ * have a typical {@code key = value} format.</li>
+ * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * There are various implementations of the ini file format by various vendors
+ * which has caused a number of differences to appear. As far as possible this
+ * configuration tries to be lenient and support most of the differences.
+ * </p>
+ * <p>
+ * Some of the differences supported are as follows:
+ * <ul>
+ * <li><b>Comments:</b> The '#' character is also accepted as a comment
+ * signifier.</li>
+ * <li><b>Key value separator:</b> The ':' character is also accepted in place of
+ * '=' to separate keys and values in parameters, for example
+ * {@code var1 : foo}.</li>
+ * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed,
+ * this configuration does however support this feature. In the event of a duplicate
+ * section, the two section's values are merged so that there is only a single
+ * section. <strong>Note</strong>: This also affects the internal data of the
+ * configuration. If it is saved, only a single section is written!</li>
+ * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only
+ * allowed if they are in two different sections, thus they are local to
+ * sections; this configuration simply merges duplicates; if a section has a
+ * duplicate parameter the values are then added to the key as a list.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * Global parameters are also allowed; any parameters declared before a section
+ * is declared are added to a global section. It is important to note that this
+ * global section does not have a name.
+ * </p>
+ * <p>
+ * In all instances, a parameter's key is prepended with its section name and a
+ * '.' (period). Thus a parameter named "var1" in "section1" will have the key
+ * {@code section1.var1} in this configuration. (This is the default
+ * behavior. Because this is a hierarchical configuration you can change this by
+ * setting a different {@link org.apache.commons.configuration.tree.ExpressionEngine}.)
+ * </p>
+ * <p>
+ * <h3>Implementation Details:</h3> Consider the following ini file:<br>
+ * <pre>
+ * default = ok
+ *
+ * [section1]
+ * var1 = foo
+ * var2 = doodle
+ *
+ * [section2]
+ * ; a comment
+ * var1 = baz
+ * var2 = shoodle
+ * bad =
+ * = worse
+ *
+ * [section3]
+ * # another comment
+ * var1 : foo
+ * var2 : bar
+ * var5 : test1
+ *
+ * [section3]
+ * var3 = foo
+ * var4 = bar
+ * var5 = test2
+ *
+ * [sectionSeparators]
+ * passwd : abc=def
+ * a:b = "value"
+ * </pre>
+ * </p>
+ * <p>
+ * This ini file will be parsed without error. Note:
+ * <ul>
+ * <li>The parameter named "default" is added to the global section, it's value
+ * is accessed simply using {@code getProperty("default")}.</li>
+ * <li>Section 1's parameters can be accessed using
+ * {@code getProperty("section1.var1")}.</li>
+ * <li>The parameter named "bad" simply adds the parameter with an empty value.</li>
+ * <li>The empty key with value "= worse" is added using a key consisting of a
+ * single space character. This key is still added to section 2 and the value
+ * can be accessed using {@code getProperty("section2. ")}, notice the
+ * period '.' and the space following the section name.</li>
+ * <li>Section three uses both '=' and ':' to separate keys and values.</li>
+ * <li>Section 3 has a duplicate key named "var5". The value for this key is
+ * [test1, test2], and is represented as a List.</li>
+ * <li>The section called <em>sectionSeparators</em> demonstrates how the
+ * configuration deals with multiple occurrences of separator characters. Per
+ * default the first separator character in a line is detected and used to
+ * split the key from the value. Therefore the first property definition in this
+ * section has the key {@code passwd} and the value {@code abc=def}.
+ * This default behavior can be changed by using quotes. If there is a separator
+ * character before the first quote character (ignoring whitespace), this
+ * character is used as separator. Thus the second property definition in the
+ * section has the key {@code a:b} and the value {@code value}.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * Internally, this configuration maps the content of the represented ini file
+ * to its node structure in the following way:
+ * <ul>
+ * <li>Sections are represented by direct child nodes of the root node.</li>
+ * <li>For the content of a section, corresponding nodes are created as children
+ * of the section node.</li>
+ * </ul>
+ * This explains how the keys for the properties can be constructed. You can
+ * also use other methods of {@link HierarchicalConfiguration} for querying or
+ * manipulating the hierarchy of configuration nodes, for instance the
+ * {@code configurationAt()} method for obtaining the data of a specific
+ * section. However, be careful that the storage scheme described above is not
+ * violated (e.g. by adding multiple levels of nodes or inserting duplicate
+ * section nodes). Otherwise, the special methods for ini configurations may not
+ * work correctly!
+ * </p>
+ * <p>
+ * The set of sections in this configuration can be retrieved using the
+ * {@code getSections()} method. For obtaining a
+ * {@code SubnodeConfiguration} with the content of a specific section the
+ * {@code getSection()} method can be used.
+ * </p>
+ * <p>
+ * <em>Note:</em> Configuration objects of this type can be read concurrently by
+ * multiple threads. However if one of these threads modifies the object,
+ * synchronization has to be performed manually.
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: HierarchicalINIConfiguration.java 1234362 2012-01-21 16:59:48Z oheger $
+ * @since 1.6
+ */
+public class HierarchicalINIConfiguration extends
+ AbstractHierarchicalFileConfiguration
+{
+ /**
+ * The characters that signal the start of a comment line.
+ */
+ protected static final String COMMENT_CHARS = "#;";
+
+ /**
+ * The characters used to separate keys from values.
+ */
+ protected static final String SEPARATOR_CHARS = "=:";
+
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = 2548006161386850670L;
+
+ /**
+ * Constant for the line separator.
+ */
+ private static final String LINE_SEPARATOR = System.getProperty("line.separator");
+
+ /**
+ * The characters used for quoting values.
+ */
+ private static final String QUOTE_CHARACTERS = "\"'";
+
+ /**
+ * The line continuation character.
+ */
+ private static final String LINE_CONT = "\\";
+
+ /**
+ * Create a new empty INI Configuration.
+ */
+ public HierarchicalINIConfiguration()
+ {
+ super();
+ }
+
+ /**
+ * Create and load the ini configuration from the given file.
+ *
+ * @param filename The name pr path of the ini file to load.
+ * @throws ConfigurationException If an error occurs while loading the file
+ */
+ public HierarchicalINIConfiguration(String filename)
+ throws ConfigurationException
+ {
+ super(filename);
+ }
+
+ /**
+ * Create and load the ini configuration from the given file.
+ *
+ * @param file The ini file to load.
+ * @throws ConfigurationException If an error occurs while loading the file
+ */
+ public HierarchicalINIConfiguration(File file)
+ throws ConfigurationException
+ {
+ super(file);
+ }
+
+ /**
+ * Create and load the ini configuration from the given url.
+ *
+ * @param url The url of the ini file to load.
+ * @throws ConfigurationException If an error occurs while loading the file
+ */
+ public HierarchicalINIConfiguration(URL url) throws ConfigurationException
+ {
+ super(url);
+ }
+
+ /**
+ * Save the configuration to the specified writer.
+ *
+ * @param writer - The writer to save the configuration to.
+ * @throws ConfigurationException If an error occurs while writing the
+ * configuration
+ */
+ public void save(Writer writer) throws ConfigurationException
+ {
+ PrintWriter out = new PrintWriter(writer);
+ Iterator<String> it = getSections().iterator();
+ while (it.hasNext())
+ {
+ String section = it.next();
+ Configuration subset;
+ if (section != null)
+ {
+ out.print("[");
+ out.print(section);
+ out.print("]");
+ out.println();
+ subset = createSubnodeConfiguration(getSectionNode(section));
+ }
+ else
+ {
+ subset = getSection(null);
+ }
+
+ Iterator<String> keys = subset.getKeys();
+ while (keys.hasNext())
+ {
+ String key = keys.next();
+ Object value = subset.getProperty(key);
+ if (value instanceof Collection)
+ {
+ Iterator<?> values = ((Collection<?>) value).iterator();
+ while (values.hasNext())
+ {
+ value = values.next();
+ out.print(key);
+ out.print(" = ");
+ out.print(formatValue(value.toString()));
+ out.println();
+ }
+ }
+ else
+ {
+ out.print(key);
+ out.print(" = ");
+ out.print(formatValue(value.toString()));
+ out.println();
+ }
+ }
+
+ out.println();
+ }
+
+ out.flush();
+ }
+
+ /**
+ * Load the configuration from the given reader. Note that the
+ * {@code clear()} method is not called so the configuration read in will
+ * be merged with the current configuration.
+ *
+ * @param reader The reader to read the configuration from.
+ * @throws ConfigurationException If an error occurs while reading the
+ * configuration
+ */
+ public void load(Reader reader) throws ConfigurationException
+ {
+ try
+ {
+ BufferedReader bufferedReader = new BufferedReader(reader);
+ ConfigurationNode sectionNode = getRootNode();
+
+ String line = bufferedReader.readLine();
+ while (line != null)
+ {
+ line = line.trim();
+ if (!isCommentLine(line))
+ {
+ if (isSectionLine(line))
+ {
+ String section = line.substring(1, line.length() - 1);
+ sectionNode = getSectionNode(section);
+ }
+
+ else
+ {
+ String key = "";
+ String value = "";
+ int index = findSeparator(line);
+ if (index >= 0)
+ {
+ key = line.substring(0, index);
+ value = parseValue(line.substring(index + 1), bufferedReader);
+ }
+ else
+ {
+ key = line;
+ }
+ key = key.trim();
+ if (key.length() < 1)
+ {
+ // use space for properties with no key
+ key = " ";
+ }
+ createValueNodes(sectionNode, key, value);
+ }
+ }
+
+ line = bufferedReader.readLine();
+ }
+ }
+ catch (IOException e)
+ {
+ throw new ConfigurationException(
+ "Unable to load the configuration", e);
+ }
+ }
+
+ /**
+ * Creates the node(s) for the given key value-pair. If delimiter parsing is
+ * enabled, the value string is split if possible, and for each single value
+ * a node is created. Otherwise only a single node is added to the section.
+ *
+ * @param sectionNode the section node new nodes have to be added
+ * @param key the key
+ * @param value the value string
+ */
+ private void createValueNodes(ConfigurationNode sectionNode, String key,
+ String value)
+ {
+ Collection<String> values;
+ if (isDelimiterParsingDisabled())
+ {
+ values = Collections.singleton(value);
+ }
+ else
+ {
+ values = PropertyConverter.split(value, getListDelimiter(), false);
+ }
+
+ for (String v : values)
+ {
+ ConfigurationNode node = createNode(key);
+ node.setValue(v);
+ sectionNode.addChild(node);
+ }
+ }
+
+ /**
+ * Parse the value to remove the quotes and ignoring the comment. Example:
+ *
+ * <pre>
+ * "value" ; comment -> value
+ * </pre>
+ *
+ * <pre>
+ * 'value' ; comment -> value
+ * </pre>
+ * Note that a comment character is only recognized if there is at least one
+ * whitespace character before it. So it can appear in the property value,
+ * e.g.:
+ * <pre>
+ * C:\\Windows;C:\\Windows\\system32
+ * </pre>
+ *
+ * @param val the value to be parsed
+ * @param reader the reader (needed if multiple lines have to be read)
+ * @throws IOException if an IO error occurs
+ */
+ private static String parseValue(String val, BufferedReader reader) throws IOException
+ {
+ StringBuilder propertyValue = new StringBuilder();
+ boolean lineContinues;
+ String value = val.trim();
+
+ do
+ {
+ boolean quoted = value.startsWith("\"") || value.startsWith("'");
+ boolean stop = false;
+ boolean escape = false;
+
+ char quote = quoted ? value.charAt(0) : 0;
+
+ int i = quoted ? 1 : 0;
+
+ StringBuilder result = new StringBuilder();
+ char lastChar = 0;
+ while (i < value.length() && !stop)
+ {
+ char c = value.charAt(i);
+
+ if (quoted)
+ {
+ if ('\\' == c && !escape)
+ {
+ escape = true;
+ }
+ else if (!escape && quote == c)
+ {
+ stop = true;
+ }
+ else if (escape && quote == c)
+ {
+ escape = false;
+ result.append(c);
+ }
+ else
+ {
+ if (escape)
+ {
+ escape = false;
+ result.append('\\');
+ }
+
+ result.append(c);
+ }
+ }
+ else
+ {
+ if (isCommentChar(c) && Character.isWhitespace(lastChar))
+ {
+ stop = true;
+ }
+ else
+ {
+ result.append(c);
+ }
+ }
+
+ i++;
+ lastChar = c;
+ }
+
+ String v = result.toString();
+ if (!quoted)
+ {
+ v = v.trim();
+ lineContinues = lineContinues(v);
+ if (lineContinues)
+ {
+ // remove trailing "\"
+ v = v.substring(0, v.length() - 1).trim();
+ }
+ }
+ else
+ {
+ lineContinues = lineContinues(value, i);
+ }
+ propertyValue.append(v);
+
+ if (lineContinues)
+ {
+ propertyValue.append(LINE_SEPARATOR);
+ value = reader.readLine();
+ }
+ } while (lineContinues && value != null);
+
+ return propertyValue.toString();
+ }
+
+ /**
+ * Tests whether the specified string contains a line continuation marker.
+ *
+ * @param line the string to check
+ * @return a flag whether this line continues
+ */
+ private static boolean lineContinues(String line)
+ {
+ String s = line.trim();
+ return s.equals(LINE_CONT)
+ || (s.length() > 2 && s.endsWith(LINE_CONT) && Character
+ .isWhitespace(s.charAt(s.length() - 2)));
+ }
+
+ /**
+ * Tests whether the specified string contains a line continuation marker
+ * after the specified position. This method parses the string to remove a
+ * comment that might be present. Then it checks whether a line continuation
+ * marker can be found at the end.
+ *
+ * @param line the line to check
+ * @param pos the start position
+ * @return a flag whether this line continues
+ */
+ private static boolean lineContinues(String line, int pos)
+ {
+ String s;
+
+ if (pos >= line.length())
+ {
+ s = line;
+ }
+ else
+ {
+ int end = pos;
+ while (end < line.length() && !isCommentChar(line.charAt(end)))
+ {
+ end++;
+ }
+ s = line.substring(pos, end);
+ }
+
+ return lineContinues(s);
+ }
+
+ /**
+ * Tests whether the specified character is a comment character.
+ *
+ * @param c the character
+ * @return a flag whether this character starts a comment
+ */
+ private static boolean isCommentChar(char c)
+ {
+ return COMMENT_CHARS.indexOf(c) >= 0;
+ }
+
+ /**
+ * Tries to find the index of the separator character in the given string.
+ * This method checks for the presence of separator characters in the given
+ * string. If multiple characters are found, the first one is assumed to be
+ * the correct separator. If there are quoting characters, they are taken
+ * into account, too.
+ *
+ * @param line the line to be checked
+ * @return the index of the separator character or -1 if none is found
+ */
+ private static int findSeparator(String line)
+ {
+ int index =
+ findSeparatorBeforeQuote(line,
+ findFirstOccurrence(line, QUOTE_CHARACTERS));
+ if (index < 0)
+ {
+ index = findFirstOccurrence(line, SEPARATOR_CHARS);
+ }
+ return index;
+ }
+
+ /**
+ * Checks for the occurrence of the specified separators in the given line.
+ * The index of the first separator is returned.
+ *
+ * @param line the line to be investigated
+ * @param separators a string with the separator characters to look for
+ * @return the lowest index of a separator character or -1 if no separator
+ * is found
+ */
+ private static int findFirstOccurrence(String line, String separators)
+ {
+ int index = -1;
+
+ for (int i = 0; i < separators.length(); i++)
+ {
+ char sep = separators.charAt(i);
+ int pos = line.indexOf(sep);
+ if (pos >= 0)
+ {
+ if (index < 0 || pos < index)
+ {
+ index = pos;
+ }
+ }
+ }
+
+ return index;
+ }
+
+ /**
+ * Searches for a separator character directly before a quoting character.
+ * If the first non-whitespace character before a quote character is a
+ * separator, it is considered the "real" separator in this line - even if
+ * there are other separators before.
+ *
+ * @param line the line to be investigated
+ * @param quoteIndex the index of the quote character
+ * @return the index of the separator before the quote or < 0 if there is
+ * none
+ */
+ private static int findSeparatorBeforeQuote(String line, int quoteIndex)
+ {
+ int index = quoteIndex - 1;
+ while (index >= 0 && Character.isWhitespace(line.charAt(index)))
+ {
+ index--;
+ }
+
+ if (index >= 0 && SEPARATOR_CHARS.indexOf(line.charAt(index)) < 0)
+ {
+ index = -1;
+ }
+
+ return index;
+ }
+
+ /**
+ * Add quotes around the specified value if it contains a comment character.
+ */
+ private String formatValue(String value)
+ {
+ boolean quoted = false;
+
+ for (int i = 0; i < COMMENT_CHARS.length() && !quoted; i++)
+ {
+ char c = COMMENT_CHARS.charAt(i);
+ if (value.indexOf(c) != -1)
+ {
+ quoted = true;
+ }
+ }
+
+ if (quoted)
+ {
+ return '"' + value.replaceAll("\"", "\\\\\\\"") + '"';
+ }
+ else
+ {
+ return value;
+ }
+ }
+
+ /**
+ * Determine if the given line is a comment line.
+ *
+ * @param line The line to check.
+ * @return true if the line is empty or starts with one of the comment
+ * characters
+ */
+ protected boolean isCommentLine(String line)
+ {
+ if (line == null)
+ {
+ return false;
+ }
+ // blank lines are also treated as comment lines
+ return line.length() < 1 || COMMENT_CHARS.indexOf(line.charAt(0)) >= 0;
+ }
+
+ /**
+ * Determine if the given line is a section.
+ *
+ * @param line The line to check.
+ * @return true if the line contains a section
+ */
+ protected boolean isSectionLine(String line)
+ {
+ if (line == null)
+ {
+ return false;
+ }
+ return line.startsWith("[") && line.endsWith("]");
+ }
+
+ /**
+ * Return a set containing the sections in this ini configuration. Note that
+ * changes to this set do not affect the configuration.
+ *
+ * @return a set containing the sections.
+ */
+ public Set<String> getSections()
+ {
+ Set<String> sections = new LinkedHashSet<String>();
+ boolean globalSection = false;
+ boolean inSection = false;
+
+ for (ConfigurationNode node : getRootNode().getChildren())
+ {
+ if (isSectionNode(node))
+ {
+ inSection = true;
+ sections.add(node.getName());
+ }
+ else
+ {
+ if (!inSection && !globalSection)
+ {
+ globalSection = true;
+ sections.add(null);
+ }
+ }
+ }
+
+ return sections;
+ }
+
+ /**
+ * Returns a configuration with the content of the specified section. This
+ * provides an easy way of working with a single section only. The way this
+ * configuration is structured internally, this method is very similar to
+ * calling {@link HierarchicalConfiguration#configurationAt(String)} with
+ * the name of the section in question. There are the following differences
+ * however:
+ * <ul>
+ * <li>This method never throws an exception. If the section does not exist,
+ * it is created now. The configuration returned in this case is empty.</li>
+ * <li>If section is contained multiple times in the configuration, the
+ * configuration returned by this method is initialized with the first
+ * occurrence of the section. (This can only happen if
+ * {@code addProperty()} has been used in a way that does not conform
+ * to the storage scheme used by {@code HierarchicalINIConfiguration}.
+ * If used correctly, there will not be duplicate sections.)</li>
+ * <li>There is special support for the global section: Passing in
+ * <b>null</b> as section name returns a configuration with the content of
+ * the global section (which may also be empty).</li>
+ * </ul>
+ *
+ * @param name the name of the section in question; <b>null</b> represents
+ * the global section
+ * @return a configuration containing only the properties of the specified
+ * section
+ */
+ public SubnodeConfiguration getSection(String name)
+ {
+ if (name == null)
+ {
+ return getGlobalSection();
+ }
+
+ else
+ {
+ try
+ {
+ return configurationAt(name);
+ }
+ catch (IllegalArgumentException iex)
+ {
+ // the passed in key does not map to exactly one node
+ // obtain the node for the section, create it on demand
+ return new SubnodeConfiguration(this, getSectionNode(name));
+ }
+ }
+ }
+
+ /**
+ * Obtains the node representing the specified section. This method is
+ * called while the configuration is loaded. If a node for this section
+ * already exists, it is returned. Otherwise a new node is created.
+ *
+ * @param sectionName the name of the section
+ * @return the node for this section
+ */
+ private ConfigurationNode getSectionNode(String sectionName)
+ {
+ List<ConfigurationNode> nodes = getRootNode().getChildren(sectionName);
+ if (!nodes.isEmpty())
+ {
+ return nodes.get(0);
+ }
+
+ ConfigurationNode node = createNode(sectionName);
+ markSectionNode(node);
+ getRootNode().addChild(node);
+ return node;
+ }
+
+ /**
+ * Creates a sub configuration for the global section of the represented INI
+ * configuration.
+ *
+ * @return the sub configuration for the global section
+ */
+ private SubnodeConfiguration getGlobalSection()
+ {
+ ViewNode parent = new ViewNode();
+
+ for (ConfigurationNode node : getRootNode().getChildren())
+ {
+ if (!isSectionNode(node))
+ {
+ synchronized (node)
+ {
+ parent.addChild(node);
+ }
+ }
+ }
+
+ return createSubnodeConfiguration(parent);
+ }
+
+ /**
+ * Marks a configuration node as a section node. This means that this node
+ * represents a section header. This implementation uses the node's
+ * reference property to store a flag.
+ *
+ * @param node the node to be marked
+ */
+ private static void markSectionNode(ConfigurationNode node)
+ {
+ node.setReference(Boolean.TRUE);
+ }
+
+ /**
+ * Checks whether the specified configuration node represents a section.
+ *
+ * @param node the node in question
+ * @return a flag whether this node represents a section
+ */
+ private static boolean isSectionNode(ConfigurationNode node)
+ {
+ return node.getReference() != null || node.getChildrenCount() > 0;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/HierarchicalReloadableConfiguration.java b/src/main/java/org/apache/commons/configuration/HierarchicalReloadableConfiguration.java
new file mode 100644
index 0000000..f56a680
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/HierarchicalReloadableConfiguration.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import org.apache.commons.configuration.reloading.Reloadable;
+
+/**
+ * <p>A base class for hierarchical configurations with specific reloading
+ * requirements.</p>
+ * <p>This class manages a lock object which can be used for synchronization.</p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @since 1.7
+ * @version $Id: HierarchicalReloadableConfiguration.java 1210000 2011-12-03 20:43:38Z oheger $
+ */
+public class HierarchicalReloadableConfiguration extends HierarchicalConfiguration
+ implements Reloadable
+{
+ /** Constant for the name used for the lock object. */
+ private static final String LOCK_NAME = "HierarchicalReloadableConfigurationLock";
+
+ /** The lock object used by this instance. */
+ private final Object reloadLock;
+
+ /**
+ * Creates a new instance of {@code HierarchicalReloadableConfiguration}.
+ */
+ public HierarchicalReloadableConfiguration()
+ {
+ super();
+ reloadLock = new Lock(LOCK_NAME);
+ }
+
+ /**
+ * Creates a new instance of {@code HierarchicalReloadableConfiguration} and
+ * initializes it with the given lock object.
+ *
+ * @param lock the lock object
+ */
+ public HierarchicalReloadableConfiguration(Object lock)
+ {
+ super();
+ reloadLock = lock == null ? new Lock(LOCK_NAME) : lock;
+ }
+
+ /**
+ * Creates a new instance of {@code HierarchicalReloadableConfiguration} and
+ * copies all data contained in the specified configuration into the new
+ * one.
+ *
+ * @param c the configuration that is to be copied (if <b>null</b>, this
+ * constructor will behave like the standard constructor)
+ */
+ public HierarchicalReloadableConfiguration(HierarchicalConfiguration c)
+ {
+ super(c);
+ reloadLock = new Lock(LOCK_NAME);
+ }
+
+ @Override
+ public Object getReloadLock()
+ {
+ return reloadLock;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/HierarchicalXMLConfiguration.java b/src/main/java/org/apache/commons/configuration/HierarchicalXMLConfiguration.java
new file mode 100644
index 0000000..a99962f
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/HierarchicalXMLConfiguration.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+/**
+ * A specialized hierarchical configuration class that is able to parse XML
+ * documents.
+ *
+ * <p>The parsed document will be stored keeping its structure. The contained
+ * properties can be accessed using all methods supported by the base class
+ * {@code HierarchicalConfiguration}.
+ *
+ * @since commons-configuration 1.0
+ *
+ * @author Jörg Schaible
+ * @version $Id: HierarchicalXMLConfiguration.java 1210002 2011-12-03 20:54:17Z oheger $
+ * @deprecated This class is deprecated. Use {@code XMLConfiguration}
+ * instead, which supports all features this class had offered before.
+ */
+ at Deprecated
+public class HierarchicalXMLConfiguration extends XMLConfiguration
+{
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = 5597530014798917521L;
+}
diff --git a/src/main/java/org/apache/commons/configuration/INIConfiguration.java b/src/main/java/org/apache/commons/configuration/INIConfiguration.java
new file mode 100644
index 0000000..26a42a2
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/INIConfiguration.java
@@ -0,0 +1,493 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * <p>
+ * An initialization or ini file is a configuration file typically found on
+ * Microsoft's Windows operating system and contains data for Windows based
+ * applications.
+ * </p>
+ *
+ * <p>
+ * Although popularized by Windows, ini files can be used on any system or
+ * platform due to the fact that they are merely text files that can easily be
+ * parsed and modified by both humans and computers.
+ * </p>
+ *
+ * <p>
+ * A typical ini file could look something like:
+ * </p>
+ * <pre>
+ * [section1]
+ * ; this is a comment!
+ * var1 = foo
+ * var2 = bar
+ *
+ * [section2]
+ * var1 = doo
+ * </pre>
+ *
+ * <p>
+ * The format of ini files is fairly straight forward and is composed of three
+ * components:<br>
+ * <ul>
+ * <li><b>Sections:</b> Ini files are split into sections, each section
+ * starting with a section declaration. A section declaration starts with a '['
+ * and ends with a ']'. Sections occur on one line only.</li>
+ * <li><b>Parameters:</b> Items in a section are known as parameters.
+ * Parameters have a typical {@code key = value} format.</li>
+ * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.
+ * </li>
+ * </ul>
+ * </p>
+ *
+ * <p>
+ * There are various implementations of the ini file format by various vendors
+ * which has caused a number of differences to appear. As far as possible this
+ * configuration tries to be lenient and support most of the differences.
+ * </p>
+ *
+ * <p>
+ * Some of the differences supported are as follows:
+ * <ul>
+ * <li><b>Comments:</b> The '#' character is also accepted as a comment
+ * signifier.</li>
+ * <li><b>Key value separtor:</b> The ':' character is also accepted in place
+ * of '=' to separate keys and values in parameters, for example
+ * {@code var1 : foo}.</li>
+ * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed ,
+ * this configuration does however support it. In the event of a duplicate
+ * section, the two section's values are merged.</li>
+ * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only
+ * allowed if they are in two different sections, thus they are local to
+ * sections; this configuration simply merges duplicates; if a section has a
+ * duplicate parameter the values are then added to the key as a list. </li>
+ * </ul>
+ * </p>
+ * <p>
+ * Global parameters are also allowed; any parameters declared before a section
+ * is declared are added to a global section. It is important to note that this
+ * global section does not have a name.
+ * </p>
+ * <p>
+ * In all instances, a parameter's key is prepended with its section name and a
+ * '.' (period). Thus a parameter named "var1" in "section1" will have the key
+ * {@code section1.var1} in this configuration. Thus, a section's
+ * parameters can easily be retrieved using the {@code subset} method
+ * using the section name as the prefix.
+ * </p>
+ * <p>
+ * <h3>Implementation Details:</h3>
+ * Consider the following ini file:<br>
+ * <pre>
+ * default = ok
+ *
+ * [section1]
+ * var1 = foo
+ * var2 = doodle
+ *
+ * [section2]
+ * ; a comment
+ * var1 = baz
+ * var2 = shoodle
+ * bad =
+ * = worse
+ *
+ * [section3]
+ * # another comment
+ * var1 : foo
+ * var2 : bar
+ * var5 : test1
+ *
+ * [section3]
+ * var3 = foo
+ * var4 = bar
+ * var5 = test2
+ * </pre>
+ * </p>
+ * <p>
+ * This ini file will be parsed without error. Note:
+ * <ul>
+ * <li>The parameter named "default" is added to the global section, it's value
+ * is accessed simply using {@code getProperty("default")}.</li>
+ * <li>Section 1's parameters can be accessed using
+ * {@code getProperty("section1.var1")}.</li>
+ * <li>The parameter named "bad" simply adds the parameter with an empty value.
+ * </li>
+ * <li>The empty key with value "= worse" is added using an empty key. This key
+ * is still added to section 2 and the value can be accessed using
+ * {@code getProperty("section2.")}, notice the period '.' following the
+ * section name.</li>
+ * <li>Section three uses both '=' and ':' to separate keys and values.</li>
+ * <li>Section 3 has a duplicate key named "var5". The value for this key is
+ * [test1, test2], and is represented as a List.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * The set of sections in this configuration can be retrieved using the
+ * {@code getSections} method.
+ * </p>
+ * <p>
+ * <em>Note:</em> Configuration objects of this type can be read concurrently
+ * by multiple threads. However if one of these threads modifies the object,
+ * synchronization has to be performed manually.
+ * </p>
+ *
+ * @author Trevor Miller
+ * @version $Id: INIConfiguration.java 1210003 2011-12-03 20:54:46Z oheger $
+ * @since 1.4
+ * @deprecated This class has been replaced by HierarchicalINIConfiguration,
+ * which provides a superset of the functionality offered by this class.
+ */
+ at Deprecated
+public class INIConfiguration extends AbstractFileConfiguration
+{
+ /**
+ * The characters that signal the start of a comment line.
+ */
+ protected static final String COMMENT_CHARS = "#;";
+
+ /**
+ * The characters used to separate keys from values.
+ */
+ protected static final String SEPARATOR_CHARS = "=:";
+
+ /**
+ * Create a new empty INI Configuration.
+ */
+ public INIConfiguration()
+ {
+ super();
+ }
+
+ /**
+ * Create and load the ini configuration from the given file.
+ *
+ * @param filename The name pr path of the ini file to load.
+ * @throws ConfigurationException If an error occurs while loading the file
+ */
+ public INIConfiguration(String filename) throws ConfigurationException
+ {
+ super(filename);
+ }
+
+ /**
+ * Create and load the ini configuration from the given file.
+ *
+ * @param file The ini file to load.
+ * @throws ConfigurationException If an error occurs while loading the file
+ */
+ public INIConfiguration(File file) throws ConfigurationException
+ {
+ super(file);
+ }
+
+ /**
+ * Create and load the ini configuration from the given url.
+ *
+ * @param url The url of the ini file to load.
+ * @throws ConfigurationException If an error occurs while loading the file
+ */
+ public INIConfiguration(URL url) throws ConfigurationException
+ {
+ super(url);
+ }
+
+ /**
+ * Save the configuration to the specified writer.
+ *
+ * @param writer - The writer to save the configuration to.
+ * @throws ConfigurationException If an error occurs while writing the
+ * configuration
+ */
+ public void save(Writer writer) throws ConfigurationException
+ {
+ PrintWriter out = new PrintWriter(writer);
+ Iterator<String> it = getSections().iterator();
+ while (it.hasNext())
+ {
+ String section = it.next();
+ out.print("[");
+ out.print(section);
+ out.print("]");
+ out.println();
+
+ Configuration subset = subset(section);
+ Iterator<String> keys = subset.getKeys();
+ while (keys.hasNext())
+ {
+ String key = keys.next();
+ Object value = subset.getProperty(key);
+ if (value instanceof Collection)
+ {
+ Iterator<?> values = ((Collection<?>) value).iterator();
+ while (values.hasNext())
+ {
+ value = values.next();
+ out.print(key);
+ out.print(" = ");
+ out.print(formatValue(value.toString()));
+ out.println();
+ }
+ }
+ else
+ {
+ out.print(key);
+ out.print(" = ");
+ out.print(formatValue(value.toString()));
+ out.println();
+ }
+ }
+
+ out.println();
+ }
+
+ out.flush();
+ }
+
+ /**
+ * Load the configuration from the given reader. Note that the
+ * {@code clear()} method is not called so the configuration read in
+ * will be merged with the current configuration.
+ *
+ * @param reader The reader to read the configuration from.
+ * @throws ConfigurationException If an error occurs while reading the
+ * configuration
+ */
+ public void load(Reader reader) throws ConfigurationException
+ {
+ try
+ {
+ BufferedReader bufferedReader = new BufferedReader(reader);
+ String line = bufferedReader.readLine();
+ String section = "";
+ while (line != null)
+ {
+ line = line.trim();
+ if (!isCommentLine(line))
+ {
+ if (isSectionLine(line))
+ {
+ section = line.substring(1, line.length() - 1) + ".";
+ }
+ else
+ {
+ String key = "";
+ String value = "";
+ int index = line.indexOf("=");
+ if (index >= 0)
+ {
+ key = section + line.substring(0, index);
+ value = parseValue(line.substring(index + 1));
+ }
+ else
+ {
+ index = line.indexOf(":");
+ if (index >= 0)
+ {
+ key = section + line.substring(0, index);
+ value = parseValue(line.substring(index + 1));
+ }
+ else
+ {
+ key = section + line;
+ }
+ }
+ addProperty(key.trim(), value);
+ }
+ }
+ line = bufferedReader.readLine();
+ }
+ }
+ catch (IOException e)
+ {
+ throw new ConfigurationException("Unable to load the configuration", e);
+ }
+ }
+
+ /**
+ * Parse the value to remove the quotes and ignoring the comment.
+ * Example:
+ *
+ * <pre>"value" ; comment -> value</pre>
+ *
+ * <pre>'value' ; comment -> value</pre>
+ *
+ * @param value
+ */
+ private String parseValue(String value)
+ {
+ value = value.trim();
+
+ boolean quoted = value.startsWith("\"") || value.startsWith("'");
+ boolean stop = false;
+ boolean escape = false;
+
+ char quote = quoted ? value.charAt(0) : 0;
+
+ int i = quoted ? 1 : 0;
+
+ StringBuilder result = new StringBuilder();
+ while (i < value.length() && !stop)
+ {
+ char c = value.charAt(i);
+
+ if (quoted)
+ {
+ if ('\\' == c && !escape)
+ {
+ escape = true;
+ }
+ else if (!escape && quote == c)
+ {
+ stop = true;
+ }
+ else if (escape && quote == c)
+ {
+ escape = false;
+ result.append(c);
+ }
+ else
+ {
+ if (escape)
+ {
+ escape = false;
+ result.append('\\');
+ }
+
+ result.append(c);
+ }
+ }
+ else
+ {
+ if (COMMENT_CHARS.indexOf(c) == -1)
+ {
+ result.append(c);
+ }
+ else
+ {
+ stop = true;
+ }
+ }
+
+ i++;
+ }
+
+ String v = result.toString();
+ if (!quoted)
+ {
+ v = v.trim();
+ }
+ return v;
+ }
+
+ /**
+ * Add quotes around the specified value if it contains a comment character.
+ */
+ private String formatValue(String value)
+ {
+ boolean quoted = false;
+
+ for (int i = 0; i < COMMENT_CHARS.length() && !quoted; i++)
+ {
+ char c = COMMENT_CHARS.charAt(i);
+ if (value.indexOf(c) != -1)
+ {
+ quoted = true;
+ }
+ }
+
+ if (quoted)
+ {
+ return '"' + value.replaceAll("\"", "\\\\\\\"") + '"';
+ }
+ else
+ {
+ return value;
+ }
+ }
+
+ /**
+ * Determine if the given line is a comment line.
+ *
+ * @param line The line to check.
+ * @return true if the line is empty or starts with one of the comment
+ * characters
+ */
+ protected boolean isCommentLine(String line)
+ {
+ if (line == null)
+ {
+ return false;
+ }
+ // blank lines are also treated as comment lines
+ return line.length() < 1 || COMMENT_CHARS.indexOf(line.charAt(0)) >= 0;
+ }
+
+ /**
+ * Determine if the given line is a section.
+ *
+ * @param line The line to check.
+ * @return true if the line contains a secion
+ */
+ protected boolean isSectionLine(String line)
+ {
+ if (line == null)
+ {
+ return false;
+ }
+ return line.startsWith("[") && line.endsWith("]");
+ }
+
+ /**
+ * Return a set containing the sections in this ini configuration. Note that
+ * changes to this set do not affect the configuration.
+ *
+ * @return a set containing the sections.
+ */
+ public Set<String> getSections()
+ {
+ Set<String> sections = new TreeSet<String>();
+
+ Iterator<String> keys = getKeys();
+ while (keys.hasNext())
+ {
+ String key = keys.next();
+ int index = key.indexOf(".");
+ if (index >= 0)
+ {
+ sections.add(key.substring(0, index));
+ }
+ }
+
+ return sections;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/JNDIConfiguration.java b/src/main/java/org/apache/commons/configuration/JNDIConfiguration.java
new file mode 100644
index 0000000..3baf38a
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/JNDIConfiguration.java
@@ -0,0 +1,482 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NameClassPair;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.NotContextException;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * This Configuration class allows you to interface with a JNDI datasource.
+ * A JNDIConfiguration is read-only, write operations will throw an
+ * UnsupportedOperationException. The clear operations are supported but the
+ * underlying JNDI data source is not changed.
+ *
+ * @author <a href="mailto:epugh at upstate.com">Eric Pugh</a>
+ * @version $Id: JNDIConfiguration.java 1234985 2012-01-23 21:09:09Z oheger $
+ */
+public class JNDIConfiguration extends AbstractConfiguration
+{
+ /** The prefix of the context. */
+ private String prefix;
+
+ /** The initial JNDI context. */
+ private Context context;
+
+ /** The base JNDI context. */
+ private Context baseContext;
+
+ /** The Set of keys that have been virtually cleared. */
+ private Set<String> clearedProperties = new HashSet<String>();
+
+ /**
+ * Creates a JNDIConfiguration using the default initial context as the
+ * root of the properties.
+ *
+ * @throws NamingException thrown if an error occurs when initializing the default context
+ */
+ public JNDIConfiguration() throws NamingException
+ {
+ this((String) null);
+ }
+
+ /**
+ * Creates a JNDIConfiguration using the default initial context, shifted
+ * with the specified prefix, as the root of the properties.
+ *
+ * @param prefix the prefix
+ *
+ * @throws NamingException thrown if an error occurs when initializing the default context
+ */
+ public JNDIConfiguration(String prefix) throws NamingException
+ {
+ this(new InitialContext(), prefix);
+ }
+
+ /**
+ * Creates a JNDIConfiguration using the specified initial context as the
+ * root of the properties.
+ *
+ * @param context the initial context
+ */
+ public JNDIConfiguration(Context context)
+ {
+ this(context, null);
+ }
+
+ /**
+ * Creates a JNDIConfiguration using the specified initial context shifted
+ * by the specified prefix as the root of the properties.
+ *
+ * @param context the initial context
+ * @param prefix the prefix
+ */
+ public JNDIConfiguration(Context context, String prefix)
+ {
+ this.context = context;
+ this.prefix = prefix;
+ setLogger(LogFactory.getLog(getClass()));
+ addErrorLogListener();
+ }
+
+ /**
+ * This method recursive traverse the JNDI tree, looking for Context objects.
+ * When it finds them, it traverses them as well. Otherwise it just adds the
+ * values to the list of keys found.
+ *
+ * @param keys All the keys that have been found.
+ * @param context The parent context
+ * @param prefix What prefix we are building on.
+ * @param processedCtx a set with the so far processed objects
+ * @throws NamingException If JNDI has an issue.
+ */
+ private void recursiveGetKeys(Set<String> keys, Context context, String prefix,
+ Set<Context> processedCtx) throws NamingException
+ {
+ processedCtx.add(context);
+ NamingEnumeration<NameClassPair> elements = null;
+
+ try
+ {
+ elements = context.list("");
+
+ // iterates through the context's elements
+ while (elements.hasMore())
+ {
+ NameClassPair nameClassPair = elements.next();
+ String name = nameClassPair.getName();
+ Object object = context.lookup(name);
+
+ // build the key
+ StringBuilder key = new StringBuilder();
+ key.append(prefix);
+ if (key.length() > 0)
+ {
+ key.append(".");
+ }
+ key.append(name);
+
+ if (object instanceof Context)
+ {
+ // add the keys of the sub context
+ Context subcontext = (Context) object;
+ if (!processedCtx.contains(subcontext))
+ {
+ recursiveGetKeys(keys, subcontext, key.toString(),
+ processedCtx);
+ }
+ }
+ else
+ {
+ // add the key
+ keys.add(key.toString());
+ }
+ }
+ }
+ finally
+ {
+ // close the enumeration
+ if (elements != null)
+ {
+ elements.close();
+ }
+ }
+ }
+
+ /**
+ * Returns an iterator with all property keys stored in this configuration.
+ *
+ * @return an iterator with all keys
+ */
+ public Iterator<String> getKeys()
+ {
+ return getKeys("");
+ }
+
+ /**
+ * Returns an iterator with all property keys starting with the given
+ * prefix.
+ *
+ * @param prefix the prefix
+ * @return an iterator with the selected keys
+ */
+ @Override
+ public Iterator<String> getKeys(String prefix)
+ {
+ // build the path
+ String[] splitPath = StringUtils.split(prefix, ".");
+
+ List<String> path = Arrays.asList(splitPath);
+
+ try
+ {
+ // find the context matching the specified path
+ Context context = getContext(path, getBaseContext());
+
+ // return all the keys under the context found
+ Set<String> keys = new HashSet<String>();
+ if (context != null)
+ {
+ recursiveGetKeys(keys, context, prefix, new HashSet<Context>());
+ }
+ else if (containsKey(prefix))
+ {
+ // add the prefix if it matches exactly a property key
+ keys.add(prefix);
+ }
+
+ return keys.iterator();
+ }
+ catch (NameNotFoundException e)
+ {
+ // expected exception, no need to log it
+ return new ArrayList<String>().iterator();
+ }
+ catch (NamingException e)
+ {
+ fireError(EVENT_READ_PROPERTY, null, null, e);
+ return new ArrayList<String>().iterator();
+ }
+ }
+
+ /**
+ * Because JNDI is based on a tree configuration, we need to filter down the
+ * tree, till we find the Context specified by the key to start from.
+ * Otherwise return null.
+ *
+ * @param path the path of keys to traverse in order to find the context
+ * @param context the context to start from
+ * @return The context at that key's location in the JNDI tree, or null if not found
+ * @throws NamingException if JNDI has an issue
+ */
+ private Context getContext(List<String> path, Context context) throws NamingException
+ {
+ // return the current context if the path is empty
+ if (path == null || path.isEmpty())
+ {
+ return context;
+ }
+
+ String key = path.get(0);
+
+ // search a context matching the key in the context's elements
+ NamingEnumeration<NameClassPair> elements = null;
+
+ try
+ {
+ elements = context.list("");
+ while (elements.hasMore())
+ {
+ NameClassPair nameClassPair = elements.next();
+ String name = nameClassPair.getName();
+ Object object = context.lookup(name);
+
+ if (object instanceof Context && name.equals(key))
+ {
+ Context subcontext = (Context) object;
+
+ // recursive search in the sub context
+ return getContext(path.subList(1, path.size()), subcontext);
+ }
+ }
+ }
+ finally
+ {
+ if (elements != null)
+ {
+ elements.close();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a flag whether this configuration is empty.
+ *
+ * @return the empty flag
+ */
+ public boolean isEmpty()
+ {
+ try
+ {
+ NamingEnumeration<NameClassPair> enumeration = null;
+
+ try
+ {
+ enumeration = getBaseContext().list("");
+ return !enumeration.hasMore();
+ }
+ finally
+ {
+ // close the enumeration
+ if (enumeration != null)
+ {
+ enumeration.close();
+ }
+ }
+ }
+ catch (NamingException e)
+ {
+ fireError(EVENT_READ_PROPERTY, null, null, e);
+ return true;
+ }
+ }
+
+ /**
+ * <p><strong>This operation is not supported and will throw an
+ * UnsupportedOperationException.</strong></p>
+ *
+ * @param key the key
+ * @param value the value
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ public void setProperty(String key, Object value)
+ {
+ throw new UnsupportedOperationException("This operation is not supported");
+ }
+
+ /**
+ * Removes the specified property.
+ *
+ * @param key the key of the property to remove
+ */
+ @Override
+ public void clearProperty(String key)
+ {
+ clearedProperties.add(key);
+ }
+
+ /**
+ * Checks whether the specified key is contained in this configuration.
+ *
+ * @param key the key to check
+ * @return a flag whether this key is stored in this configuration
+ */
+ public boolean containsKey(String key)
+ {
+ if (clearedProperties.contains(key))
+ {
+ return false;
+ }
+ key = key.replaceAll("\\.", "/");
+ try
+ {
+ // throws a NamingException if JNDI doesn't contain the key.
+ getBaseContext().lookup(key);
+ return true;
+ }
+ catch (NameNotFoundException e)
+ {
+ // expected exception, no need to log it
+ return false;
+ }
+ catch (NamingException e)
+ {
+ fireError(EVENT_READ_PROPERTY, key, null, e);
+ return false;
+ }
+ }
+
+ /**
+ * Returns the prefix.
+ * @return the prefix
+ */
+ public String getPrefix()
+ {
+ return prefix;
+ }
+
+ /**
+ * Sets the prefix.
+ *
+ * @param prefix The prefix to set
+ */
+ public void setPrefix(String prefix)
+ {
+ this.prefix = prefix;
+
+ // clear the previous baseContext
+ baseContext = null;
+ }
+
+ /**
+ * Returns the value of the specified property.
+ *
+ * @param key the key of the property
+ * @return the value of this property
+ */
+ public Object getProperty(String key)
+ {
+ if (clearedProperties.contains(key))
+ {
+ return null;
+ }
+
+ try
+ {
+ key = key.replaceAll("\\.", "/");
+ return getBaseContext().lookup(key);
+ }
+ catch (NameNotFoundException e)
+ {
+ // expected exception, no need to log it
+ return null;
+ }
+ catch (NotContextException nctxex)
+ {
+ // expected exception, no need to log it
+ return null;
+ }
+ catch (NamingException e)
+ {
+ fireError(EVENT_READ_PROPERTY, key, null, e);
+ return null;
+ }
+ }
+
+ /**
+ * <p><strong>This operation is not supported and will throw an
+ * UnsupportedOperationException.</strong></p>
+ *
+ * @param key the key
+ * @param obj the value
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ protected void addPropertyDirect(String key, Object obj)
+ {
+ throw new UnsupportedOperationException("This operation is not supported");
+ }
+
+ /**
+ * Return the base context with the prefix applied.
+ *
+ * @return the base context
+ * @throws NamingException if an error occurs
+ */
+ public Context getBaseContext() throws NamingException
+ {
+ if (baseContext == null)
+ {
+ baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
+ }
+
+ return baseContext;
+ }
+
+ /**
+ * Return the initial context used by this configuration. This context is
+ * independent of the prefix specified.
+ *
+ * @return the initial context
+ */
+ public Context getContext()
+ {
+ return context;
+ }
+
+ /**
+ * Set the initial context of the configuration.
+ *
+ * @param context the context
+ */
+ public void setContext(Context context)
+ {
+ // forget the removed properties
+ clearedProperties.clear();
+
+ // change the context
+ this.context = context;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/Lock.java b/src/main/java/org/apache/commons/configuration/Lock.java
new file mode 100644
index 0000000..8a12bf2
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/Lock.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+/**
+ * <p>
+ * A simple class acting as lock.
+ * </p>
+ * <p>
+ * Instances of this class are used by some configuration classes to synchronize
+ * themselves.
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @since 1.7
+ * @version $Id: Lock.java 1301995 2012-03-17 20:24:16Z sebb $
+ */
+public class Lock
+{
+ /** A string used internally to synchronize counter updates. */
+ private static String counterLock = "Lock";
+
+ /** A counter for generating unique instance IDs. */
+ private static int counter;
+
+ /** The name of this lock. */
+ private final String name;
+
+ /** The unique ID of this lock instance. */
+ private final int instanceId;
+
+ /**
+ * Creates a new instance of {@code Lock} with the specified name.
+ *
+ * @param name the name of this lock
+ */
+ public Lock(String name)
+ {
+ this.name = name;
+ synchronized (counterLock)
+ {
+ instanceId = ++counter;
+ }
+ }
+
+ /**
+ * Returns the name of this lock.
+ *
+ * @return the name of this lock
+ */
+ public String getName()
+ {
+ return name;
+ }
+
+ /**
+ * Returns a string representation of this object. This implementation
+ * returns a string which contains the lock name and the instance ID.
+ *
+ * @return a string for this object
+ */
+ @Override
+ public String toString()
+ {
+ return "Lock: " + name + " id = " + instanceId + ": "
+ + super.toString();
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/MapConfiguration.java b/src/main/java/org/apache/commons/configuration/MapConfiguration.java
new file mode 100644
index 0000000..f6d8b4d
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/MapConfiguration.java
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * <p>
+ * A Map based Configuration.
+ * </p>
+ * <p>
+ * This implementation of the {@code Configuration} interface is
+ * initialized with a {@code java.util.Map}. The methods of the
+ * {@code Configuration} interface are implemented on top of the content of
+ * this map. The following storage scheme is used:
+ * </p>
+ * <p>
+ * Property keys are directly mapped to map keys, i.e. the
+ * {@code getProperty()} method directly performs a {@code get()} on
+ * the map. Analogously, {@code setProperty()} or
+ * {@code addProperty()} operations write new data into the map. If a value
+ * is added to an existing property, a {@code java.util.List} is created,
+ * which stores the values of this property.
+ * </p>
+ * <p>
+ * An important use case of this class is to treat a map as a
+ * {@code Configuration} allowing access to its data through the richer
+ * interface. This can be a bit problematic in some cases because the map may
+ * contain values that need not adhere to the default storage scheme used by
+ * typical configuration implementations, e.g. regarding lists. In such cases
+ * care must be taken when manipulating the data through the
+ * {@code Configuration} interface, e.g. by calling
+ * {@code addProperty()}; results may be different than expected.
+ * </p>
+ * <p>
+ * An important point is the handling of list delimiters: If delimiter parsing
+ * is enabled (which it is per default), {@code getProperty()} checks
+ * whether the value of a property is a string and whether it contains the list
+ * delimiter character. If this is the case, the value is split at the delimiter
+ * resulting in a list. This split operation typically also involves trimming
+ * the single values as the list delimiter character may be surrounded by
+ * whitespace. Trimming can be disabled with the
+ * {@link #setTrimmingDisabled(boolean)} method. The whole list splitting
+ * behavior can be disabled using the
+ * {@link #setDelimiterParsingDisabled(boolean)} method.
+ * </p>
+ * <p>
+ * Notice that list splitting is only performed for single string values. If a
+ * property has multiple values, the single values are not split even if they
+ * contain the list delimiter character.
+ * </p>
+ * <p>
+ * As the underlying {@code Map} is directly used as store of the property
+ * values, the thread-safety of this {@code Configuration} implementation
+ * depends on the map passed to the constructor.
+ * </p>
+ * <p>
+ * Notes about type safety: For properties with multiple values this implementation
+ * creates lists of type {@code Object} and stores them. If a property is assigned
+ * another value, the value is added to the list. This can cause problems if the
+ * map passed to the constructor already contains lists of other types. This
+ * should be avoided, otherwise it cannot be guaranteed that the application
+ * might throw {@code ClassCastException} exceptions later.
+ * </p>
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: MapConfiguration.java 1534429 2013-10-22 00:45:36Z henning $
+ * @since 1.1
+ */
+public class MapConfiguration extends AbstractConfiguration implements Cloneable
+{
+ /** The Map decorated by this configuration. */
+ protected Map<String, Object> map;
+
+ /** A flag whether trimming of property values should be disabled.*/
+ private boolean trimmingDisabled;
+
+ /**
+ * Create a Configuration decorator around the specified Map. The map is
+ * used to store the configuration properties, any change will also affect
+ * the Map.
+ *
+ * @param map the map
+ */
+ public MapConfiguration(Map<String, ?> map)
+ {
+ this.map = (Map<String, Object>) map;
+ }
+
+ /**
+ * Creates a new instance of {@code MapConfiguration} and initializes its
+ * content from the specified {@code Properties} object. The resulting
+ * configuration is not connected to the {@code Properties} object, but all
+ * keys which are strings are copied (keys of other types are ignored).
+ *
+ * @param props the {@code Properties} object defining the content of this
+ * configuration
+ * @throws NullPointerException if the {@code Properties} object is
+ * <b>null</b>
+ * @since 1.8
+ */
+ public MapConfiguration(Properties props)
+ {
+ map = convertPropertiesToMap(props);
+ }
+
+ /**
+ * Return the Map decorated by this configuration.
+ *
+ * @return the map this configuration is based onto
+ */
+ public Map<String, Object> getMap()
+ {
+ return map;
+ }
+
+ /**
+ * Returns the flag whether trimming of property values is disabled.
+ *
+ * @return <b>true</b> if trimming of property values is disabled;
+ * <b>false</b> otherwise
+ * @since 1.7
+ */
+ public boolean isTrimmingDisabled()
+ {
+ return trimmingDisabled;
+ }
+
+ /**
+ * Sets a flag whether trimming of property values is disabled. This flag is
+ * only evaluated if list splitting is enabled. Refer to the header comment
+ * for more information about list splitting and trimming.
+ *
+ * @param trimmingDisabled a flag whether trimming of property values should
+ * be disabled
+ * @since 1.7
+ */
+ public void setTrimmingDisabled(boolean trimmingDisabled)
+ {
+ this.trimmingDisabled = trimmingDisabled;
+ }
+
+ public Object getProperty(String key)
+ {
+ Object value = map.get(key);
+ if ((value instanceof String) && (!isDelimiterParsingDisabled()))
+ {
+ List<String> list = PropertyConverter.split((String) value, getListDelimiter(), !isTrimmingDisabled());
+ return list.size() > 1 ? list : list.get(0);
+ }
+ else
+ {
+ return value;
+ }
+ }
+
+ @Override
+ protected void addPropertyDirect(String key, Object value)
+ {
+ Object previousValue = getProperty(key);
+
+ if (previousValue == null)
+ {
+ map.put(key, value);
+ }
+ else if (previousValue instanceof List)
+ {
+ // the value is added to the existing list
+ // Note: This is problematic. See header comment!
+ ((List<Object>) previousValue).add(value);
+ }
+ else
+ {
+ // the previous value is replaced by a list containing the previous value and the new value
+ List<Object> list = new ArrayList<Object>();
+ list.add(previousValue);
+ list.add(value);
+
+ map.put(key, list);
+ }
+ }
+
+ public boolean isEmpty()
+ {
+ return map.isEmpty();
+ }
+
+ public boolean containsKey(String key)
+ {
+ return map.containsKey(key);
+ }
+
+ @Override
+ protected void clearPropertyDirect(String key)
+ {
+ map.remove(key);
+ }
+
+ public Iterator<String> getKeys()
+ {
+ return map.keySet().iterator();
+ }
+
+ /**
+ * Returns a copy of this object. The returned configuration will contain
+ * the same properties as the original. Event listeners are not cloned.
+ *
+ * @return the copy
+ * @since 1.3
+ */
+ @Override
+ public Object clone()
+ {
+ try
+ {
+ MapConfiguration copy = (MapConfiguration) super.clone();
+ copy.clearConfigurationListeners();
+ // Safe because ConfigurationUtils returns a map of the same types.
+ @SuppressWarnings("unchecked")
+ Map<String, Object> clonedMap = (Map<String, Object>) ConfigurationUtils.clone(map);
+ copy.map = clonedMap;
+ return copy;
+ }
+ catch (CloneNotSupportedException cex)
+ {
+ // cannot happen
+ throw new ConfigurationRuntimeException(cex);
+ }
+ }
+
+ /**
+ * Helper method for copying all string keys from the given
+ * {@code Properties} object to a newly created map.
+ *
+ * @param props the {@code Properties} to be copied
+ * @return a newly created map with all string keys of the properties
+ */
+ private static Map<String, Object> convertPropertiesToMap(final Properties props)
+ {
+ return new AbstractMap<String, Object>() {
+
+ @Override
+ public Set<Map.Entry<String, Object>> entrySet()
+ {
+ Set<Map.Entry<String, Object>> entries = new HashSet<Map.Entry<String, Object>>();
+ for (final Map.Entry<Object, Object> propertyEntry : props.entrySet())
+ {
+ if (propertyEntry.getKey() instanceof String)
+ {
+ entries.add(new Map.Entry<String, Object>() {
+
+ public String getKey()
+ {
+ return propertyEntry.getKey().toString();
+ }
+
+ public Object getValue()
+ {
+ return propertyEntry.getValue();
+ }
+
+ public Object setValue(Object value)
+ {
+ throw new UnsupportedOperationException();
+ }
+ });
+ }
+ }
+ return entries;
+ }
+ };
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/MultiFileHierarchicalConfiguration.java b/src/main/java/org/apache/commons/configuration/MultiFileHierarchicalConfiguration.java
new file mode 100644
index 0000000..8a90255
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/MultiFileHierarchicalConfiguration.java
@@ -0,0 +1,871 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.commons.beanutils.BeanUtils;
+import org.apache.commons.configuration.event.ConfigurationErrorEvent;
+import org.apache.commons.configuration.event.ConfigurationErrorListener;
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.configuration.interpol.ConfigurationInterpolator;
+import org.apache.commons.configuration.reloading.ReloadingStrategy;
+import org.apache.commons.configuration.resolver.EntityResolverSupport;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.ExpressionEngine;
+import org.apache.commons.lang.text.StrSubstitutor;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.SAXParseException;
+
+/**
+ * This class provides access to multiple configuration files that reside in a location that
+ * can be specified by a pattern allowing applications to be multi-tenant. For example,
+ * providing a pattern of "file:///opt/config/${product}/${client}/config.xml" will result in
+ * "product" and "client" being resolved on every call. The configuration resulting from the
+ * resolved pattern will be saved for future access.
+ * @since 1.6
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: MultiFileHierarchicalConfiguration.java 1534064 2013-10-21 08:44:33Z henning $
+ */
+public class MultiFileHierarchicalConfiguration extends AbstractHierarchicalFileConfiguration
+ implements ConfigurationListener, ConfigurationErrorListener, EntityResolverSupport
+{
+ /**
+ * Prevent recursion while resolving unprefixed properties.
+ */
+ private static ThreadLocal<Boolean> recursive = new ThreadLocal<Boolean>()
+ {
+ @Override
+ protected synchronized Boolean initialValue()
+ {
+ return Boolean.FALSE;
+ }
+ };
+
+ /** Map of configurations */
+ private final ConcurrentMap<String, XMLConfiguration> configurationsMap =
+ new ConcurrentHashMap<String, XMLConfiguration>();
+
+ /** key pattern for configurationsMap */
+ private String pattern;
+
+ /** True if the constructor has finished */
+ private boolean init;
+
+ /** Return an empty configuration if loading fails */
+ private boolean ignoreException = true;
+
+ /** Capture the schema validation setting */
+ private boolean schemaValidation;
+
+ /** Stores a flag whether DTD or Schema validation should be performed.*/
+ private boolean validating;
+
+ /** A flag whether attribute splitting is disabled.*/
+ private boolean attributeSplittingDisabled;
+
+ /** The Logger name to use */
+ private String loggerName = MultiFileHierarchicalConfiguration.class.getName();
+
+ /** The Reloading strategy to use on created configurations */
+ private ReloadingStrategy fileStrategy;
+
+ /** The EntityResolver */
+ private EntityResolver entityResolver;
+
+ /** The internally used helper object for variable substitution. */
+ private StrSubstitutor localSubst = new StrSubstitutor(new ConfigurationInterpolator());
+
+ /**
+ * Default Constructor.
+ */
+ public MultiFileHierarchicalConfiguration()
+ {
+ super();
+ this.init = true;
+ setLogger(LogFactory.getLog(loggerName));
+ }
+
+ /**
+ * Construct the configuration with the specified pattern.
+ * @param pathPattern The pattern to use to locate configuration files.
+ */
+ public MultiFileHierarchicalConfiguration(String pathPattern)
+ {
+ super();
+ this.pattern = pathPattern;
+ this.init = true;
+ setLogger(LogFactory.getLog(loggerName));
+ }
+
+ public void setLoggerName(String name)
+ {
+ this.loggerName = name;
+ }
+
+ /**
+ * Set the File pattern
+ * @param pathPattern The pattern for the path to the configuration.
+ */
+ public void setFilePattern(String pathPattern)
+ {
+ this.pattern = pathPattern;
+ }
+
+ public boolean isSchemaValidation()
+ {
+ return schemaValidation;
+ }
+
+ public void setSchemaValidation(boolean schemaValidation)
+ {
+ this.schemaValidation = schemaValidation;
+ }
+
+ public boolean isValidating()
+ {
+ return validating;
+ }
+
+ public void setValidating(boolean validating)
+ {
+ this.validating = validating;
+ }
+
+ public boolean isAttributeSplittingDisabled()
+ {
+ return attributeSplittingDisabled;
+ }
+
+ public void setAttributeSplittingDisabled(boolean attributeSplittingDisabled)
+ {
+ this.attributeSplittingDisabled = attributeSplittingDisabled;
+ }
+
+ @Override
+ public ReloadingStrategy getReloadingStrategy()
+ {
+ return fileStrategy;
+ }
+
+ @Override
+ public void setReloadingStrategy(ReloadingStrategy strategy)
+ {
+ this.fileStrategy = strategy;
+ }
+
+ public void setEntityResolver(EntityResolver entityResolver)
+ {
+ this.entityResolver = entityResolver;
+ }
+
+ public EntityResolver getEntityResolver()
+ {
+ return this.entityResolver;
+ }
+
+ /**
+ * Set to true if an empty Configuration should be returned when loading fails. If
+ * false an exception will be thrown.
+ * @param ignoreException The ignore value.
+ */
+ public void setIgnoreException(boolean ignoreException)
+ {
+ this.ignoreException = ignoreException;
+ }
+
+ @Override
+ public void addProperty(String key, Object value)
+ {
+ this.getConfiguration().addProperty(key, value);
+ }
+
+ @Override
+ public void clear()
+ {
+ this.getConfiguration().clear();
+ }
+
+ @Override
+ public void clearProperty(String key)
+ {
+ this.getConfiguration().clearProperty(key);
+ }
+
+ @Override
+ public boolean containsKey(String key)
+ {
+ return this.getConfiguration().containsKey(key);
+ }
+
+ @Override
+ public BigDecimal getBigDecimal(String key, BigDecimal defaultValue)
+ {
+ return this.getConfiguration().getBigDecimal(key, defaultValue);
+ }
+
+ @Override
+ public BigDecimal getBigDecimal(String key)
+ {
+ return this.getConfiguration().getBigDecimal(key);
+ }
+
+ @Override
+ public BigInteger getBigInteger(String key, BigInteger defaultValue)
+ {
+ return this.getConfiguration().getBigInteger(key, defaultValue);
+ }
+
+ @Override
+ public BigInteger getBigInteger(String key)
+ {
+ return this.getConfiguration().getBigInteger(key);
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean defaultValue)
+ {
+ return this.getConfiguration().getBoolean(key, defaultValue);
+ }
+
+ @Override
+ public Boolean getBoolean(String key, Boolean defaultValue)
+ {
+ return this.getConfiguration().getBoolean(key, defaultValue);
+ }
+
+ @Override
+ public boolean getBoolean(String key)
+ {
+ return this.getConfiguration().getBoolean(key);
+ }
+
+ @Override
+ public byte getByte(String key, byte defaultValue)
+ {
+ return this.getConfiguration().getByte(key, defaultValue);
+ }
+
+ @Override
+ public Byte getByte(String key, Byte defaultValue)
+ {
+ return this.getConfiguration().getByte(key, defaultValue);
+ }
+
+ @Override
+ public byte getByte(String key)
+ {
+ return this.getConfiguration().getByte(key);
+ }
+
+ @Override
+ public double getDouble(String key, double defaultValue)
+ {
+ return this.getConfiguration().getDouble(key, defaultValue);
+ }
+
+ @Override
+ public Double getDouble(String key, Double defaultValue)
+ {
+ return this.getConfiguration().getDouble(key, defaultValue);
+ }
+
+ @Override
+ public double getDouble(String key)
+ {
+ return this.getConfiguration().getDouble(key);
+ }
+
+ @Override
+ public float getFloat(String key, float defaultValue)
+ {
+ return this.getConfiguration().getFloat(key, defaultValue);
+ }
+
+ @Override
+ public Float getFloat(String key, Float defaultValue)
+ {
+ return this.getConfiguration().getFloat(key, defaultValue);
+ }
+
+ @Override
+ public float getFloat(String key)
+ {
+ return this.getConfiguration().getFloat(key);
+ }
+
+ @Override
+ public int getInt(String key, int defaultValue)
+ {
+ return this.getConfiguration().getInt(key, defaultValue);
+ }
+
+ @Override
+ public int getInt(String key)
+ {
+ return this.getConfiguration().getInt(key);
+ }
+
+ @Override
+ public Integer getInteger(String key, Integer defaultValue)
+ {
+ return this.getConfiguration().getInteger(key, defaultValue);
+ }
+
+ @Override
+ public Iterator<String> getKeys()
+ {
+ return this.getConfiguration().getKeys();
+ }
+
+ @Override
+ public Iterator<String> getKeys(String prefix)
+ {
+ return this.getConfiguration().getKeys(prefix);
+ }
+
+ @Override
+ public List<Object> getList(String key, List<?> defaultValue)
+ {
+ return this.getConfiguration().getList(key, defaultValue);
+ }
+
+ @Override
+ public List<Object> getList(String key)
+ {
+ return this.getConfiguration().getList(key);
+ }
+
+ @Override
+ public long getLong(String key, long defaultValue)
+ {
+ return this.getConfiguration().getLong(key, defaultValue);
+ }
+
+ @Override
+ public Long getLong(String key, Long defaultValue)
+ {
+ return this.getConfiguration().getLong(key, defaultValue);
+ }
+
+ @Override
+ public long getLong(String key)
+ {
+ return this.getConfiguration().getLong(key);
+ }
+
+ @Override
+ public Properties getProperties(String key)
+ {
+ return this.getConfiguration().getProperties(key);
+ }
+
+ @Override
+ public Object getProperty(String key)
+ {
+ return this.getConfiguration().getProperty(key);
+ }
+
+ @Override
+ public short getShort(String key, short defaultValue)
+ {
+ return this.getConfiguration().getShort(key, defaultValue);
+ }
+
+ @Override
+ public Short getShort(String key, Short defaultValue)
+ {
+ return this.getConfiguration().getShort(key, defaultValue);
+ }
+
+ @Override
+ public short getShort(String key)
+ {
+ return this.getConfiguration().getShort(key);
+ }
+
+ @Override
+ public String getString(String key, String defaultValue)
+ {
+ return this.getConfiguration().getString(key, defaultValue);
+ }
+
+ @Override
+ public String getString(String key)
+ {
+ return this.getConfiguration().getString(key);
+ }
+
+ @Override
+ public String[] getStringArray(String key)
+ {
+ return this.getConfiguration().getStringArray(key);
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ return this.getConfiguration().isEmpty();
+ }
+
+ @Override
+ public void setProperty(String key, Object value)
+ {
+ if (init)
+ {
+ this.getConfiguration().setProperty(key, value);
+ }
+ }
+
+ @Override
+ public Configuration subset(String prefix)
+ {
+ return this.getConfiguration().subset(prefix);
+ }
+
+ @Override
+ public Object getReloadLock()
+ {
+ return this.getConfiguration().getReloadLock();
+ }
+
+ @Override
+ public Node getRoot()
+ {
+ return this.getConfiguration().getRoot();
+ }
+
+ @Override
+ public void setRoot(Node node)
+ {
+ if (init)
+ {
+ this.getConfiguration().setRoot(node);
+ }
+ else
+ {
+ super.setRoot(node);
+ }
+ }
+
+ @Override
+ public ConfigurationNode getRootNode()
+ {
+ return this.getConfiguration().getRootNode();
+ }
+
+ @Override
+ public void setRootNode(ConfigurationNode rootNode)
+ {
+ if (init)
+ {
+ this.getConfiguration().setRootNode(rootNode);
+ }
+ else
+ {
+ super.setRootNode(rootNode);
+ }
+ }
+
+ @Override
+ public ExpressionEngine getExpressionEngine()
+ {
+ return super.getExpressionEngine();
+ }
+
+ @Override
+ public void setExpressionEngine(ExpressionEngine expressionEngine)
+ {
+ super.setExpressionEngine(expressionEngine);
+ }
+
+ @Override
+ public void addNodes(String key, Collection<? extends ConfigurationNode> nodes)
+ {
+ this.getConfiguration().addNodes(key, nodes);
+ }
+
+ @Override
+ public SubnodeConfiguration configurationAt(String key, boolean supportUpdates)
+ {
+ return this.getConfiguration().configurationAt(key, supportUpdates);
+ }
+
+ @Override
+ public SubnodeConfiguration configurationAt(String key)
+ {
+ return this.getConfiguration().configurationAt(key);
+ }
+
+ @Override
+ public List<HierarchicalConfiguration> configurationsAt(String key)
+ {
+ return this.getConfiguration().configurationsAt(key);
+ }
+
+ @Override
+ public void clearTree(String key)
+ {
+ this.getConfiguration().clearTree(key);
+ }
+
+ @Override
+ public int getMaxIndex(String key)
+ {
+ return this.getConfiguration().getMaxIndex(key);
+ }
+
+ @Override
+ public Configuration interpolatedConfiguration()
+ {
+ return this.getConfiguration().interpolatedConfiguration();
+ }
+
+ @Override
+ public void addConfigurationListener(ConfigurationListener l)
+ {
+ super.addConfigurationListener(l);
+ }
+
+ @Override
+ public boolean removeConfigurationListener(ConfigurationListener l)
+ {
+ return super.removeConfigurationListener(l);
+ }
+
+ @Override
+ public Collection<ConfigurationListener> getConfigurationListeners()
+ {
+ return super.getConfigurationListeners();
+ }
+
+ @Override
+ public void clearConfigurationListeners()
+ {
+ super.clearConfigurationListeners();
+ }
+
+ @Override
+ public void addErrorListener(ConfigurationErrorListener l)
+ {
+ super.addErrorListener(l);
+ }
+
+ @Override
+ public boolean removeErrorListener(ConfigurationErrorListener l)
+ {
+ return super.removeErrorListener(l);
+ }
+
+ @Override
+ public void clearErrorListeners()
+ {
+ super.clearErrorListeners();
+ }
+
+ @Override
+ public Collection<ConfigurationErrorListener> getErrorListeners()
+ {
+ return super.getErrorListeners();
+ }
+
+ public void save(Writer writer) throws ConfigurationException
+ {
+ if (init)
+ {
+ this.getConfiguration().save(writer);
+ }
+ }
+
+ public void load(Reader reader) throws ConfigurationException
+ {
+ if (init)
+ {
+ this.getConfiguration().load(reader);
+ }
+ }
+
+ @Override
+ public void load() throws ConfigurationException
+ {
+ this.getConfiguration();
+ }
+
+ @Override
+ public void load(String fileName) throws ConfigurationException
+ {
+ this.getConfiguration().load(fileName);
+ }
+
+ @Override
+ public void load(File file) throws ConfigurationException
+ {
+ this.getConfiguration().load(file);
+ }
+
+ @Override
+ public void load(URL url) throws ConfigurationException
+ {
+ this.getConfiguration().load(url);
+ }
+
+ @Override
+ public void load(InputStream in) throws ConfigurationException
+ {
+ this.getConfiguration().load(in);
+ }
+
+ @Override
+ public void load(InputStream in, String encoding) throws ConfigurationException
+ {
+ this.getConfiguration().load(in, encoding);
+ }
+
+ @Override
+ public void save() throws ConfigurationException
+ {
+ this.getConfiguration().save();
+ }
+
+ @Override
+ public void save(String fileName) throws ConfigurationException
+ {
+ this.getConfiguration().save(fileName);
+ }
+
+ @Override
+ public void save(File file) throws ConfigurationException
+ {
+ this.getConfiguration().save(file);
+ }
+
+ @Override
+ public void save(URL url) throws ConfigurationException
+ {
+ this.getConfiguration().save(url);
+ }
+
+ @Override
+ public void save(OutputStream out) throws ConfigurationException
+ {
+ this.getConfiguration().save(out);
+ }
+
+ @Override
+ public void save(OutputStream out, String encoding) throws ConfigurationException
+ {
+ this.getConfiguration().save(out, encoding);
+ }
+
+ @Override
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ if (event.getSource() instanceof XMLConfiguration)
+ {
+ for (ConfigurationListener listener : getConfigurationListeners())
+ {
+ listener.configurationChanged(event);
+ }
+ }
+ }
+
+ @Override
+ public void configurationError(ConfigurationErrorEvent event)
+ {
+ if (event.getSource() instanceof XMLConfiguration)
+ {
+ for (ConfigurationErrorListener listener : getErrorListeners())
+ {
+ listener.configurationError(event);
+ }
+ }
+
+ if (event.getType() == AbstractFileConfiguration.EVENT_RELOAD)
+ {
+ if (isThrowable(event.getCause()))
+ {
+ throw new ConfigurationRuntimeException(event.getCause());
+ }
+ }
+ }
+
+ /*
+ * Don't allow resolveContainerStore to be called recursively.
+ * @param key The key to resolve.
+ * @return The value of the key.
+ */
+ @Override
+ protected Object resolveContainerStore(String key)
+ {
+ if (recursive.get().booleanValue())
+ {
+ return null;
+ }
+ recursive.set(Boolean.TRUE);
+ try
+ {
+ return super.resolveContainerStore(key);
+ }
+ finally
+ {
+ recursive.set(Boolean.FALSE);
+ }
+ }
+
+ /**
+ * Remove the current Configuration.
+ */
+ public void removeConfiguration()
+ {
+ String path = getSubstitutor().replace(pattern);
+ configurationsMap.remove(path);
+ }
+
+ /**
+ * First checks to see if the cache exists, if it does, get the associated Configuration.
+ * If not it will load a new Configuration and save it in the cache.
+ *
+ * @return the Configuration associated with the current value of the path pattern.
+ */
+ private AbstractHierarchicalFileConfiguration getConfiguration()
+ {
+ if (pattern == null)
+ {
+ throw new ConfigurationRuntimeException("File pattern must be defined");
+ }
+ String path = localSubst.replace(pattern);
+
+ if (configurationsMap.containsKey(path))
+ {
+ return configurationsMap.get(path);
+ }
+
+ if (path.equals(pattern))
+ {
+ XMLConfiguration configuration = new XMLConfiguration()
+ {
+ @Override
+ public void load() throws ConfigurationException
+ {
+ }
+ @Override
+ public void save() throws ConfigurationException
+ {
+ }
+ };
+
+ configurationsMap.putIfAbsent(pattern, configuration);
+
+ return configuration;
+ }
+
+ XMLConfiguration configuration = new XMLConfiguration();
+ if (loggerName != null)
+ {
+ Log log = LogFactory.getLog(loggerName);
+ if (log != null)
+ {
+ configuration.setLogger(log);
+ }
+ }
+ configuration.setBasePath(getBasePath());
+ configuration.setFileName(path);
+ configuration.setFileSystem(getFileSystem());
+ configuration.setExpressionEngine(getExpressionEngine());
+ ReloadingStrategy strategy = createReloadingStrategy();
+ if (strategy != null)
+ {
+ configuration.setReloadingStrategy(strategy);
+ }
+ configuration.setDelimiterParsingDisabled(isDelimiterParsingDisabled());
+ configuration.setAttributeSplittingDisabled(isAttributeSplittingDisabled());
+ configuration.setValidating(validating);
+ configuration.setSchemaValidation(schemaValidation);
+ configuration.setEntityResolver(entityResolver);
+ configuration.setListDelimiter(getListDelimiter());
+ configuration.addConfigurationListener(this);
+ configuration.addErrorListener(this);
+ try
+ {
+ configuration.load();
+ }
+ catch (ConfigurationException ce)
+ {
+ if (isThrowable(ce))
+ {
+ throw new ConfigurationRuntimeException(ce);
+ }
+ }
+ configurationsMap.putIfAbsent(path, configuration);
+ return configurationsMap.get(path);
+ }
+
+ private boolean isThrowable(Throwable throwable)
+ {
+ if (!ignoreException)
+ {
+ return true;
+ }
+ Throwable cause = throwable.getCause();
+ while (cause != null && !(cause instanceof SAXParseException))
+ {
+ cause = cause.getCause();
+ }
+ return cause != null;
+ }
+
+ /**
+ * Clone the FileReloadingStrategy since each file needs its own.
+ * @return A new FileReloadingStrategy.
+ */
+ private ReloadingStrategy createReloadingStrategy()
+ {
+ if (fileStrategy == null)
+ {
+ return null;
+ }
+ try
+ {
+ ReloadingStrategy strategy = (ReloadingStrategy) BeanUtils.cloneBean(fileStrategy);
+ strategy.setConfiguration(null);
+ return strategy;
+ }
+ catch (Exception ex)
+ {
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/apache/commons/configuration/PatternSubtreeConfigurationWrapper.java b/src/main/java/org/apache/commons/configuration/PatternSubtreeConfigurationWrapper.java
new file mode 100644
index 0000000..5f77f8e
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/PatternSubtreeConfigurationWrapper.java
@@ -0,0 +1,541 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.io.Reader;
+import java.io.Writer;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Properties;
+
+import org.apache.commons.configuration.event.ConfigurationErrorListener;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.ExpressionEngine;
+
+/**
+ * Wraps a HierarchicalConfiguration and allows subtrees to be access via a configured path with
+ * replaceable tokens derived from the ConfigurationInterpolator. When used with injection frameworks
+ * such as Spring it allows components to be injected with subtrees of the configuration.
+ * @since 1.6
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: PatternSubtreeConfigurationWrapper.java 1534064 2013-10-21 08:44:33Z henning $
+ */
+public class PatternSubtreeConfigurationWrapper extends AbstractHierarchicalFileConfiguration
+{
+ /**
+ * Prevent recursion while resolving unprefixed properties.
+ */
+ private static ThreadLocal<Boolean> recursive = new ThreadLocal<Boolean>()
+ {
+ @Override
+ protected synchronized Boolean initialValue()
+ {
+ return Boolean.FALSE;
+ }
+ };
+
+ /** The wrapped configuration */
+ private final AbstractHierarchicalFileConfiguration config;
+
+ /** The path to the subtree */
+ private final String path;
+
+ /** True if the path ends with '/', false otherwise */
+ private final boolean trailing;
+
+ /** True if the constructor has finished */
+ private boolean init;
+
+ /**
+ * Constructor
+ * @param config The Configuration to be wrapped.
+ * @param path The base path pattern.
+ */
+ public PatternSubtreeConfigurationWrapper(AbstractHierarchicalFileConfiguration config, String path)
+ {
+ this.config = config;
+ this.path = path;
+ this.trailing = path.endsWith("/");
+ this.init = true;
+ }
+
+ @Override
+ public Object getReloadLock()
+ {
+ return config.getReloadLock();
+ }
+
+ @Override
+ public void addProperty(String key, Object value)
+ {
+ config.addProperty(makePath(key), value);
+ }
+
+ @Override
+ public void clear()
+ {
+ getConfig().clear();
+ }
+
+ @Override
+ public void clearProperty(String key)
+ {
+ config.clearProperty(makePath(key));
+ }
+
+ @Override
+ public boolean containsKey(String key)
+ {
+ return config.containsKey(makePath(key));
+ }
+
+ @Override
+ public BigDecimal getBigDecimal(String key, BigDecimal defaultValue)
+ {
+ return config.getBigDecimal(makePath(key), defaultValue);
+ }
+
+ @Override
+ public BigDecimal getBigDecimal(String key)
+ {
+ return config.getBigDecimal(makePath(key));
+ }
+
+ @Override
+ public BigInteger getBigInteger(String key, BigInteger defaultValue)
+ {
+ return config.getBigInteger(makePath(key), defaultValue);
+ }
+
+ @Override
+ public BigInteger getBigInteger(String key)
+ {
+ return config.getBigInteger(makePath(key));
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean defaultValue)
+ {
+ return config.getBoolean(makePath(key), defaultValue);
+ }
+
+ @Override
+ public Boolean getBoolean(String key, Boolean defaultValue)
+ {
+ return config.getBoolean(makePath(key), defaultValue);
+ }
+
+ @Override
+ public boolean getBoolean(String key)
+ {
+ return config.getBoolean(makePath(key));
+ }
+
+ @Override
+ public byte getByte(String key, byte defaultValue)
+ {
+ return config.getByte(makePath(key), defaultValue);
+ }
+
+ @Override
+ public Byte getByte(String key, Byte defaultValue)
+ {
+ return config.getByte(makePath(key), defaultValue);
+ }
+
+ @Override
+ public byte getByte(String key)
+ {
+ return config.getByte(makePath(key));
+ }
+
+ @Override
+ public double getDouble(String key, double defaultValue)
+ {
+ return config.getDouble(makePath(key), defaultValue);
+ }
+
+ @Override
+ public Double getDouble(String key, Double defaultValue)
+ {
+ return config.getDouble(makePath(key), defaultValue);
+ }
+
+ @Override
+ public double getDouble(String key)
+ {
+ return config.getDouble(makePath(key));
+ }
+
+ @Override
+ public float getFloat(String key, float defaultValue)
+ {
+ return config.getFloat(makePath(key), defaultValue);
+ }
+
+ @Override
+ public Float getFloat(String key, Float defaultValue)
+ {
+ return config.getFloat(makePath(key), defaultValue);
+ }
+
+ @Override
+ public float getFloat(String key)
+ {
+ return config.getFloat(makePath(key));
+ }
+
+ @Override
+ public int getInt(String key, int defaultValue)
+ {
+ return config.getInt(makePath(key), defaultValue);
+ }
+
+ @Override
+ public int getInt(String key)
+ {
+ return config.getInt(makePath(key));
+ }
+
+ @Override
+ public Integer getInteger(String key, Integer defaultValue)
+ {
+ return config.getInteger(makePath(key), defaultValue);
+ }
+
+ @Override
+ public Iterator<String> getKeys()
+ {
+ return config.getKeys(makePath());
+ }
+
+ @Override
+ public Iterator<String> getKeys(String prefix)
+ {
+ return config.getKeys(makePath(prefix));
+ }
+
+ @Override
+ public List<Object> getList(String key, List<?> defaultValue)
+ {
+ return config.getList(makePath(key), defaultValue);
+ }
+
+ @Override
+ public List<Object> getList(String key)
+ {
+ return config.getList(makePath(key));
+ }
+
+ @Override
+ public long getLong(String key, long defaultValue)
+ {
+ return config.getLong(makePath(key), defaultValue);
+ }
+
+ @Override
+ public Long getLong(String key, Long defaultValue)
+ {
+ return config.getLong(makePath(key), defaultValue);
+ }
+
+ @Override
+ public long getLong(String key)
+ {
+ return config.getLong(makePath(key));
+ }
+
+ @Override
+ public Properties getProperties(String key)
+ {
+ return config.getProperties(makePath(key));
+ }
+
+ @Override
+ public Object getProperty(String key)
+ {
+ return config.getProperty(makePath(key));
+ }
+
+ @Override
+ public short getShort(String key, short defaultValue)
+ {
+ return config.getShort(makePath(key), defaultValue);
+ }
+
+ @Override
+ public Short getShort(String key, Short defaultValue)
+ {
+ return config.getShort(makePath(key), defaultValue);
+ }
+
+ @Override
+ public short getShort(String key)
+ {
+ return config.getShort(makePath(key));
+ }
+
+ @Override
+ public String getString(String key, String defaultValue)
+ {
+ return config.getString(makePath(key), defaultValue);
+ }
+
+ @Override
+ public String getString(String key)
+ {
+ return config.getString(makePath(key));
+ }
+
+ @Override
+ public String[] getStringArray(String key)
+ {
+ return config.getStringArray(makePath(key));
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ return getConfig().isEmpty();
+ }
+
+ @Override
+ public void setProperty(String key, Object value)
+ {
+ getConfig().setProperty(key, value);
+ }
+
+ @Override
+ public Configuration subset(String prefix)
+ {
+ return getConfig().subset(prefix);
+ }
+
+ @Override
+ public Node getRoot()
+ {
+ return getConfig().getRoot();
+ }
+
+ @Override
+ public void setRoot(Node node)
+ {
+ if (init)
+ {
+ getConfig().setRoot(node);
+ }
+ else
+ {
+ super.setRoot(node);
+ }
+ }
+
+ @Override
+ public ConfigurationNode getRootNode()
+ {
+ return getConfig().getRootNode();
+ }
+
+ @Override
+ public void setRootNode(ConfigurationNode rootNode)
+ {
+ if (init)
+ {
+ getConfig().setRootNode(rootNode);
+ }
+ else
+ {
+ super.setRootNode(rootNode);
+ }
+ }
+
+ @Override
+ public ExpressionEngine getExpressionEngine()
+ {
+ return config.getExpressionEngine();
+ }
+
+ @Override
+ public void setExpressionEngine(ExpressionEngine expressionEngine)
+ {
+ if (init)
+ {
+ config.setExpressionEngine(expressionEngine);
+ }
+ else
+ {
+ super.setExpressionEngine(expressionEngine);
+ }
+ }
+
+ @Override
+ public void addNodes(String key, Collection<? extends ConfigurationNode> nodes)
+ {
+ getConfig().addNodes(key, nodes);
+ }
+
+ @Override
+ public SubnodeConfiguration configurationAt(String key, boolean supportUpdates)
+ {
+ return config.configurationAt(makePath(key), supportUpdates);
+ }
+
+ @Override
+ public SubnodeConfiguration configurationAt(String key)
+ {
+ return config.configurationAt(makePath(key));
+ }
+
+ @Override
+ public List<HierarchicalConfiguration> configurationsAt(String key)
+ {
+ return config.configurationsAt(makePath(key));
+ }
+
+ @Override
+ public void clearTree(String key)
+ {
+ config.clearTree(makePath(key));
+ }
+
+ @Override
+ public int getMaxIndex(String key)
+ {
+ return config.getMaxIndex(makePath(key));
+ }
+
+ @Override
+ public Configuration interpolatedConfiguration()
+ {
+ return getConfig().interpolatedConfiguration();
+ }
+
+ @Override
+ public void addConfigurationListener(ConfigurationListener l)
+ {
+ getConfig().addConfigurationListener(l);
+ }
+
+ @Override
+ public boolean removeConfigurationListener(ConfigurationListener l)
+ {
+ return getConfig().removeConfigurationListener(l);
+ }
+
+ @Override
+ public Collection<ConfigurationListener> getConfigurationListeners()
+ {
+ return getConfig().getConfigurationListeners();
+ }
+
+ @Override
+ public void clearConfigurationListeners()
+ {
+ getConfig().clearConfigurationListeners();
+ }
+
+ @Override
+ public void addErrorListener(ConfigurationErrorListener l)
+ {
+ getConfig().addErrorListener(l);
+ }
+
+ @Override
+ public boolean removeErrorListener(ConfigurationErrorListener l)
+ {
+ return getConfig().removeErrorListener(l);
+ }
+
+ @Override
+ public void clearErrorListeners()
+ {
+ getConfig().clearErrorListeners();
+ }
+
+ public void save(Writer writer) throws ConfigurationException
+ {
+ config.save(writer);
+ }
+
+ public void load(Reader reader) throws ConfigurationException
+ {
+ config.load(reader);
+ }
+
+ @Override
+ public Collection<ConfigurationErrorListener> getErrorListeners()
+ {
+ return getConfig().getErrorListeners();
+ }
+
+ @Override
+ protected Object resolveContainerStore(String key)
+ {
+ if (recursive.get().booleanValue())
+ {
+ return null;
+ }
+ recursive.set(Boolean.TRUE);
+ try
+ {
+ return super.resolveContainerStore(key);
+ }
+ finally
+ {
+ recursive.set(Boolean.FALSE);
+ }
+ }
+
+ private HierarchicalConfiguration getConfig()
+ {
+ return config.configurationAt(makePath());
+ }
+
+ private String makePath()
+ {
+ String pathPattern = trailing ? path.substring(0, path.length() - 1) : path;
+ return getSubstitutor().replace(pathPattern);
+ }
+
+ /*
+ * Resolve the root expression and then add the item being retrieved. Insert a
+ * separator character as required.
+ */
+ private String makePath(String item)
+ {
+ String pathPattern;
+ if ((item.length() == 0 || item.startsWith("/")) && trailing)
+ {
+ pathPattern = path.substring(0, path.length() - 1);
+ }
+ else if (!item.startsWith("/") || !trailing)
+ {
+ pathPattern = path + "/";
+ }
+ else
+ {
+ pathPattern = path;
+ }
+ return getSubstitutor().replace(pathPattern) + item;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/PrefixedKeysIterator.java b/src/main/java/org/apache/commons/configuration/PrefixedKeysIterator.java
new file mode 100644
index 0000000..5d63949
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/PrefixedKeysIterator.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * * A specialized iterator implementation used by {@link AbstractConfiguration}
+ * to return an iteration over all keys starting with a specified prefix.
+ *
+ * <p>This class is basically a stripped-down version of the
+ * {@code FilterIterator} class of Commons Collections</p>
+ *
+ * @author <a href="http://commons.apache.org/configuration/team-list.html">Commons Configuration team</a>
+ * @version $Id: PrefixedKeysIterator.java 1205603 2011-11-23 21:17:38Z oheger $
+ */
+class PrefixedKeysIterator implements Iterator<String>
+{
+ /** Stores the wrapped iterator. */
+ private final Iterator<String> iterator;
+
+ /** Stores the prefix. */
+ private final String prefix;
+
+ /** Stores the next element in the iteration. */
+ private String nextElement;
+
+ /** A flag whether the next element has been calculated. */
+ private boolean nextElementSet;
+
+ /**
+ * Creates a new instance of {@code PrefixedKeysIterator} and sets
+ * the wrapped iterator and the prefix for the accepted keys.
+ *
+ * @param wrappedIterator the wrapped iterator
+ * @param keyPrefix the prefix of the allowed keys
+ */
+ public PrefixedKeysIterator(Iterator<String> wrappedIterator, String keyPrefix)
+ {
+ iterator = wrappedIterator;
+ prefix = keyPrefix;
+ }
+
+ /**
+ * Returns a flag whether there are more elements in the iteration.
+ *
+ * @return a flag if there is a next element
+ */
+ public boolean hasNext()
+ {
+ return nextElementSet || setNextElement();
+ }
+
+ /**
+ * Returns the next element in the iteration. This is the next key that
+ * matches the specified prefix.
+ *
+ * @return the next element in the iteration
+ * @throws NoSuchElementException if there is no next element
+ */
+ public String next()
+ {
+ if (!nextElementSet && !setNextElement())
+ {
+ throw new NoSuchElementException();
+ }
+ nextElementSet = false;
+ return nextElement;
+ }
+
+ /**
+ * Removes from the underlying collection of the base iterator the last
+ * element returned by this iterator. This method can only be called if
+ * {@code next()} was called, but not after {@code hasNext()},
+ * because the {@code hasNext()} call changes the base iterator.
+ *
+ * @throws IllegalStateException if {@code hasNext()} has already
+ * been called.
+ */
+ public void remove()
+ {
+ if (nextElementSet)
+ {
+ throw new IllegalStateException("remove() cannot be called");
+ }
+ iterator.remove();
+ }
+
+ /**
+ * Determines the next element in the iteration. The return value indicates
+ * whether such an element can be found.
+ *
+ * @return a flag whether a next element exists
+ */
+ private boolean setNextElement()
+ {
+ while (iterator.hasNext())
+ {
+ String key = iterator.next();
+ if (key.startsWith(prefix + ".") || key.equals(prefix))
+ {
+ nextElement = key;
+ nextElementSet = true;
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/PropertiesConfiguration.java b/src/main/java/org/apache/commons/configuration/PropertiesConfiguration.java
new file mode 100644
index 0000000..13cacfb
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/PropertiesConfiguration.java
@@ -0,0 +1,1516 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.io.FilterWriter;
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * This is the "classic" Properties loader which loads the values from
+ * a single or multiple files (which can be chained with "include =".
+ * All given path references are either absolute or relative to the
+ * file name supplied in the constructor.
+ * <p>
+ * In this class, empty PropertyConfigurations can be built, properties
+ * added and later saved. include statements are (obviously) not supported
+ * if you don't construct a PropertyConfiguration from a file.
+ *
+ * <p>The properties file syntax is explained here, basically it follows
+ * the syntax of the stream parsed by {@link java.util.Properties#load} and
+ * adds several useful extensions:
+ *
+ * <ul>
+ * <li>
+ * Each property has the syntax <code>key <separator> value</code>. The
+ * separators accepted are {@code '='}, {@code ':'} and any white
+ * space character. Examples:
+ * <pre>
+ * key1 = value1
+ * key2 : value2
+ * key3 value3</pre>
+ * </li>
+ * <li>
+ * The <i>key</i> may use any character, separators must be escaped:
+ * <pre>
+ * key\:foo = bar</pre>
+ * </li>
+ * <li>
+ * <i>value</i> may be separated on different lines if a backslash
+ * is placed at the end of the line that continues below.
+ * </li>
+ * <li>
+ * <i>value</i> can contain <em>value delimiters</em> and will then be interpreted
+ * as a list of tokens. Default value delimiter is the comma ','. So the
+ * following property definition
+ * <pre>
+ * key = This property, has multiple, values
+ * </pre>
+ * will result in a property with three values. You can change the value
+ * delimiter using the {@link AbstractConfiguration#setListDelimiter(char)}
+ * method. Setting the delimiter to 0 will disable value splitting completely.
+ * </li>
+ * <li>
+ * Commas in each token are escaped placing a backslash right before
+ * the comma.
+ * </li>
+ * <li>
+ * If a <i>key</i> is used more than once, the values are appended
+ * like if they were on the same line separated with commas. <em>Note</em>:
+ * When the configuration file is written back to disk the associated
+ * {@link PropertiesConfigurationLayout} object (see below) will
+ * try to preserve as much of the original format as possible, i.e. properties
+ * with multiple values defined on a single line will also be written back on
+ * a single line, and multiple occurrences of a single key will be written on
+ * multiple lines. If the {@code addProperty()} method was called
+ * multiple times for adding multiple values to a property, these properties
+ * will per default be written on multiple lines in the output file, too.
+ * Some options of the {@code PropertiesConfigurationLayout} class have
+ * influence on that behavior.
+ * </li>
+ * <li>
+ * Blank lines and lines starting with character '#' or '!' are skipped.
+ * </li>
+ * <li>
+ * If a property is named "include" (or whatever is defined by
+ * setInclude() and getInclude() and the value of that property is
+ * the full path to a file on disk, that file will be included into
+ * the configuration. You can also pull in files relative to the parent
+ * configuration file. So if you have something like the following:
+ *
+ * include = additional.properties
+ *
+ * Then "additional.properties" is expected to be in the same
+ * directory as the parent configuration file.
+ *
+ * The properties in the included file are added to the parent configuration,
+ * they do not replace existing properties with the same key.
+ *
+ * </li>
+ * </ul>
+ *
+ * <p>Here is an example of a valid extended properties file:
+ *
+ * <p><pre>
+ * # lines starting with # are comments
+ *
+ * # This is the simplest property
+ * key = value
+ *
+ * # A long property may be separated on multiple lines
+ * longvalue = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
+ * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ *
+ * # This is a property with many tokens
+ * tokens_on_a_line = first token, second token
+ *
+ * # This sequence generates exactly the same result
+ * tokens_on_multiple_lines = first token
+ * tokens_on_multiple_lines = second token
+ *
+ * # commas may be escaped in tokens
+ * commas.escaped = Hi\, what'up?
+ *
+ * # properties can reference other properties
+ * base.prop = /base
+ * first.prop = ${base.prop}/first
+ * second.prop = ${first.prop}/second
+ * </pre>
+ *
+ * <p>A {@code PropertiesConfiguration} object is associated with an
+ * instance of the {@link PropertiesConfigurationLayout} class,
+ * which is responsible for storing the layout of the parsed properties file
+ * (i.e. empty lines, comments, and such things). The {@code getLayout()}
+ * method can be used to obtain this layout object. With {@code setLayout()}
+ * a new layout object can be set. This should be done before a properties file
+ * was loaded.
+ * <p><em>Note:</em>Configuration objects of this type can be read concurrently
+ * by multiple threads. However if one of these threads modifies the object,
+ * synchronization has to be performed manually.
+ *
+ * @see java.util.Properties#load
+ *
+ * @author <a href="mailto:stefano at apache.org">Stefano Mazzocchi</a>
+ * @author <a href="mailto:jon at latchkey.com">Jon S. Stevens</a>
+ * @author <a href="mailto:daveb at miceda-data">Dave Bryson</a>
+ * @author <a href="mailto:geirm at optonline.net">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:leon at opticode.co.za">Leon Messerschmidt</a>
+ * @author <a href="mailto:kjohnson at transparent.com">Kent Johnson</a>
+ * @author <a href="mailto:dlr at finemaltcoding.com">Daniel Rall</a>
+ * @author <a href="mailto:ipriha at surfeu.fi">Ilkka Priha</a>
+ * @author <a href="mailto:jvanzyl at apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:mpoeschl at marmot.at">Martin Poeschl</a>
+ * @author <a href="mailto:hps at intermeta.de">Henning P. Schmiedehausen</a>
+ * @author <a href="mailto:epugh at upstate.com">Eric Pugh</a>
+ * @author <a href="mailto:ebourg at apache.org">Emmanuel Bourg</a>
+ * @version $Id: PropertiesConfiguration.java 1534445 2013-10-22 01:19:43Z henning $
+ */
+public class PropertiesConfiguration extends AbstractFileConfiguration
+{
+ /** Constant for the supported comment characters.*/
+ static final String COMMENT_CHARS = "#!";
+
+ /** Constant for the default properties separator.*/
+ static final String DEFAULT_SEPARATOR = " = ";
+
+ /**
+ * Constant for the default {@code IOFactory}. This instance is used
+ * when no specific factory was set.
+ */
+ private static final IOFactory DEFAULT_IO_FACTORY = new DefaultIOFactory();
+
+ /**
+ * This is the name of the property that can point to other
+ * properties file for including other properties files.
+ */
+ private static String include = "include";
+
+ /** The list of possible key/value separators */
+ private static final char[] SEPARATORS = new char[] {'=', ':'};
+
+ /** The white space characters used as key/value separators. */
+ private static final char[] WHITE_SPACE = new char[]{' ', '\t', '\f'};
+
+ /**
+ * The default encoding (ISO-8859-1 as specified by
+ * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html)
+ */
+ private static final String DEFAULT_ENCODING = "ISO-8859-1";
+
+ /** Constant for the platform specific line separator.*/
+ private static final String LINE_SEPARATOR = System.getProperty("line.separator");
+
+ /** Constant for the escaping character.*/
+ private static final String ESCAPE = "\\";
+
+ /** Constant for the escaped escaping character.*/
+ private static final String DOUBLE_ESC = ESCAPE + ESCAPE;
+
+ /** Constant for the radix of hex numbers.*/
+ private static final int HEX_RADIX = 16;
+
+ /** Constant for the length of a unicode literal.*/
+ private static final int UNICODE_LEN = 4;
+
+ /** Stores the layout object.*/
+ private PropertiesConfigurationLayout layout;
+
+ /** The IOFactory for creating readers and writers.*/
+ private volatile IOFactory ioFactory;
+
+ /** Allow file inclusion or not */
+ private boolean includesAllowed = true;
+
+ /**
+ * Creates an empty PropertyConfiguration object which can be
+ * used to synthesize a new Properties file by adding values and
+ * then saving().
+ */
+ public PropertiesConfiguration()
+ {
+ layout = createLayout();
+ }
+
+ /**
+ * Creates and loads the extended properties from the specified file.
+ * The specified file can contain "include = " properties which then
+ * are loaded and merged into the properties.
+ *
+ * @param fileName The name of the properties file to load.
+ * @throws ConfigurationException Error while loading the properties file
+ */
+ public PropertiesConfiguration(String fileName) throws ConfigurationException
+ {
+ super(fileName);
+ }
+
+ /**
+ * Creates and loads the extended properties from the specified file.
+ * The specified file can contain "include = " properties which then
+ * are loaded and merged into the properties. If the file does not exist,
+ * an empty configuration will be created. Later the {@code save()}
+ * method can be called to save the properties to the specified file.
+ *
+ * @param file The properties file to load.
+ * @throws ConfigurationException Error while loading the properties file
+ */
+ public PropertiesConfiguration(File file) throws ConfigurationException
+ {
+ super(file);
+
+ // If the file does not exist, no layout object was created. We have to
+ // do this manually in this case.
+ getLayout();
+ }
+
+ /**
+ * Creates and loads the extended properties from the specified URL.
+ * The specified file can contain "include = " properties which then
+ * are loaded and merged into the properties.
+ *
+ * @param url The location of the properties file to load.
+ * @throws ConfigurationException Error while loading the properties file
+ */
+ public PropertiesConfiguration(URL url) throws ConfigurationException
+ {
+ super(url);
+ }
+
+ /**
+ * Gets the property value for including other properties files.
+ * By default it is "include".
+ *
+ * @return A String.
+ */
+ public static String getInclude()
+ {
+ return PropertiesConfiguration.include;
+ }
+
+ /**
+ * Sets the property value for including other properties files.
+ * By default it is "include".
+ *
+ * @param inc A String.
+ */
+ public static void setInclude(String inc)
+ {
+ PropertiesConfiguration.include = inc;
+ }
+
+ /**
+ * Controls whether additional files can be loaded by the include = <xxx>
+ * statement or not. This is <b>true</b> per default.
+ *
+ * @param includesAllowed True if Includes are allowed.
+ */
+ public void setIncludesAllowed(boolean includesAllowed)
+ {
+ this.includesAllowed = includesAllowed;
+ }
+
+ /**
+ * Reports the status of file inclusion.
+ *
+ * @return True if include files are loaded.
+ *
+ * @see PropertiesConfiguration#isIncludesAllowed()
+ *
+ * @deprecated Only exists to not break backwards compatibility.
+ * Use {@link PropertiesConfiguration#isIncludesAllowed()} instead.
+ */
+ @Deprecated
+ public boolean getIncludesAllowed()
+ {
+ return isIncludesAllowed();
+ }
+
+ /**
+ * Reports the status of file inclusion.
+ *
+ * @return True if include files are loaded.
+ */
+ public boolean isIncludesAllowed()
+ {
+ return this.includesAllowed;
+ }
+
+ /**
+ * Return the comment header.
+ *
+ * @return the comment header
+ * @since 1.1
+ */
+ public String getHeader()
+ {
+ return getLayout().getHeaderComment();
+ }
+
+ /**
+ * Set the comment header.
+ *
+ * @param header the header to use
+ * @since 1.1
+ */
+ public void setHeader(String header)
+ {
+ getLayout().setHeaderComment(header);
+ }
+
+ /**
+ * Returns the footer comment. This is a comment at the very end of the
+ * file.
+ *
+ * @return the footer comment
+ * @since 1.10
+ */
+ public String getFooter()
+ {
+ return getLayout().getFooterComment();
+ }
+
+ /**
+ * Sets the footer comment. If set, this comment is written after all
+ * properties at the end of the file.
+ *
+ * @param footer the footer comment
+ * @since 1.10
+ */
+ public void setFooter(String footer)
+ {
+ getLayout().setFooterComment(footer);
+ }
+
+ /**
+ * Returns the encoding to be used when loading or storing configuration
+ * data. This implementation ensures that the default encoding will be used
+ * if none has been set explicitly.
+ *
+ * @return the encoding
+ */
+ @Override
+ public String getEncoding()
+ {
+ String enc = super.getEncoding();
+ return (enc != null) ? enc : DEFAULT_ENCODING;
+ }
+
+ /**
+ * Returns the associated layout object.
+ *
+ * @return the associated layout object
+ * @since 1.3
+ */
+ public synchronized PropertiesConfigurationLayout getLayout()
+ {
+ if (layout == null)
+ {
+ layout = createLayout();
+ }
+ return layout;
+ }
+
+ /**
+ * Sets the associated layout object.
+ *
+ * @param layout the new layout object; can be <b>null</b>, then a new
+ * layout object will be created
+ * @since 1.3
+ */
+ public synchronized void setLayout(PropertiesConfigurationLayout layout)
+ {
+ // only one layout must exist
+ if (this.layout != null)
+ {
+ removeConfigurationListener(this.layout);
+ }
+
+ if (layout == null)
+ {
+ this.layout = createLayout();
+ }
+ else
+ {
+ this.layout = layout;
+ }
+ }
+
+ /**
+ * Creates the associated layout object. This method is invoked when the
+ * layout object is accessed and has not been created yet. Derived classes
+ * can override this method to hook in a different layout implementation.
+ *
+ * @return the layout object to use
+ * @since 1.3
+ */
+ protected PropertiesConfigurationLayout createLayout()
+ {
+ return new PropertiesConfigurationLayout(this);
+ }
+
+ /**
+ * Returns the {@code IOFactory} to be used for creating readers and
+ * writers when loading or saving this configuration.
+ *
+ * @return the {@code IOFactory}
+ * @since 1.7
+ */
+ public IOFactory getIOFactory()
+ {
+ return (ioFactory != null) ? ioFactory : DEFAULT_IO_FACTORY;
+ }
+
+ /**
+ * Sets the {@code IOFactory} to be used for creating readers and
+ * writers when loading or saving this configuration. Using this method a
+ * client can customize the reader and writer classes used by the load and
+ * save operations. Note that this method must be called before invoking
+ * one of the {@code load()} and {@code save()} methods.
+ * Especially, if you want to use a custom {@code IOFactory} for
+ * changing the {@code PropertiesReader}, you cannot load the
+ * configuration data in the constructor.
+ *
+ * @param ioFactory the new {@code IOFactory} (must not be <b>null</b>)
+ * @throws IllegalArgumentException if the {@code IOFactory} is
+ * <b>null</b>
+ * @since 1.7
+ */
+ public void setIOFactory(IOFactory ioFactory)
+ {
+ if (ioFactory == null)
+ {
+ throw new IllegalArgumentException("IOFactory must not be null!");
+ }
+
+ this.ioFactory = ioFactory;
+ }
+
+ /**
+ * Load the properties from the given reader.
+ * Note that the {@code clear()} method is not called, so
+ * the properties contained in the loaded file will be added to the
+ * actual set of properties.
+ *
+ * @param in An InputStream.
+ *
+ * @throws ConfigurationException if an error occurs
+ */
+ public synchronized void load(Reader in) throws ConfigurationException
+ {
+ boolean oldAutoSave = isAutoSave();
+ setAutoSave(false);
+
+ try
+ {
+ getLayout().load(in);
+ }
+ finally
+ {
+ setAutoSave(oldAutoSave);
+ }
+ }
+
+ /**
+ * Save the configuration to the specified stream.
+ *
+ * @param writer the output stream used to save the configuration
+ * @throws ConfigurationException if an error occurs
+ */
+ public void save(Writer writer) throws ConfigurationException
+ {
+ enterNoReload();
+ try
+ {
+ getLayout().save(writer);
+ }
+ finally
+ {
+ exitNoReload();
+ }
+ }
+
+ /**
+ * Extend the setBasePath method to turn includes
+ * on and off based on the existence of a base path.
+ *
+ * @param basePath The new basePath to set.
+ */
+ @Override
+ public void setBasePath(String basePath)
+ {
+ super.setBasePath(basePath);
+ setIncludesAllowed(StringUtils.isNotEmpty(basePath));
+ }
+
+ /**
+ * Creates a copy of this object.
+ *
+ * @return the copy
+ */
+ @Override
+ public Object clone()
+ {
+ PropertiesConfiguration copy = (PropertiesConfiguration) super.clone();
+ if (layout != null)
+ {
+ copy.setLayout(new PropertiesConfigurationLayout(copy, layout));
+ }
+ return copy;
+ }
+
+ /**
+ * This method is invoked by the associated
+ * {@link PropertiesConfigurationLayout} object for each
+ * property definition detected in the parsed properties file. Its task is
+ * to check whether this is a special property definition (e.g. the
+ * {@code include} property). If not, the property must be added to
+ * this configuration. The return value indicates whether the property
+ * should be treated as a normal property. If it is <b>false</b>, the
+ * layout object will ignore this property.
+ *
+ * @param key the property key
+ * @param value the property value
+ * @return a flag whether this is a normal property
+ * @throws ConfigurationException if an error occurs
+ * @since 1.3
+ */
+ boolean propertyLoaded(String key, String value)
+ throws ConfigurationException
+ {
+ boolean result;
+
+ if (StringUtils.isNotEmpty(getInclude())
+ && key.equalsIgnoreCase(getInclude()))
+ {
+ if (isIncludesAllowed())
+ {
+ String[] files;
+ if (!isDelimiterParsingDisabled())
+ {
+ files = StringUtils.split(value, getListDelimiter());
+ }
+ else
+ {
+ files = new String[]{value};
+ }
+ for (String f : files)
+ {
+ loadIncludeFile(interpolate(f.trim()));
+ }
+ }
+ result = false;
+ }
+
+ else
+ {
+ addProperty(key, value);
+ result = true;
+ }
+
+ return result;
+ }
+
+ /**
+ * Tests whether a line is a comment, i.e. whether it starts with a comment
+ * character.
+ *
+ * @param line the line
+ * @return a flag if this is a comment line
+ * @since 1.3
+ */
+ static boolean isCommentLine(String line)
+ {
+ String s = line.trim();
+ // blanc lines are also treated as comment lines
+ return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0;
+ }
+
+ /**
+ * Returns the number of trailing backslashes. This is sometimes needed for
+ * the correct handling of escape characters.
+ *
+ * @param line the string to investigate
+ * @return the number of trailing backslashes
+ */
+ private static int countTrailingBS(String line)
+ {
+ int bsCount = 0;
+ for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--)
+ {
+ bsCount++;
+ }
+
+ return bsCount;
+ }
+
+ /**
+ * This class is used to read properties lines. These lines do
+ * not terminate with new-line chars but rather when there is no
+ * backslash sign a the end of the line. This is used to
+ * concatenate multiple lines for readability.
+ */
+ public static class PropertiesReader extends LineNumberReader
+ {
+ /** The regular expression to parse the key and the value of a property. */
+ private static final Pattern PROPERTY_PATTERN = Pattern
+ .compile("(([\\S&&[^\\\\" + new String(SEPARATORS)
+ + "]]|\\\\.)*)(\\s*(\\s+|[" + new String(SEPARATORS)
+ + "])\\s*)(.*)");
+
+ /** Constant for the index of the group for the key. */
+ private static final int IDX_KEY = 1;
+
+ /** Constant for the index of the group for the value. */
+ private static final int IDX_VALUE = 5;
+
+ /** Constant for the index of the group for the separator. */
+ private static final int IDX_SEPARATOR = 3;
+
+ /** Stores the comment lines for the currently processed property.*/
+ private List<String> commentLines;
+
+ /** Stores the name of the last read property.*/
+ private String propertyName;
+
+ /** Stores the value of the last read property.*/
+ private String propertyValue;
+
+ /** Stores the property separator of the last read property.*/
+ private String propertySeparator = DEFAULT_SEPARATOR;
+
+ /** Stores the list delimiter character.*/
+ private char delimiter;
+
+ /**
+ * Constructor.
+ *
+ * @param reader A Reader.
+ */
+ public PropertiesReader(Reader reader)
+ {
+ this(reader, AbstractConfiguration.getDefaultListDelimiter());
+ }
+
+ /**
+ * Creates a new instance of {@code PropertiesReader} and sets
+ * the underlying reader and the list delimiter.
+ *
+ * @param reader the reader
+ * @param listDelimiter the list delimiter character
+ * @since 1.3
+ */
+ public PropertiesReader(Reader reader, char listDelimiter)
+ {
+ super(reader);
+ commentLines = new ArrayList<String>();
+ delimiter = listDelimiter;
+ }
+
+ /**
+ * Reads a property line. Returns null if Stream is
+ * at EOF. Concatenates lines ending with "\".
+ * Skips lines beginning with "#" or "!" and empty lines.
+ * The return value is a property definition (<code><name></code>
+ * = <code><value></code>)
+ *
+ * @return A string containing a property value or null
+ *
+ * @throws IOException in case of an I/O error
+ */
+ public String readProperty() throws IOException
+ {
+ commentLines.clear();
+ StringBuilder buffer = new StringBuilder();
+
+ while (true)
+ {
+ String line = readLine();
+ if (line == null)
+ {
+ // EOF
+ return null;
+ }
+
+ if (isCommentLine(line))
+ {
+ commentLines.add(line);
+ continue;
+ }
+
+ line = line.trim();
+
+ if (checkCombineLines(line))
+ {
+ line = line.substring(0, line.length() - 1);
+ buffer.append(line);
+ }
+ else
+ {
+ buffer.append(line);
+ break;
+ }
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * Parses the next property from the input stream and stores the found
+ * name and value in internal fields. These fields can be obtained using
+ * the provided getter methods. The return value indicates whether EOF
+ * was reached (<b>false</b>) or whether further properties are
+ * available (<b>true</b>).
+ *
+ * @return a flag if further properties are available
+ * @throws IOException if an error occurs
+ * @since 1.3
+ */
+ public boolean nextProperty() throws IOException
+ {
+ String line = readProperty();
+
+ if (line == null)
+ {
+ return false; // EOF
+ }
+
+ // parse the line
+ parseProperty(line);
+ return true;
+ }
+
+ /**
+ * Returns the comment lines that have been read for the last property.
+ *
+ * @return the comment lines for the last property returned by
+ * {@code readProperty()}
+ * @since 1.3
+ */
+ public List<String> getCommentLines()
+ {
+ return commentLines;
+ }
+
+ /**
+ * Returns the name of the last read property. This method can be called
+ * after {@link #nextProperty()} was invoked and its
+ * return value was <b>true</b>.
+ *
+ * @return the name of the last read property
+ * @since 1.3
+ */
+ public String getPropertyName()
+ {
+ return propertyName;
+ }
+
+ /**
+ * Returns the value of the last read property. This method can be
+ * called after {@link #nextProperty()} was invoked and
+ * its return value was <b>true</b>.
+ *
+ * @return the value of the last read property
+ * @since 1.3
+ */
+ public String getPropertyValue()
+ {
+ return propertyValue;
+ }
+
+ /**
+ * Returns the separator that was used for the last read property. The
+ * separator can be stored so that it can later be restored when saving
+ * the configuration.
+ *
+ * @return the separator for the last read property
+ * @since 1.7
+ */
+ public String getPropertySeparator()
+ {
+ return propertySeparator;
+ }
+
+ /**
+ * Parses a line read from the properties file. This method is called
+ * for each non-comment line read from the source file. Its task is to
+ * split the passed in line into the property key and its value. The
+ * results of the parse operation can be stored by calling the
+ * {@code initPropertyXXX()} methods.
+ *
+ * @param line the line read from the properties file
+ * @since 1.7
+ */
+ protected void parseProperty(String line)
+ {
+ String[] property = doParseProperty(line);
+ initPropertyName(property[0]);
+ initPropertyValue(property[1]);
+ initPropertySeparator(property[2]);
+ }
+
+ /**
+ * Sets the name of the current property. This method can be called by
+ * {@code parseProperty()} for storing the results of the parse
+ * operation. It also ensures that the property key is correctly
+ * escaped.
+ *
+ * @param name the name of the current property
+ * @since 1.7
+ */
+ protected void initPropertyName(String name)
+ {
+ propertyName = StringEscapeUtils.unescapeJava(name);
+ }
+
+ /**
+ * Sets the value of the current property. This method can be called by
+ * {@code parseProperty()} for storing the results of the parse
+ * operation. It also ensures that the property value is correctly
+ * escaped.
+ *
+ * @param value the value of the current property
+ * @since 1.7
+ */
+ protected void initPropertyValue(String value)
+ {
+ propertyValue = unescapeJava(value, delimiter);
+ }
+
+ /**
+ * Sets the separator of the current property. This method can be called
+ * by {@code parseProperty()}. It allows the associated layout
+ * object to keep track of the property separators. When saving the
+ * configuration the separators can be restored.
+ *
+ * @param value the separator used for the current property
+ * @since 1.7
+ */
+ protected void initPropertySeparator(String value)
+ {
+ propertySeparator = value;
+ }
+
+ /**
+ * Checks if the passed in line should be combined with the following.
+ * This is true, if the line ends with an odd number of backslashes.
+ *
+ * @param line the line
+ * @return a flag if the lines should be combined
+ */
+ private static boolean checkCombineLines(String line)
+ {
+ return countTrailingBS(line) % 2 != 0;
+ }
+
+ /**
+ * Parse a property line and return the key, the value, and the separator in an array.
+ *
+ * @param line the line to parse
+ * @return an array with the property's key, value, and separator
+ */
+ private static String[] doParseProperty(String line)
+ {
+ Matcher matcher = PROPERTY_PATTERN.matcher(line);
+
+ String[] result = {"", "", ""};
+
+ if (matcher.matches())
+ {
+ result[0] = matcher.group(IDX_KEY).trim();
+ result[1] = matcher.group(IDX_VALUE).trim();
+ result[2] = matcher.group(IDX_SEPARATOR);
+ }
+
+ return result;
+ }
+ } // class PropertiesReader
+
+ /**
+ * This class is used to write properties lines. The most important method
+ * is {@code writeProperty(String, Object, boolean)}, which is called
+ * during a save operation for each property found in the configuration.
+ */
+ public static class PropertiesWriter extends FilterWriter
+ {
+ /** Constant for the initial size when creating a string buffer. */
+ private static final int BUF_SIZE = 8;
+
+ /** The delimiter for multi-valued properties.*/
+ private char delimiter;
+
+ /** The separator to be used for the current property. */
+ private String currentSeparator;
+
+ /** The global separator. If set, it overrides the current separator.*/
+ private String globalSeparator;
+
+ /** The line separator.*/
+ private String lineSeparator;
+
+ /**
+ * Constructor.
+ *
+ * @param writer a Writer object providing the underlying stream
+ * @param delimiter the delimiter character for multi-valued properties
+ */
+ public PropertiesWriter(Writer writer, char delimiter)
+ {
+ super(writer);
+ this.delimiter = delimiter;
+ }
+
+ /**
+ * Returns the current property separator.
+ *
+ * @return the current property separator
+ * @since 1.7
+ */
+ public String getCurrentSeparator()
+ {
+ return currentSeparator;
+ }
+
+ /**
+ * Sets the current property separator. This separator is used when
+ * writing the next property.
+ *
+ * @param currentSeparator the current property separator
+ * @since 1.7
+ */
+ public void setCurrentSeparator(String currentSeparator)
+ {
+ this.currentSeparator = currentSeparator;
+ }
+
+ /**
+ * Returns the global property separator.
+ *
+ * @return the global property separator
+ * @since 1.7
+ */
+ public String getGlobalSeparator()
+ {
+ return globalSeparator;
+ }
+
+ /**
+ * Sets the global property separator. This separator corresponds to the
+ * {@code globalSeparator} property of
+ * {@link PropertiesConfigurationLayout}. It defines the separator to be
+ * used for all properties. If it is undefined, the current separator is
+ * used.
+ *
+ * @param globalSeparator the global property separator
+ * @since 1.7
+ */
+ public void setGlobalSeparator(String globalSeparator)
+ {
+ this.globalSeparator = globalSeparator;
+ }
+
+ /**
+ * Returns the line separator.
+ *
+ * @return the line separator
+ * @since 1.7
+ */
+ public String getLineSeparator()
+ {
+ return (lineSeparator != null) ? lineSeparator : LINE_SEPARATOR;
+ }
+
+ /**
+ * Sets the line separator. Each line written by this writer is
+ * terminated with this separator. If not set, the platform-specific
+ * line separator is used.
+ *
+ * @param lineSeparator the line separator to be used
+ * @since 1.7
+ */
+ public void setLineSeparator(String lineSeparator)
+ {
+ this.lineSeparator = lineSeparator;
+ }
+
+ /**
+ * Write a property.
+ *
+ * @param key the key of the property
+ * @param value the value of the property
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ public void writeProperty(String key, Object value) throws IOException
+ {
+ writeProperty(key, value, false);
+ }
+
+ /**
+ * Write a property.
+ *
+ * @param key The key of the property
+ * @param values The array of values of the property
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ public void writeProperty(String key, List<?> values) throws IOException
+ {
+ for (int i = 0; i < values.size(); i++)
+ {
+ writeProperty(key, values.get(i));
+ }
+ }
+
+ /**
+ * Writes the given property and its value. If the value happens to be a
+ * list, the {@code forceSingleLine} flag is evaluated. If it is
+ * set, all values are written on a single line using the list delimiter
+ * as separator.
+ *
+ * @param key the property key
+ * @param value the property value
+ * @param forceSingleLine the "force single line" flag
+ * @throws IOException if an error occurs
+ * @since 1.3
+ */
+ public void writeProperty(String key, Object value,
+ boolean forceSingleLine) throws IOException
+ {
+ String v;
+
+ if (value instanceof List)
+ {
+ List<?> values = (List<?>) value;
+ if (forceSingleLine)
+ {
+ v = makeSingleLineValue(values);
+ }
+ else
+ {
+ writeProperty(key, values);
+ return;
+ }
+ }
+ else
+ {
+ v = escapeValue(value, false);
+ }
+
+ write(escapeKey(key));
+ write(fetchSeparator(key, value));
+ write(v);
+
+ writeln(null);
+ }
+
+ /**
+ * Write a comment.
+ *
+ * @param comment the comment to write
+ * @throws IOException if an I/O error occurs
+ */
+ public void writeComment(String comment) throws IOException
+ {
+ writeln("# " + comment);
+ }
+
+ /**
+ * Escape the separators in the key.
+ *
+ * @param key the key
+ * @return the escaped key
+ * @since 1.2
+ */
+ private String escapeKey(String key)
+ {
+ StringBuilder newkey = new StringBuilder();
+
+ for (int i = 0; i < key.length(); i++)
+ {
+ char c = key.charAt(i);
+
+ if (ArrayUtils.contains(SEPARATORS, c) || ArrayUtils.contains(WHITE_SPACE, c))
+ {
+ // escape the separator
+ newkey.append('\\');
+ newkey.append(c);
+ }
+ else
+ {
+ newkey.append(c);
+ }
+ }
+
+ return newkey.toString();
+ }
+
+ /**
+ * Escapes the given property value. Delimiter characters in the value
+ * will be escaped.
+ *
+ * @param value the property value
+ * @param inList a flag whether the value is part of a list
+ * @return the escaped property value
+ * @since 1.3
+ */
+ private String escapeValue(Object value, boolean inList)
+ {
+ String escapedValue = handleBackslashs(value, inList);
+ if (delimiter != 0)
+ {
+ escapedValue = StringUtils.replace(escapedValue, String.valueOf(delimiter), ESCAPE + delimiter);
+ }
+ return escapedValue;
+ }
+
+ /**
+ * Performs the escaping of backslashes in the specified properties
+ * value. Because a double backslash is used to escape the escape
+ * character of a list delimiter, double backslashes also have to be
+ * escaped if the property is part of a (single line) list. Then, in all
+ * cases each backslash has to be doubled in order to produce a valid
+ * properties file.
+ *
+ * @param value the value to be escaped
+ * @param inList a flag whether the value is part of a list
+ * @return the value with escaped backslashes as string
+ */
+ private String handleBackslashs(Object value, boolean inList)
+ {
+ String strValue = String.valueOf(value);
+
+ if (inList && strValue.indexOf(DOUBLE_ESC) >= 0)
+ {
+ char esc = ESCAPE.charAt(0);
+ StringBuilder buf = new StringBuilder(strValue.length() + BUF_SIZE);
+ for (int i = 0; i < strValue.length(); i++)
+ {
+ if (strValue.charAt(i) == esc && i < strValue.length() - 1
+ && strValue.charAt(i + 1) == esc)
+ {
+ buf.append(DOUBLE_ESC).append(DOUBLE_ESC);
+ i++;
+ }
+ else
+ {
+ buf.append(strValue.charAt(i));
+ }
+ }
+
+ strValue = buf.toString();
+ }
+
+ return StringEscapeUtils.escapeJava(strValue);
+ }
+
+ /**
+ * Transforms a list of values into a single line value.
+ *
+ * @param values the list with the values
+ * @return a string with the single line value (can be <b>null</b>)
+ * @since 1.3
+ */
+ private String makeSingleLineValue(List<?> values)
+ {
+ if (!values.isEmpty())
+ {
+ Iterator<?> it = values.iterator();
+ String lastValue = escapeValue(it.next(), true);
+ StringBuilder buf = new StringBuilder(lastValue);
+ while (it.hasNext())
+ {
+ // if the last value ended with an escape character, it has
+ // to be escaped itself; otherwise the list delimiter will
+ // be escaped
+ if (lastValue.endsWith(ESCAPE) && (countTrailingBS(lastValue) / 2) % 2 != 0)
+ {
+ buf.append(ESCAPE).append(ESCAPE);
+ }
+ buf.append(delimiter);
+ lastValue = escapeValue(it.next(), true);
+ buf.append(lastValue);
+ }
+ return buf.toString();
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Helper method for writing a line with the platform specific line
+ * ending.
+ *
+ * @param s the content of the line (may be <b>null</b>)
+ * @throws IOException if an error occurs
+ * @since 1.3
+ */
+ public void writeln(String s) throws IOException
+ {
+ if (s != null)
+ {
+ write(s);
+ }
+ write(getLineSeparator());
+ }
+
+ /**
+ * Returns the separator to be used for the given property. This method
+ * is called by {@code writeProperty()}. The string returned here
+ * is used as separator between the property key and its value. Per
+ * default the method checks whether a global separator is set. If this
+ * is the case, it is returned. Otherwise the separator returned by
+ * {@code getCurrentSeparator()} is used, which was set by the
+ * associated layout object. Derived classes may implement a different
+ * strategy for defining the separator.
+ *
+ * @param key the property key
+ * @param value the value
+ * @return the separator to be used
+ * @since 1.7
+ */
+ protected String fetchSeparator(String key, Object value)
+ {
+ return (getGlobalSeparator() != null) ? getGlobalSeparator()
+ : getCurrentSeparator();
+ }
+ } // class PropertiesWriter
+
+ /**
+ * <p>
+ * Definition of an interface that allows customization of read and write
+ * operations.
+ * </p>
+ * <p>
+ * For reading and writing properties files the inner classes
+ * {@code PropertiesReader} and {@code PropertiesWriter} are used.
+ * This interface defines factory methods for creating both a
+ * {@code PropertiesReader} and a {@code PropertiesWriter}. An
+ * object implementing this interface can be passed to the
+ * {@code setIOFactory()} method of
+ * {@code PropertiesConfiguration}. Every time the configuration is
+ * read or written the {@code IOFactory} is asked to create the
+ * appropriate reader or writer object. This provides an opportunity to
+ * inject custom reader or writer implementations.
+ * </p>
+ *
+ * @since 1.7
+ */
+ public interface IOFactory
+ {
+ /**
+ * Creates a {@code PropertiesReader} for reading a properties
+ * file. This method is called whenever the
+ * {@code PropertiesConfiguration} is loaded. The reader returned
+ * by this method is then used for parsing the properties file.
+ *
+ * @param in the underlying reader (of the properties file)
+ * @param delimiter the delimiter character for list parsing
+ * @return the {@code PropertiesReader} for loading the
+ * configuration
+ */
+ PropertiesReader createPropertiesReader(Reader in, char delimiter);
+
+ /**
+ * Creates a {@code PropertiesWriter} for writing a properties
+ * file. This method is called before the
+ * {@code PropertiesConfiguration} is saved. The writer returned by
+ * this method is then used for writing the properties file.
+ *
+ * @param out the underlying writer (to the properties file)
+ * @param delimiter the delimiter character for list parsing
+ * @return the {@code PropertiesWriter} for saving the
+ * configuration
+ */
+ PropertiesWriter createPropertiesWriter(Writer out, char delimiter);
+ }
+
+ /**
+ * <p>
+ * A default implementation of the {@code IOFactory} interface.
+ * </p>
+ * <p>
+ * This class implements the {@code createXXXX()} methods defined by
+ * the {@code IOFactory} interface in a way that the default objects
+ * (i.e. {@code PropertiesReader} and {@code PropertiesWriter} are
+ * returned. Customizing either the reader or the writer (or both) can be
+ * done by extending this class and overriding the corresponding
+ * {@code createXXXX()} method.
+ * </p>
+ *
+ * @since 1.7
+ */
+ public static class DefaultIOFactory implements IOFactory
+ {
+ public PropertiesReader createPropertiesReader(Reader in, char delimiter)
+ {
+ return new PropertiesReader(in, delimiter);
+ }
+
+ public PropertiesWriter createPropertiesWriter(Writer out,
+ char delimiter)
+ {
+ return new PropertiesWriter(out, delimiter);
+ }
+ }
+
+ /**
+ * <p>Unescapes any Java literals found in the {@code String} to a
+ * {@code Writer}.</p> This is a slightly modified version of the
+ * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't
+ * drop escaped separators (i.e '\,').
+ *
+ * @param str the {@code String} to unescape, may be null
+ * @param delimiter the delimiter for multi-valued properties
+ * @return the processed string
+ * @throws IllegalArgumentException if the Writer is {@code null}
+ */
+ protected static String unescapeJava(String str, char delimiter)
+ {
+ if (str == null)
+ {
+ return null;
+ }
+ int sz = str.length();
+ StringBuilder out = new StringBuilder(sz);
+ StringBuilder unicode = new StringBuilder(UNICODE_LEN);
+ boolean hadSlash = false;
+ boolean inUnicode = false;
+ for (int i = 0; i < sz; i++)
+ {
+ char ch = str.charAt(i);
+ if (inUnicode)
+ {
+ // if in unicode, then we're reading unicode
+ // values in somehow
+ unicode.append(ch);
+ if (unicode.length() == UNICODE_LEN)
+ {
+ // unicode now contains the four hex digits
+ // which represents our unicode character
+ try
+ {
+ int value = Integer.parseInt(unicode.toString(), HEX_RADIX);
+ out.append((char) value);
+ unicode.setLength(0);
+ inUnicode = false;
+ hadSlash = false;
+ }
+ catch (NumberFormatException nfe)
+ {
+ throw new ConfigurationRuntimeException("Unable to parse unicode value: " + unicode, nfe);
+ }
+ }
+ continue;
+ }
+
+ if (hadSlash)
+ {
+ // handle an escaped value
+ hadSlash = false;
+
+ if (ch == '\\')
+ {
+ out.append('\\');
+ }
+ else if (ch == '\'')
+ {
+ out.append('\'');
+ }
+ else if (ch == '\"')
+ {
+ out.append('"');
+ }
+ else if (ch == 'r')
+ {
+ out.append('\r');
+ }
+ else if (ch == 'f')
+ {
+ out.append('\f');
+ }
+ else if (ch == 't')
+ {
+ out.append('\t');
+ }
+ else if (ch == 'n')
+ {
+ out.append('\n');
+ }
+ else if (ch == 'b')
+ {
+ out.append('\b');
+ }
+ else if (ch == delimiter)
+ {
+ out.append('\\');
+ out.append(delimiter);
+ }
+ else if (ch == 'u')
+ {
+ // uh-oh, we're in unicode country....
+ inUnicode = true;
+ }
+ else
+ {
+ out.append(ch);
+ }
+
+ continue;
+ }
+ else if (ch == '\\')
+ {
+ hadSlash = true;
+ continue;
+ }
+ out.append(ch);
+ }
+
+ if (hadSlash)
+ {
+ // then we're in the weird case of a \ at the end of the
+ // string, let's output it anyway.
+ out.append('\\');
+ }
+
+ return out.toString();
+ }
+
+ /**
+ * Helper method for loading an included properties file. This method is
+ * called by {@code load()} when an {@code include} property
+ * is encountered. It tries to resolve relative file names based on the
+ * current base path. If this fails, a resolution based on the location of
+ * this properties file is tried.
+ *
+ * @param fileName the name of the file to load
+ * @throws ConfigurationException if loading fails
+ */
+ private void loadIncludeFile(String fileName) throws ConfigurationException
+ {
+ URL url = ConfigurationUtils.locate(getFileSystem(), getBasePath(), fileName);
+ if (url == null)
+ {
+ URL baseURL = getURL();
+ if (baseURL != null)
+ {
+ url = ConfigurationUtils.locate(getFileSystem(), baseURL.toString(), fileName);
+ }
+ }
+
+ if (url == null)
+ {
+ throw new ConfigurationException("Cannot resolve include file "
+ + fileName);
+ }
+ load(url);
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java b/src/main/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java
new file mode 100644
index 0000000..e4f0891
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java
@@ -0,0 +1,1064 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * <p>
+ * A helper class used by {@link PropertiesConfiguration} to keep
+ * the layout of a properties file.
+ * </p>
+ * <p>
+ * Instances of this class are associated with a
+ * {@code PropertiesConfiguration} object. They are responsible for
+ * analyzing properties files and for extracting as much information about the
+ * file layout (e.g. empty lines, comments) as possible. When the properties
+ * file is written back again it should be close to the original.
+ * </p>
+ * <p>
+ * The {@code PropertiesConfigurationLayout} object associated with a
+ * {@code PropertiesConfiguration} object can be obtained using the
+ * {@code getLayout()} method of the configuration. Then the methods
+ * provided by this class can be used to alter the properties file's layout.
+ * </p>
+ * <p>
+ * Implementation note: This is a very simple implementation, which is far away
+ * from being perfect, i.e. the original layout of a properties file won't be
+ * reproduced in all cases. One limitation is that comments for multi-valued
+ * property keys are concatenated. Maybe this implementation can later be
+ * improved.
+ * </p>
+ * <p>
+ * To get an impression how this class works consider the following properties
+ * file:
+ * </p>
+ * <p>
+ *
+ * <pre>
+ * # A demo configuration file
+ * # for Demo App 1.42
+ *
+ * # Application name
+ * AppName=Demo App
+ *
+ * # Application vendor
+ * AppVendor=DemoSoft
+ *
+ *
+ * # GUI properties
+ * # Window Color
+ * windowColors=0xFFFFFF,0x000000
+ *
+ * # Include some setting
+ * include=settings.properties
+ * # Another vendor
+ * AppVendor=TestSoft
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * For this example the following points are relevant:
+ * </p>
+ * <p>
+ * <ul>
+ * <li>The first two lines are set as header comment. The header comment is
+ * determined by the last blanc line before the first property definition.</li>
+ * <li>For the property {@code AppName} one comment line and one
+ * leading blanc line is stored.</li>
+ * <li>For the property {@code windowColors} two comment lines and two
+ * leading blanc lines are stored.</li>
+ * <li>Include files is something this class cannot deal with well. When saving
+ * the properties configuration back, the included properties are simply
+ * contained in the original file. The comment before the include property is
+ * skipped.</li>
+ * <li>For all properties except for {@code AppVendor} the "single
+ * line" flag is set. This is relevant only for {@code windowColors},
+ * which has multiple values defined in one line using the separator character.</li>
+ * <li>The {@code AppVendor} property appears twice. The comment lines
+ * are concatenated, so that {@code layout.getComment("AppVendor");} will
+ * result in <code>Application vendor<CR>Another vendor</code>, with
+ * <code><CR></code> meaning the line separator. In addition the
+ * "single line" flag is set to <b>false</b> for this property. When
+ * the file is saved, two property definitions will be written (in series).</li>
+ * </ul>
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: PropertiesConfigurationLayout.java 1534402 2013-10-21 22:35:52Z henning $
+ * @since 1.3
+ */
+public class PropertiesConfigurationLayout implements ConfigurationListener
+{
+ /** Constant for the line break character. */
+ private static final String CR = "\n";
+
+ /** Constant for the default comment prefix. */
+ private static final String COMMENT_PREFIX = "# ";
+
+ /** Stores the associated configuration object. */
+ private PropertiesConfiguration configuration;
+
+ /** Stores a map with the contained layout information. */
+ private Map<String, PropertyLayoutData> layoutData;
+
+ /** Stores the header comment. */
+ private String headerComment;
+
+ /** Stores the footer comment. */
+ private String footerComment;
+
+ /** The global separator that will be used for all properties. */
+ private String globalSeparator;
+
+ /** The line separator.*/
+ private String lineSeparator;
+
+ /** A counter for determining nested load calls. */
+ private int loadCounter;
+
+ /** Stores the force single line flag. */
+ private boolean forceSingleLine;
+
+ /**
+ * Creates a new instance of {@code PropertiesConfigurationLayout}
+ * and initializes it with the associated configuration object.
+ *
+ * @param config the configuration (must not be <b>null</b>)
+ */
+ public PropertiesConfigurationLayout(PropertiesConfiguration config)
+ {
+ this(config, null);
+ }
+
+ /**
+ * Creates a new instance of {@code PropertiesConfigurationLayout}
+ * and initializes it with the given configuration object. The data of the
+ * specified layout object is copied.
+ *
+ * @param config the configuration (must not be <b>null</b>)
+ * @param c the layout object to be copied
+ */
+ public PropertiesConfigurationLayout(PropertiesConfiguration config,
+ PropertiesConfigurationLayout c)
+ {
+ if (config == null)
+ {
+ throw new IllegalArgumentException(
+ "Configuration must not be null!");
+ }
+ configuration = config;
+ layoutData = new LinkedHashMap<String, PropertyLayoutData>();
+ config.addConfigurationListener(this);
+
+ if (c != null)
+ {
+ copyFrom(c);
+ }
+ }
+
+ /**
+ * Returns the associated configuration object.
+ *
+ * @return the associated configuration
+ */
+ public PropertiesConfiguration getConfiguration()
+ {
+ return configuration;
+ }
+
+ /**
+ * Returns the comment for the specified property key in a canonical form.
+ * "Canonical" means that either all lines start with a comment
+ * character or none. If the {@code commentChar} parameter is <b>false</b>,
+ * all comment characters are removed, so that the result is only the plain
+ * text of the comment. Otherwise it is ensured that each line of the
+ * comment starts with a comment character. Also, line breaks in the comment
+ * are normalized to the line separator "\n".
+ *
+ * @param key the key of the property
+ * @param commentChar determines whether all lines should start with comment
+ * characters or not
+ * @return the canonical comment for this key (can be <b>null</b>)
+ */
+ public String getCanonicalComment(String key, boolean commentChar)
+ {
+ return constructCanonicalComment(getComment(key), commentChar);
+ }
+
+ /**
+ * Returns the comment for the specified property key. The comment is
+ * returned as it was set (either manually by calling
+ * {@code setComment()} or when it was loaded from a properties
+ * file). No modifications are performed.
+ *
+ * @param key the key of the property
+ * @return the comment for this key (can be <b>null</b>)
+ */
+ public String getComment(String key)
+ {
+ return fetchLayoutData(key).getComment();
+ }
+
+ /**
+ * Sets the comment for the specified property key. The comment (or its
+ * single lines if it is a multi-line comment) can start with a comment
+ * character. If this is the case, it will be written without changes.
+ * Otherwise a default comment character is added automatically.
+ *
+ * @param key the key of the property
+ * @param comment the comment for this key (can be <b>null</b>, then the
+ * comment will be removed)
+ */
+ public void setComment(String key, String comment)
+ {
+ fetchLayoutData(key).setComment(comment);
+ }
+
+ /**
+ * Returns the number of blanc lines before this property key. If this key
+ * does not exist, 0 will be returned.
+ *
+ * @param key the property key
+ * @return the number of blanc lines before the property definition for this
+ * key
+ */
+ public int getBlancLinesBefore(String key)
+ {
+ return fetchLayoutData(key).getBlancLines();
+ }
+
+ /**
+ * Sets the number of blanc lines before the given property key. This can be
+ * used for a logical grouping of properties.
+ *
+ * @param key the property key
+ * @param number the number of blanc lines to add before this property
+ * definition
+ */
+ public void setBlancLinesBefore(String key, int number)
+ {
+ fetchLayoutData(key).setBlancLines(number);
+ }
+
+ /**
+ * Returns the header comment of the represented properties file in a
+ * canonical form. With the {@code commentChar} parameter it can be
+ * specified whether comment characters should be stripped or be always
+ * present.
+ *
+ * @param commentChar determines the presence of comment characters
+ * @return the header comment (can be <b>null</b>)
+ */
+ public String getCanonicalHeaderComment(boolean commentChar)
+ {
+ return constructCanonicalComment(getHeaderComment(), commentChar);
+ }
+
+ /**
+ * Returns the header comment of the represented properties file. This
+ * method returns the header comment exactly as it was set using
+ * {@code setHeaderComment()} or extracted from the loaded properties
+ * file.
+ *
+ * @return the header comment (can be <b>null</b>)
+ */
+ public String getHeaderComment()
+ {
+ return headerComment;
+ }
+
+ /**
+ * Sets the header comment for the represented properties file. This comment
+ * will be output on top of the file.
+ *
+ * @param comment the comment
+ */
+ public void setHeaderComment(String comment)
+ {
+ headerComment = comment;
+ }
+
+ /**
+ * Returns the footer comment of the represented properties file in a
+ * canonical form. This method works like
+ * {@code getCanonicalHeaderComment()}, but reads the footer comment.
+ *
+ * @param commentChar determines the presence of comment characters
+ * @return the footer comment (can be <b>null</b>)
+ * @see #getCanonicalHeaderComment(boolean)
+ * @since 1.10
+ */
+ public String getCanonicalFooterCooment(boolean commentChar)
+ {
+ return constructCanonicalComment(getFooterComment(), commentChar);
+ }
+
+ /**
+ * Returns the footer comment of the represented properties file. This
+ * method returns the footer comment exactly as it was set using
+ * {@code setFooterComment()} or extracted from the loaded properties
+ * file.
+ *
+ * @return the footer comment (can be <b>null</b>)
+ * @since 1.10
+ */
+ public String getFooterComment()
+ {
+ return footerComment;
+ }
+
+ /**
+ * Sets the footer comment for the represented properties file. This comment
+ * will be output at the bottom of the file.
+ *
+ * @param footerComment the footer comment
+ * @since 1.10
+ */
+ public void setFooterComment(String footerComment)
+ {
+ this.footerComment = footerComment;
+ }
+
+ /**
+ * Returns a flag whether the specified property is defined on a single
+ * line. This is meaningful only if this property has multiple values.
+ *
+ * @param key the property key
+ * @return a flag if this property is defined on a single line
+ */
+ public boolean isSingleLine(String key)
+ {
+ return fetchLayoutData(key).isSingleLine();
+ }
+
+ /**
+ * Sets the "single line flag" for the specified property key.
+ * This flag is evaluated if the property has multiple values (i.e. if it is
+ * a list property). In this case, if the flag is set, all values will be
+ * written in a single property definition using the list delimiter as
+ * separator. Otherwise multiple lines will be written for this property,
+ * each line containing one property value.
+ *
+ * @param key the property key
+ * @param f the single line flag
+ */
+ public void setSingleLine(String key, boolean f)
+ {
+ fetchLayoutData(key).setSingleLine(f);
+ }
+
+ /**
+ * Returns the "force single line" flag.
+ *
+ * @return the force single line flag
+ * @see #setForceSingleLine(boolean)
+ */
+ public boolean isForceSingleLine()
+ {
+ return forceSingleLine;
+ }
+
+ /**
+ * Sets the "force single line" flag. If this flag is set, all
+ * properties with multiple values are written on single lines. This mode
+ * provides more compatibility with {@code java.lang.Properties},
+ * which cannot deal with multiple definitions of a single property. This
+ * mode has no effect if the list delimiter parsing is disabled.
+ *
+ * @param f the force single line flag
+ */
+ public void setForceSingleLine(boolean f)
+ {
+ forceSingleLine = f;
+ }
+
+ /**
+ * Returns the separator for the property with the given key.
+ *
+ * @param key the property key
+ * @return the property separator for this property
+ * @since 1.7
+ */
+ public String getSeparator(String key)
+ {
+ return fetchLayoutData(key).getSeparator();
+ }
+
+ /**
+ * Sets the separator to be used for the property with the given key. The
+ * separator is the string between the property key and its value. For new
+ * properties " = " is used. When a properties file is read, the
+ * layout tries to determine the separator for each property. With this
+ * method the separator can be changed. To be compatible with the properties
+ * format only the characters {@code =} and {@code :} (with or
+ * without whitespace) should be used, but this method does not enforce this
+ * - it accepts arbitrary strings. If the key refers to a property with
+ * multiple values that are written on multiple lines, this separator will
+ * be used on all lines.
+ *
+ * @param key the key for the property
+ * @param sep the separator to be used for this property
+ * @since 1.7
+ */
+ public void setSeparator(String key, String sep)
+ {
+ fetchLayoutData(key).setSeparator(sep);
+ }
+
+ /**
+ * Returns the global separator.
+ *
+ * @return the global properties separator
+ * @since 1.7
+ */
+ public String getGlobalSeparator()
+ {
+ return globalSeparator;
+ }
+
+ /**
+ * Sets the global separator for properties. With this method a separator
+ * can be set that will be used for all properties when writing the
+ * configuration. This is an easy way of determining the properties
+ * separator globally. To be compatible with the properties format only the
+ * characters {@code =} and {@code :} (with or without whitespace)
+ * should be used, but this method does not enforce this - it accepts
+ * arbitrary strings. If the global separator is set to <b>null</b>,
+ * property separators are not changed. This is the default behavior as it
+ * produces results that are closer to the original properties file.
+ *
+ * @param globalSeparator the separator to be used for all properties
+ * @since 1.7
+ */
+ public void setGlobalSeparator(String globalSeparator)
+ {
+ this.globalSeparator = globalSeparator;
+ }
+
+ /**
+ * Returns the line separator.
+ *
+ * @return the line separator
+ * @since 1.7
+ */
+ public String getLineSeparator()
+ {
+ return lineSeparator;
+ }
+
+ /**
+ * Sets the line separator. When writing the properties configuration, all
+ * lines are terminated with this separator. If no separator was set, the
+ * platform-specific default line separator is used.
+ *
+ * @param lineSeparator the line separator
+ * @since 1.7
+ */
+ public void setLineSeparator(String lineSeparator)
+ {
+ this.lineSeparator = lineSeparator;
+ }
+
+ /**
+ * Returns a set with all property keys managed by this object.
+ *
+ * @return a set with all contained property keys
+ */
+ public Set<String> getKeys()
+ {
+ return layoutData.keySet();
+ }
+
+ /**
+ * Reads a properties file and stores its internal structure. The found
+ * properties will be added to the associated configuration object.
+ *
+ * @param in the reader to the properties file
+ * @throws ConfigurationException if an error occurs
+ */
+ public void load(Reader in) throws ConfigurationException
+ {
+ if (++loadCounter == 1)
+ {
+ getConfiguration().removeConfigurationListener(this);
+ }
+ PropertiesConfiguration.PropertiesReader reader = getConfiguration()
+ .getIOFactory().createPropertiesReader(in,
+ getConfiguration().getListDelimiter());
+
+ try
+ {
+ while (reader.nextProperty())
+ {
+ if (getConfiguration().propertyLoaded(reader.getPropertyName(),
+ reader.getPropertyValue()))
+ {
+ boolean contained = layoutData.containsKey(reader
+ .getPropertyName());
+ int blancLines = 0;
+ int idx = checkHeaderComment(reader.getCommentLines());
+ while (idx < reader.getCommentLines().size()
+ && reader.getCommentLines().get(idx).length() < 1)
+ {
+ idx++;
+ blancLines++;
+ }
+ String comment = extractComment(reader.getCommentLines(),
+ idx, reader.getCommentLines().size() - 1);
+ PropertyLayoutData data = fetchLayoutData(reader
+ .getPropertyName());
+ if (contained)
+ {
+ data.addComment(comment);
+ data.setSingleLine(false);
+ }
+ else
+ {
+ data.setComment(comment);
+ data.setBlancLines(blancLines);
+ data.setSeparator(reader.getPropertySeparator());
+ }
+ }
+ }
+
+ setFooterComment(extractComment(reader.getCommentLines(), 0, reader
+ .getCommentLines().size() - 1));
+ }
+ catch (IOException ioex)
+ {
+ throw new ConfigurationException(ioex);
+ }
+ finally
+ {
+ if (--loadCounter == 0)
+ {
+ getConfiguration().addConfigurationListener(this);
+ }
+ }
+ }
+
+ /**
+ * Writes the properties file to the given writer, preserving as much of its
+ * structure as possible.
+ *
+ * @param out the writer
+ * @throws ConfigurationException if an error occurs
+ */
+ public void save(Writer out) throws ConfigurationException
+ {
+ try
+ {
+ char delimiter = getConfiguration().isDelimiterParsingDisabled() ? 0
+ : getConfiguration().getListDelimiter();
+ PropertiesConfiguration.PropertiesWriter writer = getConfiguration()
+ .getIOFactory().createPropertiesWriter(out, delimiter);
+ writer.setGlobalSeparator(getGlobalSeparator());
+ if (getLineSeparator() != null)
+ {
+ writer.setLineSeparator(getLineSeparator());
+ }
+
+ if (headerComment != null)
+ {
+ writeComment(writer, getCanonicalHeaderComment(true));
+ writer.writeln(null);
+ }
+
+ for (String key : layoutData.keySet())
+ {
+ if (getConfiguration().containsKey(key))
+ {
+
+ // Output blank lines before property
+ for (int i = 0; i < getBlancLinesBefore(key); i++)
+ {
+ writer.writeln(null);
+ }
+
+ // Output the comment
+ writeComment(writer, getCanonicalComment(key, true));
+
+ // Output the property and its value
+ boolean singleLine = (isForceSingleLine() || isSingleLine(key))
+ && !getConfiguration().isDelimiterParsingDisabled();
+ writer.setCurrentSeparator(getSeparator(key));
+ writer.writeProperty(key, getConfiguration().getProperty(
+ key), singleLine);
+ }
+ }
+
+ writeComment(writer, getCanonicalFooterCooment(true));
+ writer.flush();
+ }
+ catch (IOException ioex)
+ {
+ throw new ConfigurationException(ioex);
+ }
+ }
+
+ /**
+ * The event listener callback. Here event notifications of the
+ * configuration object are processed to update the layout object properly.
+ *
+ * @param event the event object
+ */
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ if (event.isBeforeUpdate())
+ {
+ if (AbstractFileConfiguration.EVENT_RELOAD == event.getType())
+ {
+ clear();
+ }
+ }
+
+ else
+ {
+ switch (event.getType())
+ {
+ case AbstractConfiguration.EVENT_ADD_PROPERTY:
+ boolean contained = layoutData.containsKey(event
+ .getPropertyName());
+ PropertyLayoutData data = fetchLayoutData(event
+ .getPropertyName());
+ data.setSingleLine(!contained);
+ break;
+ case AbstractConfiguration.EVENT_CLEAR_PROPERTY:
+ layoutData.remove(event.getPropertyName());
+ break;
+ case AbstractConfiguration.EVENT_CLEAR:
+ clear();
+ break;
+ case AbstractConfiguration.EVENT_SET_PROPERTY:
+ fetchLayoutData(event.getPropertyName());
+ break;
+ }
+ }
+ }
+
+ /**
+ * Returns a layout data object for the specified key. If this is a new key,
+ * a new object is created and initialized with default values.
+ *
+ * @param key the key
+ * @return the corresponding layout data object
+ */
+ private PropertyLayoutData fetchLayoutData(String key)
+ {
+ if (key == null)
+ {
+ throw new IllegalArgumentException("Property key must not be null!");
+ }
+
+ PropertyLayoutData data = layoutData.get(key);
+ if (data == null)
+ {
+ data = new PropertyLayoutData();
+ data.setSingleLine(true);
+ layoutData.put(key, data);
+ }
+
+ return data;
+ }
+
+ /**
+ * Removes all content from this layout object.
+ */
+ private void clear()
+ {
+ layoutData.clear();
+ setHeaderComment(null);
+ }
+
+ /**
+ * Tests whether a line is a comment, i.e. whether it starts with a comment
+ * character.
+ *
+ * @param line the line
+ * @return a flag if this is a comment line
+ */
+ static boolean isCommentLine(String line)
+ {
+ return PropertiesConfiguration.isCommentLine(line);
+ }
+
+ /**
+ * Trims a comment. This method either removes all comment characters from
+ * the given string, leaving only the plain comment text or ensures that
+ * every line starts with a valid comment character.
+ *
+ * @param s the string to be processed
+ * @param comment if <b>true</b>, a comment character will always be
+ * enforced; if <b>false</b>, it will be removed
+ * @return the trimmed comment
+ */
+ static String trimComment(String s, boolean comment)
+ {
+ StringBuilder buf = new StringBuilder(s.length());
+ int lastPos = 0;
+ int pos;
+
+ do
+ {
+ pos = s.indexOf(CR, lastPos);
+ if (pos >= 0)
+ {
+ String line = s.substring(lastPos, pos);
+ buf.append(stripCommentChar(line, comment)).append(CR);
+ lastPos = pos + CR.length();
+ }
+ } while (pos >= 0);
+
+ if (lastPos < s.length())
+ {
+ buf.append(stripCommentChar(s.substring(lastPos), comment));
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Either removes the comment character from the given comment line or
+ * ensures that the line starts with a comment character.
+ *
+ * @param s the comment line
+ * @param comment if <b>true</b>, a comment character will always be
+ * enforced; if <b>false</b>, it will be removed
+ * @return the line without comment character
+ */
+ static String stripCommentChar(String s, boolean comment)
+ {
+ if (s.length() < 1 || (isCommentLine(s) == comment))
+ {
+ return s;
+ }
+
+ else
+ {
+ if (!comment)
+ {
+ int pos = 0;
+ // find first comment character
+ while (PropertiesConfiguration.COMMENT_CHARS.indexOf(s
+ .charAt(pos)) < 0)
+ {
+ pos++;
+ }
+
+ // Remove leading spaces
+ pos++;
+ while (pos < s.length()
+ && Character.isWhitespace(s.charAt(pos)))
+ {
+ pos++;
+ }
+
+ return (pos < s.length()) ? s.substring(pos)
+ : StringUtils.EMPTY;
+ }
+ else
+ {
+ return COMMENT_PREFIX + s;
+ }
+ }
+ }
+
+ /**
+ * Extracts a comment string from the given range of the specified comment
+ * lines. The single lines are added using a line feed as separator.
+ *
+ * @param commentLines a list with comment lines
+ * @param from the start index
+ * @param to the end index (inclusive)
+ * @return the comment string (<b>null</b> if it is undefined)
+ */
+ private String extractComment(List<String> commentLines, int from, int to)
+ {
+ if (to < from)
+ {
+ return null;
+ }
+
+ else
+ {
+ StringBuilder buf = new StringBuilder(commentLines.get(from));
+ for (int i = from + 1; i <= to; i++)
+ {
+ buf.append(CR);
+ buf.append(commentLines.get(i));
+ }
+ return buf.toString();
+ }
+ }
+
+ /**
+ * Checks if parts of the passed in comment can be used as header comment.
+ * This method checks whether a header comment can be defined (i.e. whether
+ * this is the first comment in the loaded file). If this is the case, it is
+ * searched for the latest blanc line. This line will mark the end of the
+ * header comment. The return value is the index of the first line in the
+ * passed in list, which does not belong to the header comment.
+ *
+ * @param commentLines the comment lines
+ * @return the index of the next line after the header comment
+ */
+ private int checkHeaderComment(List<String> commentLines)
+ {
+ if (loadCounter == 1 && getHeaderComment() == null
+ && layoutData.isEmpty())
+ {
+ // This is the first comment. Search for blanc lines.
+ int index = commentLines.size() - 1;
+ while (index >= 0
+ && commentLines.get(index).length() > 0)
+ {
+ index--;
+ }
+ setHeaderComment(extractComment(commentLines, 0, index - 1));
+ return index + 1;
+ }
+ else
+ {
+ return 0;
+ }
+ }
+
+ /**
+ * Copies the data from the given layout object.
+ *
+ * @param c the layout object to copy
+ */
+ private void copyFrom(PropertiesConfigurationLayout c)
+ {
+ for (String key : c.getKeys())
+ {
+ PropertyLayoutData data = c.layoutData.get(key);
+ layoutData.put(key, data.clone());
+ }
+
+ setHeaderComment(c.getHeaderComment());
+ setFooterComment(c.getFooterComment());
+ }
+
+ /**
+ * Helper method for writing a comment line. This method ensures that the
+ * correct line separator is used if the comment spans multiple lines.
+ *
+ * @param writer the writer
+ * @param comment the comment to write
+ * @throws IOException if an IO error occurs
+ */
+ private static void writeComment(
+ PropertiesConfiguration.PropertiesWriter writer, String comment)
+ throws IOException
+ {
+ if (comment != null)
+ {
+ writer.writeln(StringUtils.replace(comment, CR, writer
+ .getLineSeparator()));
+ }
+ }
+
+ /**
+ * Helper method for generating a comment string. Depending on the boolean
+ * argument the resulting string either has no comment characters or a
+ * leading comment character at each line.
+ *
+ * @param comment the comment string to be processed
+ * @param commentChar determines the presence of comment characters
+ * @return the canonical comment string (can be <b>null</b>)
+ */
+ private static String constructCanonicalComment(String comment,
+ boolean commentChar)
+ {
+ return (comment == null) ? null : trimComment(comment, commentChar);
+ }
+
+ /**
+ * A helper class for storing all layout related information for a
+ * configuration property.
+ */
+ static class PropertyLayoutData implements Cloneable
+ {
+ /** Stores the comment for the property. */
+ private StringBuffer comment;
+
+ /** The separator to be used for this property. */
+ private String separator;
+
+ /** Stores the number of blanc lines before this property. */
+ private int blancLines;
+
+ /** Stores the single line property. */
+ private boolean singleLine;
+
+ /**
+ * Creates a new instance of {@code PropertyLayoutData}.
+ */
+ public PropertyLayoutData()
+ {
+ singleLine = true;
+ separator = PropertiesConfiguration.DEFAULT_SEPARATOR;
+ }
+
+ /**
+ * Returns the number of blanc lines before this property.
+ *
+ * @return the number of blanc lines before this property
+ */
+ public int getBlancLines()
+ {
+ return blancLines;
+ }
+
+ /**
+ * Sets the number of properties before this property.
+ *
+ * @param blancLines the number of properties before this property
+ */
+ public void setBlancLines(int blancLines)
+ {
+ this.blancLines = blancLines;
+ }
+
+ /**
+ * Returns the single line flag.
+ *
+ * @return the single line flag
+ */
+ public boolean isSingleLine()
+ {
+ return singleLine;
+ }
+
+ /**
+ * Sets the single line flag.
+ *
+ * @param singleLine the single line flag
+ */
+ public void setSingleLine(boolean singleLine)
+ {
+ this.singleLine = singleLine;
+ }
+
+ /**
+ * Adds a comment for this property. If already a comment exists, the
+ * new comment is added (separated by a newline).
+ *
+ * @param s the comment to add
+ */
+ public void addComment(String s)
+ {
+ if (s != null)
+ {
+ if (comment == null)
+ {
+ comment = new StringBuffer(s);
+ }
+ else
+ {
+ comment.append(CR).append(s);
+ }
+ }
+ }
+
+ /**
+ * Sets the comment for this property.
+ *
+ * @param s the new comment (can be <b>null</b>)
+ */
+ public void setComment(String s)
+ {
+ if (s == null)
+ {
+ comment = null;
+ }
+ else
+ {
+ comment = new StringBuffer(s);
+ }
+ }
+
+ /**
+ * Returns the comment for this property. The comment is returned as it
+ * is, without processing of comment characters.
+ *
+ * @return the comment (can be <b>null</b>)
+ */
+ public String getComment()
+ {
+ return (comment == null) ? null : comment.toString();
+ }
+
+ /**
+ * Returns the separator that was used for this property.
+ *
+ * @return the property separator
+ */
+ public String getSeparator()
+ {
+ return separator;
+ }
+
+ /**
+ * Sets the separator to be used for the represented property.
+ *
+ * @param separator the property separator
+ */
+ public void setSeparator(String separator)
+ {
+ this.separator = separator;
+ }
+
+ /**
+ * Creates a copy of this object.
+ *
+ * @return the copy
+ */
+ @Override
+ public PropertyLayoutData clone()
+ {
+ try
+ {
+ PropertyLayoutData copy = (PropertyLayoutData) super.clone();
+ if (comment != null)
+ {
+ // must copy string buffer, too
+ copy.comment = new StringBuffer(getComment());
+ }
+ return copy;
+ }
+ catch (CloneNotSupportedException cnex)
+ {
+ // This cannot happen!
+ throw new ConfigurationRuntimeException(cnex);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/PropertyConverter.java b/src/main/java/org/apache/commons/configuration/PropertyConverter.java
new file mode 100644
index 0000000..d3a4030
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/PropertyConverter.java
@@ -0,0 +1,1062 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.awt.Color;
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+
+import org.apache.commons.lang.BooleanUtils;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * A utility class to convert the configuration properties into any type.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: PropertyConverter.java 1534376 2013-10-21 21:14:18Z henning $
+ * @since 1.1
+ */
+public final class PropertyConverter
+{
+ /** Constant for the list delimiter as char.*/
+ static final char LIST_ESC_CHAR = '\\';
+
+ /** Constant for the list delimiter escaping character as string.*/
+ static final String LIST_ESCAPE = String.valueOf(LIST_ESC_CHAR);
+
+ /** Constant for the prefix of hex numbers.*/
+ private static final String HEX_PREFIX = "0x";
+
+ /** Constant for the radix of hex numbers.*/
+ private static final int HEX_RADIX = 16;
+
+ /** Constant for the prefix of binary numbers.*/
+ private static final String BIN_PREFIX = "0b";
+
+ /** Constant for the radix of binary numbers.*/
+ private static final int BIN_RADIX = 2;
+
+ /** Constant for the argument classes of the Number constructor that takes a String. */
+ private static final Class<?>[] CONSTR_ARGS = {String.class};
+
+ /** The fully qualified name of {@link javax.mail.internet.InternetAddress} */
+ private static final String INTERNET_ADDRESS_CLASSNAME = "javax.mail.internet.InternetAddress";
+
+ /**
+ * Private constructor prevents instances from being created.
+ */
+ private PropertyConverter()
+ {
+ // to prevent instantiation...
+ }
+
+ /**
+ * Converts the specified value to the target class. If the class is a
+ * primitive type (Integer.TYPE, Boolean.TYPE, etc) the value returned
+ * will use the wrapper type (Integer.class, Boolean.class, etc).
+ *
+ * @param cls the target class of the converted value
+ * @param value the value to convert
+ * @param params optional parameters used for the conversion
+ * @return the converted value
+ * @throws ConversionException if the value is not compatible with the requested type
+ *
+ * @since 1.5
+ */
+ static Object to(Class<?> cls, Object value, Object[] params) throws ConversionException
+ {
+ if (cls.isInstance(value))
+ {
+ return value; // no conversion needed
+ }
+
+ if (Boolean.class.equals(cls) || Boolean.TYPE.equals(cls))
+ {
+ return toBoolean(value);
+ }
+ else if (Character.class.equals(cls) || Character.TYPE.equals(cls))
+ {
+ return toCharacter(value);
+ }
+ else if (Number.class.isAssignableFrom(cls) || cls.isPrimitive())
+ {
+ if (Integer.class.equals(cls) || Integer.TYPE.equals(cls))
+ {
+ return toInteger(value);
+ }
+ else if (Long.class.equals(cls) || Long.TYPE.equals(cls))
+ {
+ return toLong(value);
+ }
+ else if (Byte.class.equals(cls) || Byte.TYPE.equals(cls))
+ {
+ return toByte(value);
+ }
+ else if (Short.class.equals(cls) || Short.TYPE.equals(cls))
+ {
+ return toShort(value);
+ }
+ else if (Float.class.equals(cls) || Float.TYPE.equals(cls))
+ {
+ return toFloat(value);
+ }
+ else if (Double.class.equals(cls) || Double.TYPE.equals(cls))
+ {
+ return toDouble(value);
+ }
+ else if (BigInteger.class.equals(cls))
+ {
+ return toBigInteger(value);
+ }
+ else if (BigDecimal.class.equals(cls))
+ {
+ return toBigDecimal(value);
+ }
+ }
+ else if (Date.class.equals(cls))
+ {
+ return toDate(value, (String) params[0]);
+ }
+ else if (Calendar.class.equals(cls))
+ {
+ return toCalendar(value, (String) params[0]);
+ }
+ else if (URL.class.equals(cls))
+ {
+ return toURL(value);
+ }
+ else if (Locale.class.equals(cls))
+ {
+ return toLocale(value);
+ }
+ else if (isEnum(cls))
+ {
+ return convertToEnum(cls, value);
+ }
+ else if (Color.class.equals(cls))
+ {
+ return toColor(value);
+ }
+ else if (cls.getName().equals(INTERNET_ADDRESS_CLASSNAME))
+ {
+ return toInternetAddress(value);
+ }
+ else if (InetAddress.class.isAssignableFrom(cls))
+ {
+ return toInetAddress(value);
+ }
+
+ throw new ConversionException("The value '" + value + "' (" + value.getClass() + ")"
+ + " can't be converted to a " + cls.getName() + " object");
+ }
+
+ /**
+ * Convert the specified object into a Boolean. Internally the
+ * {@code org.apache.commons.lang.BooleanUtils} class from the
+ * <a href="http://commons.apache.org/lang/">Commons Lang</a>
+ * project is used to perform this conversion. This class accepts some more
+ * tokens for the boolean value of <b>true</b>, e.g. {@code yes} and
+ * {@code on}. Please refer to the documentation of this class for more
+ * details.
+ *
+ * @param value the value to convert
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to a boolean
+ */
+ public static Boolean toBoolean(Object value) throws ConversionException
+ {
+ if (value instanceof Boolean)
+ {
+ return (Boolean) value;
+ }
+ else if (value instanceof String)
+ {
+ Boolean b = BooleanUtils.toBooleanObject((String) value);
+ if (b == null)
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a Boolean object");
+ }
+ return b;
+ }
+ else
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a Boolean object");
+ }
+ }
+
+ /**
+ * Converts the specified value object to a {@code Character}. This method
+ * converts the passed in object to a string. If the string has exactly one
+ * character, this character is returned as result. Otherwise, conversion
+ * fails.
+ *
+ * @param value the value to be converted
+ * @return the resulting {@code Character} object
+ * @throws ConversionException if the conversion is not possible
+ */
+ public static Character toCharacter(Object value) throws ConversionException
+ {
+ String strValue = String.valueOf(value);
+ if (strValue.length() == 1)
+ {
+ return Character.valueOf(strValue.charAt(0));
+ }
+ else
+ {
+ throw new ConversionException(
+ String.format(
+ "The value '%s' cannot be converted to a Character object!",
+ strValue));
+ }
+ }
+
+ /**
+ * Convert the specified object into a Byte.
+ *
+ * @param value the value to convert
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to a byte
+ */
+ public static Byte toByte(Object value) throws ConversionException
+ {
+ Number n = toNumber(value, Byte.class);
+ if (n instanceof Byte)
+ {
+ return (Byte) n;
+ }
+ else
+ {
+ return new Byte(n.byteValue());
+ }
+ }
+
+ /**
+ * Convert the specified object into a Short.
+ *
+ * @param value the value to convert
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to a short
+ */
+ public static Short toShort(Object value) throws ConversionException
+ {
+ Number n = toNumber(value, Short.class);
+ if (n instanceof Short)
+ {
+ return (Short) n;
+ }
+ else
+ {
+ return new Short(n.shortValue());
+ }
+ }
+
+ /**
+ * Convert the specified object into an Integer.
+ *
+ * @param value the value to convert
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to an integer
+ */
+ public static Integer toInteger(Object value) throws ConversionException
+ {
+ Number n = toNumber(value, Integer.class);
+ if (n instanceof Integer)
+ {
+ return (Integer) n;
+ }
+ else
+ {
+ return new Integer(n.intValue());
+ }
+ }
+
+ /**
+ * Convert the specified object into a Long.
+ *
+ * @param value the value to convert
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to a Long
+ */
+ public static Long toLong(Object value) throws ConversionException
+ {
+ Number n = toNumber(value, Long.class);
+ if (n instanceof Long)
+ {
+ return (Long) n;
+ }
+ else
+ {
+ return new Long(n.longValue());
+ }
+ }
+
+ /**
+ * Convert the specified object into a Float.
+ *
+ * @param value the value to convert
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to a Float
+ */
+ public static Float toFloat(Object value) throws ConversionException
+ {
+ Number n = toNumber(value, Float.class);
+ if (n instanceof Float)
+ {
+ return (Float) n;
+ }
+ else
+ {
+ return new Float(n.floatValue());
+ }
+ }
+
+ /**
+ * Convert the specified object into a Double.
+ *
+ * @param value the value to convert
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to a Double
+ */
+ public static Double toDouble(Object value) throws ConversionException
+ {
+ Number n = toNumber(value, Double.class);
+ if (n instanceof Double)
+ {
+ return (Double) n;
+ }
+ else
+ {
+ return new Double(n.doubleValue());
+ }
+ }
+
+ /**
+ * Convert the specified object into a BigInteger.
+ *
+ * @param value the value to convert
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to a BigInteger
+ */
+ public static BigInteger toBigInteger(Object value) throws ConversionException
+ {
+ Number n = toNumber(value, BigInteger.class);
+ if (n instanceof BigInteger)
+ {
+ return (BigInteger) n;
+ }
+ else
+ {
+ return BigInteger.valueOf(n.longValue());
+ }
+ }
+
+ /**
+ * Convert the specified object into a BigDecimal.
+ *
+ * @param value the value to convert
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to a BigDecimal
+ */
+ public static BigDecimal toBigDecimal(Object value) throws ConversionException
+ {
+ Number n = toNumber(value, BigDecimal.class);
+ if (n instanceof BigDecimal)
+ {
+ return (BigDecimal) n;
+ }
+ else
+ {
+ return new BigDecimal(n.doubleValue());
+ }
+ }
+
+ /**
+ * Tries to convert the specified object into a number object. This method
+ * is used by the conversion methods for number types. Note that the return
+ * value is not in always of the specified target class, but only if a new
+ * object has to be created.
+ *
+ * @param value the value to be converted (must not be <b>null</b>)
+ * @param targetClass the target class of the conversion (must be derived
+ * from {@code java.lang.Number})
+ * @return the converted number
+ * @throws ConversionException if the object cannot be converted
+ */
+ static Number toNumber(Object value, Class<?> targetClass) throws ConversionException
+ {
+ if (value instanceof Number)
+ {
+ return (Number) value;
+ }
+ else
+ {
+ String str = value.toString();
+ if (str.startsWith(HEX_PREFIX))
+ {
+ try
+ {
+ return new BigInteger(str.substring(HEX_PREFIX.length()), HEX_RADIX);
+ }
+ catch (NumberFormatException nex)
+ {
+ throw new ConversionException("Could not convert " + str
+ + " to " + targetClass.getName()
+ + "! Invalid hex number.", nex);
+ }
+ }
+
+ if (str.startsWith(BIN_PREFIX))
+ {
+ try
+ {
+ return new BigInteger(str.substring(BIN_PREFIX.length()), BIN_RADIX);
+ }
+ catch (NumberFormatException nex)
+ {
+ throw new ConversionException("Could not convert " + str
+ + " to " + targetClass.getName()
+ + "! Invalid binary number.", nex);
+ }
+ }
+
+ try
+ {
+ Constructor<?> constr = targetClass.getConstructor(CONSTR_ARGS);
+ return (Number) constr.newInstance(new Object[]{str});
+ }
+ catch (InvocationTargetException itex)
+ {
+ throw new ConversionException("Could not convert " + str
+ + " to " + targetClass.getName(), itex
+ .getTargetException());
+ }
+ catch (Exception ex)
+ {
+ // Treat all possible exceptions the same way
+ throw new ConversionException(
+ "Conversion error when trying to convert " + str
+ + " to " + targetClass.getName(), ex);
+ }
+ }
+ }
+
+ /**
+ * Convert the specified object into an URL.
+ *
+ * @param value the value to convert
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to an URL
+ */
+ public static URL toURL(Object value) throws ConversionException
+ {
+ if (value instanceof URL)
+ {
+ return (URL) value;
+ }
+ else if (value instanceof String)
+ {
+ try
+ {
+ return new URL((String) value);
+ }
+ catch (MalformedURLException e)
+ {
+ throw new ConversionException("The value " + value + " can't be converted to an URL", e);
+ }
+ }
+ else
+ {
+ throw new ConversionException("The value " + value + " can't be converted to an URL");
+ }
+ }
+
+ /**
+ * Convert the specified object into a Locale.
+ *
+ * @param value the value to convert
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to a Locale
+ */
+ public static Locale toLocale(Object value) throws ConversionException
+ {
+ if (value instanceof Locale)
+ {
+ return (Locale) value;
+ }
+ else if (value instanceof String)
+ {
+ List<String> elements = split((String) value, '_');
+ int size = elements.size();
+
+ if (size >= 1 && ((elements.get(0)).length() == 2 || (elements.get(0)).length() == 0))
+ {
+ String language = elements.get(0);
+ String country = (size >= 2) ? elements.get(1) : "";
+ String variant = (size >= 3) ? elements.get(2) : "";
+
+ return new Locale(language, country, variant);
+ }
+ else
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a Locale");
+ }
+ }
+ else
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a Locale");
+ }
+ }
+
+ /**
+ * Split a string on the specified delimiter. To be removed when
+ * commons-lang has a better replacement available (Tokenizer?).
+ *
+ * todo: replace with a commons-lang equivalent
+ *
+ * @param s the string to split
+ * @param delimiter the delimiter
+ * @param trim a flag whether the single elements should be trimmed
+ * @return a list with the single tokens
+ */
+ public static List<String> split(String s, char delimiter, boolean trim)
+ {
+ if (s == null)
+ {
+ return new ArrayList<String>();
+ }
+
+ List<String> list = new ArrayList<String>();
+
+ StringBuilder token = new StringBuilder();
+ int begin = 0;
+ boolean inEscape = false;
+
+ while (begin < s.length())
+ {
+ char c = s.charAt(begin);
+ if (inEscape)
+ {
+ // last character was the escape marker
+ // can current character be escaped?
+ if (c != delimiter && c != LIST_ESC_CHAR)
+ {
+ // no, also add escape character
+ token.append(LIST_ESC_CHAR);
+ }
+ token.append(c);
+ inEscape = false;
+ }
+
+ else
+ {
+ if (c == delimiter)
+ {
+ // found a list delimiter -> add token and resetDefaultFileSystem buffer
+ String t = token.toString();
+ if (trim)
+ {
+ t = t.trim();
+ }
+ list.add(t);
+ token = new StringBuilder();
+ }
+ else if (c == LIST_ESC_CHAR)
+ {
+ // eventually escape next character
+ inEscape = true;
+ }
+ else
+ {
+ token.append(c);
+ }
+ }
+
+ begin++;
+ }
+
+ // Trailing delimiter?
+ if (inEscape)
+ {
+ token.append(LIST_ESC_CHAR);
+ }
+ // Add last token
+ String t = token.toString();
+ if (trim)
+ {
+ t = t.trim();
+ }
+ list.add(t);
+
+ return list;
+ }
+
+ /**
+ * Split a string on the specified delimiter always trimming the elements.
+ * This is a shortcut for {@code split(s, delimiter, true)}.
+ *
+ * @param s the string to split
+ * @param delimiter the delimiter
+ * @return a list with the single tokens
+ */
+ public static List<String> split(String s, char delimiter)
+ {
+ return split(s, delimiter, true);
+ }
+
+ /**
+ * Escapes the delimiters that might be contained in the given string. This
+ * method works like {@link #escapeListDelimiter(String, char)}. In addition,
+ * a single backslash will also be escaped.
+ *
+ * @param s the string with the value
+ * @param delimiter the list delimiter to use
+ * @return the correctly escaped string
+ */
+ public static String escapeDelimiters(String s, char delimiter)
+ {
+ String s1 = StringUtils.replace(s, LIST_ESCAPE, LIST_ESCAPE + LIST_ESCAPE);
+ return escapeListDelimiter(s1, delimiter);
+ }
+
+ /**
+ * Escapes the list delimiter if it is contained in the given string. This
+ * method ensures that list delimiter characters that are part of a
+ * property's value are correctly escaped when a configuration is saved to a
+ * file. Otherwise when loaded again the property will be treated as a list
+ * property.
+ *
+ * @param s the string with the value
+ * @param delimiter the list delimiter to use
+ * @return the escaped string
+ * @since 1.7
+ */
+ public static String escapeListDelimiter(String s, char delimiter)
+ {
+ return StringUtils.replace(s, String.valueOf(delimiter), LIST_ESCAPE
+ + delimiter);
+ }
+
+ /**
+ * Convert the specified object into a Color. If the value is a String,
+ * the format allowed is (#)?[0-9A-F]{6}([0-9A-F]{2})?. Examples:
+ * <ul>
+ * <li>FF0000 (red)</li>
+ * <li>0000FFA0 (semi transparent blue)</li>
+ * <li>#CCCCCC (gray)</li>
+ * <li>#00FF00A0 (semi transparent green)</li>
+ * </ul>
+ *
+ * @param value the value to convert
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to a Color
+ */
+ public static Color toColor(Object value) throws ConversionException
+ {
+ if (value instanceof Color)
+ {
+ return (Color) value;
+ }
+ else if (value instanceof String && !StringUtils.isBlank((String) value))
+ {
+ String color = ((String) value).trim();
+
+ int[] components = new int[3];
+
+ // check the size of the string
+ int minlength = components.length * 2;
+ if (color.length() < minlength)
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a Color");
+ }
+
+ // remove the leading #
+ if (color.startsWith("#"))
+ {
+ color = color.substring(1);
+ }
+
+ try
+ {
+ // parse the components
+ for (int i = 0; i < components.length; i++)
+ {
+ components[i] = Integer.parseInt(color.substring(2 * i, 2 * i + 2), HEX_RADIX);
+ }
+
+ // parse the transparency
+ int alpha;
+ if (color.length() >= minlength + 2)
+ {
+ alpha = Integer.parseInt(color.substring(minlength, minlength + 2), HEX_RADIX);
+ }
+ else
+ {
+ alpha = Color.black.getAlpha();
+ }
+
+ return new Color(components[0], components[1], components[2], alpha);
+ }
+ catch (Exception e)
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a Color", e);
+ }
+ }
+ else
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a Color");
+ }
+ }
+
+ /**
+ * Convert the specified value into an internet address.
+ *
+ * @param value the value to convert
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to a InetAddress
+ *
+ * @since 1.5
+ */
+ static InetAddress toInetAddress(Object value) throws ConversionException
+ {
+ if (value instanceof InetAddress)
+ {
+ return (InetAddress) value;
+ }
+ else if (value instanceof String)
+ {
+ try
+ {
+ return InetAddress.getByName((String) value);
+ }
+ catch (UnknownHostException e)
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a InetAddress", e);
+ }
+ }
+ else
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a InetAddress");
+ }
+ }
+
+ /**
+ * Convert the specified value into an email address.
+ *
+ * @param value the value to convert
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to an email address
+ *
+ * @since 1.5
+ */
+ static Object toInternetAddress(Object value) throws ConversionException
+ {
+ if (value.getClass().getName().equals(INTERNET_ADDRESS_CLASSNAME))
+ {
+ return value;
+ }
+ else if (value instanceof String)
+ {
+ try
+ {
+ Constructor<?> ctor = Class.forName(INTERNET_ADDRESS_CLASSNAME)
+ .getConstructor(new Class[] {String.class});
+ return ctor.newInstance(new Object[] {value});
+ }
+ catch (Exception e)
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a InternetAddress", e);
+ }
+ }
+ else
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a InternetAddress");
+ }
+ }
+
+ /**
+ * Calls Class.isEnum() on Java 5, returns false on older JRE.
+ */
+ static boolean isEnum(Class<?> cls)
+ {
+ return cls.isEnum();
+ }
+
+ /**
+ * Convert the specified value into a Java 5 enum.
+ *
+ * @param value the value to convert
+ * @param cls the type of the enumeration
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to an enumeration
+ *
+ * @since 1.5
+ */
+ static <E extends Enum<E>> E toEnum(Object value, Class<E> cls) throws ConversionException
+ {
+ if (value.getClass().equals(cls))
+ {
+ return cls.cast(value);
+ }
+ else if (value instanceof String)
+ {
+ try
+ {
+ return Enum.valueOf(cls, (String) value);
+ }
+ catch (Exception e)
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a " + cls.getName());
+ }
+ }
+ else if (value instanceof Number)
+ {
+ try
+ {
+ E[] enumConstants = cls.getEnumConstants();
+ return enumConstants[((Number) value).intValue()];
+ }
+ catch (Exception e)
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a " + cls.getName());
+ }
+ }
+ else
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a " + cls.getName());
+ }
+ }
+
+ /**
+ * Convert the specified object into a Date.
+ *
+ * @param value the value to convert
+ * @param format the DateFormat pattern to parse String values
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to a Calendar
+ */
+ public static Date toDate(Object value, String format) throws ConversionException
+ {
+ if (value instanceof Date)
+ {
+ return (Date) value;
+ }
+ else if (value instanceof Calendar)
+ {
+ return ((Calendar) value).getTime();
+ }
+ else if (value instanceof String)
+ {
+ try
+ {
+ return new SimpleDateFormat(format).parse((String) value);
+ }
+ catch (ParseException e)
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a Date", e);
+ }
+ }
+ else
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a Date");
+ }
+ }
+
+ /**
+ * Convert the specified object into a Calendar.
+ *
+ * @param value the value to convert
+ * @param format the DateFormat pattern to parse String values
+ * @return the converted value
+ * @throws ConversionException thrown if the value cannot be converted to a Calendar
+ */
+ public static Calendar toCalendar(Object value, String format) throws ConversionException
+ {
+ if (value instanceof Calendar)
+ {
+ return (Calendar) value;
+ }
+ else if (value instanceof Date)
+ {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime((Date) value);
+ return calendar;
+ }
+ else if (value instanceof String)
+ {
+ try
+ {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(new SimpleDateFormat(format).parse((String) value));
+ return calendar;
+ }
+ catch (ParseException e)
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a Calendar", e);
+ }
+ }
+ else
+ {
+ throw new ConversionException("The value " + value + " can't be converted to a Calendar");
+ }
+ }
+
+ /**
+ * Returns an iterator over the simple values of a composite value. This
+ * implementation calls {@link #flatten(Object, char)} and
+ * returns an iterator over the returned collection.
+ *
+ * @param value the value to "split"
+ * @param delimiter the delimiter for String values
+ * @return an iterator for accessing the single values
+ */
+ public static Iterator<?> toIterator(Object value, char delimiter)
+ {
+ return flatten(value, delimiter).iterator();
+ }
+
+ /**
+ * Returns a collection with all values contained in the specified object.
+ * This method is used for instance by the {@code addProperty()}
+ * implementation of the default configurations to gather all values of the
+ * property to add. Depending on the type of the passed in object the
+ * following things happen:
+ * <ul>
+ * <li>Strings are checked for delimiter characters and split if necessary.</li>
+ * <li>For objects implementing the {@code Iterable} interface, the
+ * corresponding {@code Iterator} is obtained, and contained elements
+ * are added to the resulting collection.</li>
+ * <li>Arrays are treated as {@code Iterable} objects.</li>
+ * <li>All other types are directly inserted.</li>
+ * <li>Recursive combinations are supported, e.g. a collection containing
+ * an array that contains strings: The resulting collection will only
+ * contain primitive objects (hence the name "flatten").</li>
+ * </ul>
+ *
+ * @param value the value to be processed
+ * @param delimiter the delimiter for String values
+ * @return a "flat" collection containing all primitive values of
+ * the passed in object
+ */
+ private static Collection<?> flatten(Object value, char delimiter)
+ {
+ if (value instanceof String)
+ {
+ String s = (String) value;
+ if (s.indexOf(delimiter) > 0)
+ {
+ return split(s, delimiter);
+ }
+ }
+
+ Collection<Object> result = new LinkedList<Object>();
+ if (value instanceof Iterable)
+ {
+ flattenIterator(result, ((Iterable<?>) value).iterator(), delimiter);
+ }
+ else if (value instanceof Iterator)
+ {
+ flattenIterator(result, (Iterator<?>) value, delimiter);
+ }
+ else if (value != null)
+ {
+ if (value.getClass().isArray())
+ {
+ for (int len = Array.getLength(value), idx = 0; idx < len; idx++)
+ {
+ result.addAll(flatten(Array.get(value, idx), delimiter));
+ }
+ }
+ else
+ {
+ result.add(value);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Flattens the given iterator. For each element in the iteration
+ * {@code flatten()} will be called recursively.
+ *
+ * @param target the target collection
+ * @param it the iterator to process
+ * @param delimiter the delimiter for String values
+ */
+ private static void flattenIterator(Collection<Object> target, Iterator<?> it, char delimiter)
+ {
+ while (it.hasNext())
+ {
+ target.addAll(flatten(it.next(), delimiter));
+ }
+ }
+
+ /**
+ * Performs interpolation of the specified value. This method checks if the
+ * given value contains variables of the form <code>${...}</code>. If
+ * this is the case, all occurrences will be substituted by their current
+ * values.
+ *
+ * @param value the value to be interpolated
+ * @param config the current configuration object
+ * @return the interpolated value
+ */
+ public static Object interpolate(Object value, AbstractConfiguration config)
+ {
+ if (value instanceof String)
+ {
+ return config.getSubstitutor().replace((String) value);
+ }
+ else
+ {
+ return value;
+ }
+ }
+
+ /**
+ * Helper method for converting a value to a constant of an enumeration
+ * class.
+ *
+ * @param enumClass the enumeration class
+ * @param value the value to be converted
+ * @return the converted value
+ */
+ @SuppressWarnings("unchecked")
+ // conversion is safe because we know that the class is an Enum class
+ private static Object convertToEnum(Class<?> enumClass, Object value)
+ {
+ return toEnum(value, enumClass.asSubclass(Enum.class));
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/StrictConfigurationComparator.java b/src/main/java/org/apache/commons/configuration/StrictConfigurationComparator.java
new file mode 100644
index 0000000..472c754
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/StrictConfigurationComparator.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.util.Iterator;
+
+/**
+ * Strict comparator for configurations.
+ *
+ * @since 1.0
+ *
+ * @author <a href="mailto:herve.quiroz at esil.univ-mrs.fr">Herve Quiroz</a>
+ * @author <a href="mailto:shapira at mpi.com">Yoav Shapira</a>
+ * @version $Id: StrictConfigurationComparator.java 1210177 2011-12-04 18:58:19Z oheger $
+ */
+public class StrictConfigurationComparator implements ConfigurationComparator
+{
+ /**
+ * Create a new strict comparator.
+ */
+ public StrictConfigurationComparator()
+ {
+ }
+
+ /**
+ * Compare two configuration objects.
+ *
+ * @param a the first configuration
+ * @param b the second configuration
+ * @return true if keys from a are found in b and keys from b are
+ * found in a and for each key in a, the corresponding value
+ * is the sale in for the same key in b
+ */
+ public boolean compare(Configuration a, Configuration b)
+ {
+ if (a == null && b == null)
+ {
+ return true;
+ }
+ else if (a == null || b == null)
+ {
+ return false;
+ }
+
+ for (Iterator<String> keys = a.getKeys(); keys.hasNext();)
+ {
+ String key = keys.next();
+ Object value = a.getProperty(key);
+ if (!value.equals(b.getProperty(key)))
+ {
+ return false;
+ }
+ }
+
+ for (Iterator<String> keys = b.getKeys(); keys.hasNext();)
+ {
+ String key = keys.next();
+ Object value = b.getProperty(key);
+ if (!value.equals(a.getProperty(key)))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/SubnodeConfiguration.java b/src/main/java/org/apache/commons/configuration/SubnodeConfiguration.java
new file mode 100644
index 0000000..de2cff8
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/SubnodeConfiguration.java
@@ -0,0 +1,347 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.configuration.interpol.ConfigurationInterpolator;
+import org.apache.commons.configuration.reloading.Reloadable;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+
+/**
+ * <p>
+ * A specialized hierarchical configuration class that wraps a single node of
+ * its parent configuration.
+ * </p>
+ * <p>
+ * Configurations of this type are initialized with a parent configuration and a
+ * configuration node of this configuration. This node becomes the root node of
+ * the subnode configuration. All property accessor methods are evaluated
+ * relative to this root node. A good use case for a
+ * {@code SubnodeConfiguration} is when multiple properties from a
+ * specific sub tree of the whole configuration need to be accessed. Then a
+ * {@code SubnodeConfiguration} can be created with the parent node of
+ * the affected sub tree as root node. This allows for simpler property keys and
+ * is also more efficient.
+ * </p>
+ * <p>
+ * A subnode configuration and its parent configuration operate on the same
+ * hierarchy of configuration nodes. So if modifications are performed at the
+ * subnode configuration, these changes are immediately visible in the parent
+ * configuration. Analogously will updates of the parent configuration affect
+ * the subnode configuration if the sub tree spanned by the subnode
+ * configuration's root node is involved.
+ * </p>
+ * <p>
+ * There are however changes at the parent configuration, which cause the
+ * subnode configuration to become detached. An example for such a change is a
+ * reload operation of a file-based configuration, which replaces all nodes of
+ * the parent configuration. The subnode configuration per default still
+ * references the old nodes. Another example are list structures: a subnode
+ * configuration can be created to point on the <em>i</em>th element of the
+ * list. Now list elements can be added or removed, so that the list elements'
+ * indices change. In such a scenario the subnode configuration would always
+ * point to the same list element, regardless of its current index.
+ * </p>
+ * <p>
+ * To solve these problems and make a subnode configuration aware of
+ * such structural changes of its parent, it is possible to associate a
+ * subnode configuration with a configuration key. This can be done by calling
+ * the {@code setSubnodeKey()} method. If here a key is set, the subnode
+ * configuration will evaluate it on each access, thus ensuring that it is
+ * always in sync with its parent. In this mode the subnode configuration really
+ * behaves like a live-view on its parent. The price for this is a decreased
+ * performance because now an additional evaluation has to be performed on each
+ * property access. So this mode should only be used if necessary; if for
+ * instance a subnode configuration is only used for a temporary convenient
+ * access to a complex configuration, there is no need to make it aware for
+ * structural changes of its parent. If a subnode configuration is created
+ * using the {@link HierarchicalConfiguration#configurationAt(String, boolean)
+ * configurationAt()} method of {@code HierarchicalConfiguration}
+ * (which should be the preferred way), with an additional boolean parameter it
+ * can be specified whether the resulting subnode configuration should be
+ * aware of structural changes or not. Then the configuration key will be
+ * automatically set.
+ * </p>
+ * <p>
+ * <em>Note:</em> At the moment support for creating a subnode configuration
+ * that is aware of structural changes of its parent from another subnode
+ * configuration (a "sub subnode configuration") is limited. This only works if
+ * <ol><li>the subnode configuration that serves as the parent for the new
+ * subnode configuration is itself associated with a configuration key and</li>
+ * <li>the key passed in to create the new subnode configuration is not too
+ * complex (if configuration keys are used that contain indices, a corresponding
+ * key that is valid from the parent configuration's point of view cannot be
+ * constructed).</li></ol>
+ * </p>
+ * <p>
+ * When a subnode configuration is created, it inherits the settings of its
+ * parent configuration, e.g. some flags like the
+ * {@code throwExceptionOnMissing} flag or the settings for handling list
+ * delimiters) or the expression engine. If these settings are changed later in
+ * either the subnode or the parent configuration, the changes are not visible
+ * for each other. So you could create a subnode configuration, change its
+ * expression engine without affecting the parent configuration.
+ * </p>
+ * <p>
+ * From its purpose this class is quite similar to
+ * {@link SubsetConfiguration}. The difference is that a subset
+ * configuration of a hierarchical configuration may combine multiple
+ * configuration nodes from different sub trees of the configuration, while all
+ * nodes in a subnode configuration belong to the same sub tree. If an
+ * application can live with this limitation, it is recommended to use this
+ * class instead of {@code SubsetConfiguration} because creating a subset
+ * configuration is more expensive than creating a subnode configuration.
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: SubnodeConfiguration.java 1210178 2011-12-04 18:58:51Z oheger $
+ */
+public class SubnodeConfiguration extends HierarchicalReloadableConfiguration
+{
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = 3105734147019386480L;
+
+ /** Stores the parent configuration. */
+ private HierarchicalConfiguration parent;
+
+ /** Stores the key that was used to construct this configuration.*/
+ private String subnodeKey;
+
+ /**
+ * Creates a new instance of {@code SubnodeConfiguration} and
+ * initializes it with the parent configuration and the new root node.
+ *
+ * @param parent the parent configuration
+ * @param root the root node of this subnode configuration
+ */
+ public SubnodeConfiguration(HierarchicalConfiguration parent, ConfigurationNode root)
+ {
+ super(parent instanceof Reloadable ? ((Reloadable) parent).getReloadLock() : null);
+ if (parent == null)
+ {
+ throw new IllegalArgumentException(
+ "Parent configuration must not be null!");
+ }
+ if (root == null)
+ {
+ throw new IllegalArgumentException("Root node must not be null!");
+ }
+
+ setRootNode(root);
+ this.parent = parent;
+ initFromParent(parent);
+ }
+
+ /**
+ * Returns the parent configuration of this subnode configuration.
+ *
+ * @return the parent configuration
+ */
+ public HierarchicalConfiguration getParent()
+ {
+ return parent;
+ }
+
+ /**
+ * Returns the key that was used to construct this configuration. If here a
+ * non-<b>null</b> value is returned, the subnode configuration will
+ * always check its parent for structural changes and reconstruct itself if
+ * necessary.
+ *
+ * @return the key for selecting this configuration's root node
+ * @since 1.5
+ */
+ public String getSubnodeKey()
+ {
+ return subnodeKey;
+ }
+
+ /**
+ * Sets the key to the root node of this subnode configuration. If here a
+ * key is set, the subnode configuration will behave like a live-view on its
+ * parent for this key. See the class comment for more details.
+ *
+ * @param subnodeKey the key used to construct this configuration
+ * @since 1.5
+ */
+ public void setSubnodeKey(String subnodeKey)
+ {
+ this.subnodeKey = subnodeKey;
+ }
+
+ /**
+ * Returns the root node for this configuration. If a subnode key is set,
+ * this implementation re-evaluates this key to find out if this subnode
+ * configuration needs to be reconstructed. This ensures that the subnode
+ * configuration is always synchronized with its parent configuration.
+ *
+ * @return the root node of this configuration
+ * @since 1.5
+ * @see #setSubnodeKey(String)
+ */
+ @Override
+ public ConfigurationNode getRootNode()
+ {
+ if (getSubnodeKey() != null)
+ {
+ try
+ {
+ List<ConfigurationNode> nodes = getParent().fetchNodeList(getSubnodeKey());
+ if (nodes.size() != 1)
+ {
+ // key is invalid, so detach this subnode configuration
+ setSubnodeKey(null);
+ }
+ else
+ {
+ ConfigurationNode currentRoot = nodes.get(0);
+ if (currentRoot != super.getRootNode())
+ {
+ // the root node was changed due to a change of the
+ // parent
+ fireEvent(EVENT_SUBNODE_CHANGED, null, null, true);
+ setRootNode(currentRoot);
+ fireEvent(EVENT_SUBNODE_CHANGED, null, null, false);
+ }
+ return currentRoot;
+ }
+ }
+ catch (Exception ex)
+ {
+ // Evaluation of the key caused an exception. Probably the
+ // expression engine has changed on the parent. Detach this
+ // configuration, there is not much we can do about this.
+ setSubnodeKey(null);
+ }
+ }
+
+ return super.getRootNode(); // use stored root node
+ }
+
+ /**
+ * Returns a hierarchical configuration object for the given sub node.
+ * This implementation will ensure that the returned
+ * {@code SubnodeConfiguration} object will have the same parent than
+ * this object.
+ *
+ * @param node the sub node, for which the configuration is to be created
+ * @return a hierarchical configuration for this sub node
+ */
+ @Override
+ protected SubnodeConfiguration createSubnodeConfiguration(ConfigurationNode node)
+ {
+ SubnodeConfiguration result = new SubnodeConfiguration(getParent(), node);
+ getParent().registerSubnodeConfiguration(result);
+ return result;
+ }
+
+ /**
+ * Returns a hierarchical configuration object for the given sub node that
+ * is aware of structural changes of its parent. Works like the method with
+ * the same name, but also sets the subnode key for the new subnode
+ * configuration, so it can check whether the parent has been changed. This
+ * only works if this subnode configuration has itself a valid subnode key.
+ * So if a subnode configuration that should be aware of structural changes
+ * is created from an already existing subnode configuration, this subnode
+ * configuration must also be aware of such changes.
+ *
+ * @param node the sub node, for which the configuration is to be created
+ * @param subnodeKey the construction key
+ * @return a hierarchical configuration for this sub node
+ * @since 1.5
+ */
+ @Override
+ protected SubnodeConfiguration createSubnodeConfiguration(
+ ConfigurationNode node, String subnodeKey)
+ {
+ SubnodeConfiguration result = createSubnodeConfiguration(node);
+
+ if (getSubnodeKey() != null)
+ {
+ // construct the correct subnode key
+ // determine path to root node
+ List<ConfigurationNode> lstPathToRoot = new ArrayList<ConfigurationNode>();
+ ConfigurationNode top = super.getRootNode();
+ ConfigurationNode nd = node;
+ while (nd != top)
+ {
+ lstPathToRoot.add(nd);
+ nd = nd.getParentNode();
+ }
+
+ // construct the keys for the nodes on this path
+ Collections.reverse(lstPathToRoot);
+ String key = getSubnodeKey();
+ for (ConfigurationNode pathNode : lstPathToRoot)
+ {
+ key = getParent().getExpressionEngine().nodeKey(pathNode, key);
+ }
+ result.setSubnodeKey(key);
+ }
+
+ return result;
+ }
+
+ /**
+ * Creates a new node. This task is delegated to the parent.
+ *
+ * @param name the node's name
+ * @return the new node
+ */
+ @Override
+ protected Node createNode(String name)
+ {
+ return getParent().createNode(name);
+ }
+
+ /**
+ * Initializes this subnode configuration from the given parent
+ * configuration. This method is called by the constructor. It will copy
+ * many settings from the parent.
+ *
+ * @param parentConfig the parent configuration
+ */
+ protected void initFromParent(HierarchicalConfiguration parentConfig)
+ {
+ setExpressionEngine(parentConfig.getExpressionEngine());
+ setListDelimiter(parentConfig.getListDelimiter());
+ setDelimiterParsingDisabled(parentConfig.isDelimiterParsingDisabled());
+ setThrowExceptionOnMissing(parentConfig.isThrowExceptionOnMissing());
+ }
+
+ /**
+ * Creates a ConfigurationInterpolator with a chain to the parent's
+ * interpolator.
+ *
+ * @return the new interpolator
+ */
+ @Override
+ protected ConfigurationInterpolator createInterpolator()
+ {
+ ConfigurationInterpolator interpolator = super.createInterpolator();
+ interpolator.setParentInterpolator(getParent().getInterpolator());
+ return interpolator;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/SubsetConfiguration.java b/src/main/java/org/apache/commons/configuration/SubsetConfiguration.java
new file mode 100644
index 0000000..62f943d
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/SubsetConfiguration.java
@@ -0,0 +1,386 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.util.Iterator;
+
+import org.apache.commons.configuration.interpol.ConfigurationInterpolator;
+
+/**
+ * <p>A subset of another configuration. The new Configuration object contains
+ * every key from the parent Configuration that starts with prefix. The prefix
+ * is removed from the keys in the subset.</p>
+ * <p>It is usually not necessary to use this class directly. Instead the
+ * {@link Configuration#subset(String)} method should be used,
+ * which will return a correctly initialized instance.</p>
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: SubsetConfiguration.java 1210202 2011-12-04 20:30:46Z oheger $
+ */
+public class SubsetConfiguration extends AbstractConfiguration
+{
+ /** The parent configuration. */
+ protected Configuration parent;
+
+ /** The prefix used to select the properties. */
+ protected String prefix;
+
+ /** The prefix delimiter */
+ protected String delimiter;
+
+ /**
+ * Create a subset of the specified configuration
+ *
+ * @param parent The parent configuration
+ * @param prefix The prefix used to select the properties
+ */
+ public SubsetConfiguration(Configuration parent, String prefix)
+ {
+ this.parent = parent;
+ this.prefix = prefix;
+ }
+
+ /**
+ * Create a subset of the specified configuration
+ *
+ * @param parent The parent configuration
+ * @param prefix The prefix used to select the properties
+ * @param delimiter The prefix delimiter
+ */
+ public SubsetConfiguration(Configuration parent, String prefix, String delimiter)
+ {
+ this.parent = parent;
+ this.prefix = prefix;
+ this.delimiter = delimiter;
+ }
+
+ /**
+ * Return the key in the parent configuration associated to the specified
+ * key in this subset.
+ *
+ * @param key The key in the subset.
+ * @return the key as to be used by the parent
+ */
+ protected String getParentKey(String key)
+ {
+ if ("".equals(key) || key == null)
+ {
+ return prefix;
+ }
+ else
+ {
+ return delimiter == null ? prefix + key : prefix + delimiter + key;
+ }
+ }
+
+ /**
+ * Return the key in the subset configuration associated to the specified
+ * key in the parent configuration.
+ *
+ * @param key The key in the parent configuration.
+ * @return the key in the context of this subset configuration
+ */
+ protected String getChildKey(String key)
+ {
+ if (!key.startsWith(prefix))
+ {
+ throw new IllegalArgumentException("The parent key '" + key + "' is not in the subset.");
+ }
+ else
+ {
+ String modifiedKey = null;
+ if (key.length() == prefix.length())
+ {
+ modifiedKey = "";
+ }
+ else
+ {
+ int i = prefix.length() + (delimiter != null ? delimiter.length() : 0);
+ modifiedKey = key.substring(i);
+ }
+
+ return modifiedKey;
+ }
+ }
+
+ /**
+ * Return the parent configuration for this subset.
+ *
+ * @return the parent configuration
+ */
+ public Configuration getParent()
+ {
+ return parent;
+ }
+
+ /**
+ * Return the prefix used to select the properties in the parent configuration.
+ *
+ * @return the prefix used by this subset
+ */
+ public String getPrefix()
+ {
+ return prefix;
+ }
+
+ /**
+ * Set the prefix used to select the properties in the parent configuration.
+ *
+ * @param prefix the prefix
+ */
+ public void setPrefix(String prefix)
+ {
+ this.prefix = prefix;
+ }
+
+ @Override
+ public Configuration subset(String prefix)
+ {
+ return parent.subset(getParentKey(prefix));
+ }
+
+ public boolean isEmpty()
+ {
+ return !getKeys().hasNext();
+ }
+
+ public boolean containsKey(String key)
+ {
+ return parent.containsKey(getParentKey(key));
+ }
+
+ @Override
+ public void addPropertyDirect(String key, Object value)
+ {
+ parent.addProperty(getParentKey(key), value);
+ }
+
+ @Override
+ protected void clearPropertyDirect(String key)
+ {
+ parent.clearProperty(getParentKey(key));
+ }
+
+ public Object getProperty(String key)
+ {
+ return parent.getProperty(getParentKey(key));
+ }
+
+ @Override
+ public Iterator<String> getKeys(String prefix)
+ {
+ return new SubsetIterator(parent.getKeys(getParentKey(prefix)));
+ }
+
+ public Iterator<String> getKeys()
+ {
+ return new SubsetIterator(parent.getKeys(prefix));
+ }
+
+ @Override
+ protected Object interpolate(Object base)
+ {
+ if (delimiter == null && "".equals(prefix))
+ {
+ return super.interpolate(base);
+ }
+ else
+ {
+ SubsetConfiguration config = new SubsetConfiguration(parent, "");
+ ConfigurationInterpolator interpolator = config.getInterpolator();
+ getInterpolator().registerLocalLookups(interpolator);
+ if (parent instanceof AbstractConfiguration)
+ {
+ interpolator.setParentInterpolator(((AbstractConfiguration) parent).getInterpolator());
+ }
+ return config.interpolate(base);
+ }
+ }
+
+ @Override
+ protected String interpolate(String base)
+ {
+ return super.interpolate(base);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Change the behavior of the parent configuration if it supports this feature.
+ */
+ @Override
+ public void setThrowExceptionOnMissing(boolean throwExceptionOnMissing)
+ {
+ if (parent instanceof AbstractConfiguration)
+ {
+ ((AbstractConfiguration) parent).setThrowExceptionOnMissing(throwExceptionOnMissing);
+ }
+ else
+ {
+ super.setThrowExceptionOnMissing(throwExceptionOnMissing);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * The subset inherits this feature from its parent if it supports this feature.
+ */
+ @Override
+ public boolean isThrowExceptionOnMissing()
+ {
+ if (parent instanceof AbstractConfiguration)
+ {
+ return ((AbstractConfiguration) parent).isThrowExceptionOnMissing();
+ }
+ else
+ {
+ return super.isThrowExceptionOnMissing();
+ }
+ }
+
+ /**
+ * Returns the list delimiter. This property will be fetched from the parent
+ * configuration if supported.
+ *
+ * @return the list delimiter
+ * @since 1.4
+ */
+ @Override
+ public char getListDelimiter()
+ {
+ return (parent instanceof AbstractConfiguration) ? ((AbstractConfiguration) parent)
+ .getListDelimiter()
+ : super.getListDelimiter();
+ }
+
+ /**
+ * Sets the list delimiter. If the parent configuration supports this
+ * feature, the delimiter will be set at the parent.
+ *
+ * @param delim the new list delimiter
+ * @since 1.4
+ */
+ @Override
+ public void setListDelimiter(char delim)
+ {
+ if (parent instanceof AbstractConfiguration)
+ {
+ ((AbstractConfiguration) parent).setListDelimiter(delim);
+ }
+ else
+ {
+ super.setListDelimiter(delim);
+ }
+ }
+
+ /**
+ * Returns a flag whether string properties should be checked for list
+ * delimiter characters. This implementation ensures that this flag is kept
+ * in sync with the parent configuration if this object supports this
+ * feature.
+ *
+ * @return the delimiter parsing disabled flag
+ * @since 1.4
+ */
+ @Override
+ public boolean isDelimiterParsingDisabled()
+ {
+ return (parent instanceof AbstractConfiguration) ? ((AbstractConfiguration) parent)
+ .isDelimiterParsingDisabled()
+ : super.isDelimiterParsingDisabled();
+ }
+
+ /**
+ * Sets a flag whether list parsing is disabled. This implementation will
+ * also set the flag at the parent configuration if this object supports
+ * this feature.
+ *
+ * @param delimiterParsingDisabled the delimiter parsing disabled flag
+ * @since 1.4
+ */
+ @Override
+ public void setDelimiterParsingDisabled(boolean delimiterParsingDisabled)
+ {
+ if (parent instanceof AbstractConfiguration)
+ {
+ ((AbstractConfiguration) parent)
+ .setDelimiterParsingDisabled(delimiterParsingDisabled);
+ }
+ else
+ {
+ super.setDelimiterParsingDisabled(delimiterParsingDisabled);
+ }
+ }
+
+
+ /**
+ * A specialized iterator to be returned by the {@code getKeys()}
+ * methods. This implementation wraps an iterator from the parent
+ * configuration. The keys returned by this iterator are correspondingly
+ * transformed.
+ */
+ private class SubsetIterator implements Iterator<String>
+ {
+ /** Stores the wrapped iterator. */
+ private final Iterator<String> parentIterator;
+
+ /**
+ * Creates a new instance of {@code SubsetIterator} and
+ * initializes it with the parent iterator.
+ *
+ * @param it the iterator of the parent configuration
+ */
+ public SubsetIterator(Iterator<String> it)
+ {
+ parentIterator = it;
+ }
+
+ /**
+ * Checks whether there are more elements. Delegates to the parent
+ * iterator.
+ *
+ * @return a flag whether there are more elements
+ */
+ public boolean hasNext()
+ {
+ return parentIterator.hasNext();
+ }
+
+ /**
+ * Returns the next element in the iteration. This is the next key from
+ * the parent configuration, transformed to correspond to the point of
+ * view of this subset configuration.
+ *
+ * @return the next element
+ */
+ public String next()
+ {
+ return getChildKey(parentIterator.next());
+ }
+
+ /**
+ * Removes the current element from the iteration. Delegates to the
+ * parent iterator.
+ */
+ public void remove()
+ {
+ parentIterator.remove();
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/SystemConfiguration.java b/src/main/java/org/apache/commons/configuration/SystemConfiguration.java
new file mode 100644
index 0000000..91620f5
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/SystemConfiguration.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.util.Iterator;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * A configuration based on the system properties.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: SystemConfiguration.java 1210204 2011-12-04 20:38:02Z oheger $
+ * @since 1.1
+ */
+public class SystemConfiguration extends MapConfiguration
+{
+ /** The logger. */
+ private static Log log = LogFactory.getLog(SystemConfiguration.class);
+
+ /**
+ * Create a Configuration based on the system properties.
+ *
+ * @see System#getProperties
+ */
+ public SystemConfiguration()
+ {
+ super(System.getProperties());
+ }
+
+ /**
+ * The method allows system properties to be set from a property file.
+ * @param fileName The name of the property file.
+ * @throws Exception if an error occurs.
+ * @since 1.6
+ */
+ public static void setSystemProperties(String fileName) throws Exception
+ {
+ setSystemProperties(null, fileName);
+ }
+
+ /**
+ * The method allows system properties to be set from a property file.
+ * @param basePath The base path to look for the property file.
+ * @param fileName The name of the property file.
+ * @throws Exception if an error occurs.
+ * @since 1.6
+ */
+ public static void setSystemProperties(String basePath, String fileName) throws Exception
+ {
+ PropertiesConfiguration config = fileName.endsWith(".xml")
+ ? new XMLPropertiesConfiguration() : new PropertiesConfiguration();
+ if (basePath != null)
+ {
+ config.setBasePath(basePath);
+ }
+ config.setFileName(fileName);
+ config.load();
+ setSystemProperties(config);
+ }
+
+ /**
+ * Set System properties from a configuration file.
+ * @param systemConfig The configuration containing the properties to be set.
+ * @since 1.6
+ */
+ public static void setSystemProperties(PropertiesConfiguration systemConfig)
+ {
+ Iterator<String> iter = systemConfig.getKeys();
+ while (iter.hasNext())
+ {
+ String key = iter.next();
+ String value = (String) systemConfig.getProperty(key);
+ if (log.isDebugEnabled())
+ {
+ log.debug("Setting system property " + key + " to " + value);
+ }
+ System.setProperty(key, value);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/VFSFileSystem.java b/src/main/java/org/apache/commons/configuration/VFSFileSystem.java
new file mode 100644
index 0000000..0bf9147
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/VFSFileSystem.java
@@ -0,0 +1,409 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+import java.util.Map;
+
+import org.apache.commons.vfs2.FileContent;
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemConfigBuilder;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileSystemManager;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.FileType;
+import org.apache.commons.vfs2.VFS;
+import org.apache.commons.vfs2.provider.UriParser;
+
+/**
+ * FileSystem that uses Commons VFS
+ * @since 1.7
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons Configuration team</a>
+ * @version $Id: VFSFileSystem.java 1210205 2011-12-04 20:38:19Z oheger $
+ */
+public class VFSFileSystem extends DefaultFileSystem
+{
+ public VFSFileSystem()
+ {
+ }
+
+ @Override
+ public InputStream getInputStream(String basePath, String fileName)
+ throws ConfigurationException
+ {
+ try
+ {
+ FileSystemManager manager = VFS.getManager();
+ FileName path;
+ if (basePath != null)
+ {
+ FileName base = manager.resolveURI(basePath);
+ path = manager.resolveName(base, fileName);
+ }
+ else
+ {
+ FileName file = manager.resolveURI(fileName);
+ FileName base = file.getParent();
+ path = manager.resolveName(base, file.getBaseName());
+ }
+ FileSystemOptions opts = getOptions(path.getScheme());
+ FileObject file = (opts == null) ? manager.resolveFile(path.getURI())
+ : manager.resolveFile(path.getURI(), opts);
+ FileContent content = file.getContent();
+ if (content == null)
+ {
+ String msg = "Cannot access content of " + file.getName().getFriendlyURI();
+ throw new ConfigurationException(msg);
+ }
+ return content.getInputStream();
+ }
+ catch (ConfigurationException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new ConfigurationException("Unable to load the configuration file " + fileName, e);
+ }
+ }
+
+ @Override
+ public InputStream getInputStream(URL url) throws ConfigurationException
+ {
+ FileObject file;
+ try
+ {
+ FileSystemOptions opts = getOptions(url.getProtocol());
+ file = (opts == null) ? VFS.getManager().resolveFile(url.toString())
+ : VFS.getManager().resolveFile(url.toString(), opts);
+ if (file.getType() != FileType.FILE)
+ {
+ throw new ConfigurationException("Cannot load a configuration from a directory");
+ }
+ FileContent content = file.getContent();
+ if (content == null)
+ {
+ String msg = "Cannot access content of " + file.getName().getFriendlyURI();
+ throw new ConfigurationException(msg);
+ }
+ return content.getInputStream();
+ }
+ catch (FileSystemException fse)
+ {
+ String msg = "Unable to access " + url.toString();
+ throw new ConfigurationException(msg, fse);
+ }
+ }
+
+ @Override
+ public OutputStream getOutputStream(URL url) throws ConfigurationException
+ {
+ try
+ {
+ FileSystemOptions opts = getOptions(url.getProtocol());
+ FileSystemManager fsManager = VFS.getManager();
+ FileObject file = (opts == null) ? fsManager.resolveFile(url.toString())
+ : fsManager.resolveFile(url.toString(), opts);
+ // throw an exception if the target URL is a directory
+ if (file == null || file.getType() == FileType.FOLDER)
+ {
+ throw new ConfigurationException("Cannot save a configuration to a directory");
+ }
+ FileContent content = file.getContent();
+
+ if (content == null)
+ {
+ throw new ConfigurationException("Cannot access content of " + url);
+ }
+ return content.getOutputStream();
+ }
+ catch (FileSystemException fse)
+ {
+ throw new ConfigurationException("Unable to access " + url, fse);
+ }
+ }
+
+ @Override
+ public String getPath(File file, URL url, String basePath, String fileName)
+ {
+ if (file != null)
+ {
+ return super.getPath(file, url, basePath, fileName);
+ }
+ try
+ {
+ FileSystemManager fsManager = VFS.getManager();
+ if (url != null)
+ {
+ FileName name = fsManager.resolveURI(url.toString());
+ if (name != null)
+ {
+ return name.toString();
+ }
+ }
+
+ if (UriParser.extractScheme(fileName) != null)
+ {
+ return fileName;
+ }
+ else if (basePath != null)
+ {
+ FileName base = fsManager.resolveURI(basePath);
+ return fsManager.resolveName(base, fileName).getURI();
+ }
+ else
+ {
+ FileName name = fsManager.resolveURI(fileName);
+ FileName base = name.getParent();
+ return fsManager.resolveName(base, name.getBaseName()).getURI();
+ }
+ }
+ catch (FileSystemException fse)
+ {
+ fse.printStackTrace();
+ return null;
+ }
+ }
+
+ @Override
+ public String getBasePath(String path)
+ {
+ if (UriParser.extractScheme(path) == null)
+ {
+ return super.getBasePath(path);
+ }
+ try
+ {
+ FileSystemManager fsManager = VFS.getManager();
+ FileName name = fsManager.resolveURI(path);
+ return name.getParent().getURI();
+ }
+ catch (FileSystemException fse)
+ {
+ fse.printStackTrace();
+ return null;
+ }
+ }
+
+ @Override
+ public String getFileName(String path)
+ {
+ if (UriParser.extractScheme(path) == null)
+ {
+ return super.getFileName(path);
+ }
+ try
+ {
+ FileSystemManager fsManager = VFS.getManager();
+ FileName name = fsManager.resolveURI(path);
+ return name.getBaseName();
+ }
+ catch (FileSystemException fse)
+ {
+ fse.printStackTrace();
+ return null;
+ }
+ }
+
+ @Override
+ public URL getURL(String basePath, String file) throws MalformedURLException
+ {
+ if ((basePath != null && UriParser.extractScheme(basePath) == null)
+ || (basePath == null && UriParser.extractScheme(file) == null))
+ {
+ return super.getURL(basePath, file);
+ }
+ try
+ {
+ FileSystemManager fsManager = VFS.getManager();
+
+ FileName path;
+ if (basePath != null && UriParser.extractScheme(file) == null)
+ {
+ FileName base = fsManager.resolveURI(basePath);
+ path = fsManager.resolveName(base, file);
+ }
+ else
+ {
+ path = fsManager.resolveURI(file);
+ }
+
+ URLStreamHandler handler = new VFSURLStreamHandler(path);
+ return new URL(null, path.getURI(), handler);
+ }
+ catch (FileSystemException fse)
+ {
+ throw new ConfigurationRuntimeException("Could not parse basePath: " + basePath
+ + " and fileName: " + file, fse);
+ }
+ }
+
+ @Override
+ public URL locateFromURL(String basePath, String fileName)
+ {
+ String fileScheme = UriParser.extractScheme(fileName);
+
+ // Use DefaultFileSystem if basePath and fileName don't have a scheme.
+ if ((basePath == null || UriParser.extractScheme(basePath) == null) && fileScheme == null)
+ {
+ return super.locateFromURL(basePath, fileName);
+ }
+ try
+ {
+ FileSystemManager fsManager = VFS.getManager();
+
+ FileObject file;
+ // Only use the base path if the file name doesn't have a scheme.
+ if (basePath != null && fileScheme == null)
+ {
+ String scheme = UriParser.extractScheme(basePath);
+ FileSystemOptions opts = (scheme != null) ? getOptions(scheme) : null;
+ FileObject base = (opts == null) ? fsManager.resolveFile(basePath)
+ : fsManager.resolveFile(basePath, opts);
+ if (base.getType() == FileType.FILE)
+ {
+ base = base.getParent();
+ }
+
+ file = fsManager.resolveFile(base, fileName);
+ }
+ else
+ {
+ FileSystemOptions opts = (fileScheme != null) ? getOptions(fileScheme) : null;
+ file = (opts == null) ? fsManager.resolveFile(fileName)
+ : fsManager.resolveFile(fileName, opts);
+ }
+
+ if (!file.exists())
+ {
+ return null;
+ }
+ FileName path = file.getName();
+ URLStreamHandler handler = new VFSURLStreamHandler(path);
+ return new URL(null, path.getURI(), handler);
+ }
+ catch (FileSystemException fse)
+ {
+ return null;
+ }
+ catch (MalformedURLException ex)
+ {
+ return null;
+ }
+ }
+
+ private FileSystemOptions getOptions(String scheme)
+ {
+ FileSystemOptions opts = new FileSystemOptions();
+ FileSystemConfigBuilder builder;
+ try
+ {
+ builder = VFS.getManager().getFileSystemConfigBuilder(scheme);
+ }
+ catch (Exception ex)
+ {
+ return null;
+ }
+ FileOptionsProvider provider = getFileOptionsProvider();
+ if (provider != null)
+ {
+ Map<String, Object> map = provider.getOptions();
+ if (map == null)
+ {
+ return null;
+ }
+ int count = 0;
+ for (Map.Entry<String, Object> entry : map.entrySet())
+ {
+ try
+ {
+ String key = entry.getKey();
+ if (FileOptionsProvider.CURRENT_USER.equals(key))
+ {
+ key = "creatorName";
+ }
+ setProperty(builder, opts, key, entry.getValue());
+ ++count;
+ }
+ catch (Exception ex)
+ {
+ // Ignore an incorrect property.
+ continue;
+ }
+ }
+ if (count > 0)
+ {
+ return opts;
+ }
+ }
+ return null;
+
+ }
+
+ private void setProperty(FileSystemConfigBuilder builder, FileSystemOptions options,
+ String key, Object value)
+ {
+ String methodName = "set" + key.substring(0, 1).toUpperCase() + key.substring(1);
+ Class<?>[] paramTypes = new Class<?>[2];
+ paramTypes[0] = FileSystemOptions.class;
+ paramTypes[1] = value.getClass();
+
+ try
+ {
+ Method method = builder.getClass().getMethod(methodName, paramTypes);
+ Object[] params = new Object[2];
+ params[0] = options;
+ params[1] = value;
+ method.invoke(builder, params);
+ }
+ catch (Exception ex)
+ {
+ return;
+ }
+
+ }
+
+ /**
+ * Stream handler required to create URL.
+ */
+ private static class VFSURLStreamHandler extends URLStreamHandler
+ {
+ /** The Protocol used */
+ private final String protocol;
+
+ public VFSURLStreamHandler(FileName file)
+ {
+ this.protocol = file.getScheme();
+ }
+
+ @Override
+ protected URLConnection openConnection(URL url) throws IOException
+ {
+ throw new IOException("VFS URLs can only be used with VFS APIs");
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/VerifiableOutputStream.java b/src/main/java/org/apache/commons/configuration/VerifiableOutputStream.java
new file mode 100644
index 0000000..cfc53fa
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/VerifiableOutputStream.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.io.OutputStream;
+import java.io.IOException;
+
+/**
+ * OutputStream that can be checked for errors after it is written to.
+ * @since 1.7
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons Configuration team</a>
+ */
+abstract class VerifiableOutputStream extends OutputStream
+{
+ public abstract void verify() throws IOException;
+}
diff --git a/src/main/java/org/apache/commons/configuration/XMLConfiguration.java b/src/main/java/org/apache/commons/configuration/XMLConfiguration.java
new file mode 100644
index 0000000..d6b4df1
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/XMLConfiguration.java
@@ -0,0 +1,1650 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.TransformerFactoryConfigurationError;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.configuration.resolver.DefaultEntityResolver;
+import org.apache.commons.configuration.resolver.EntityRegistry;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.LogFactory;
+import org.w3c.dom.Attr;
+import org.w3c.dom.CDATASection;
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Text;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * <p>A specialized hierarchical configuration class that is able to parse XML
+ * documents.</p>
+ *
+ * <p>The parsed document will be stored keeping its structure. The class also
+ * tries to preserve as much information from the loaded XML document as
+ * possible, including comments and processing instructions. These will be
+ * contained in documents created by the {@code save()} methods, too.</p>
+ *
+ * <p>Like other file based configuration classes this class maintains the name
+ * and path to the loaded configuration file. These properties can be altered
+ * using several setter methods, but they are not modified by {@code save()}
+ * and {@code load()} methods. If XML documents contain relative paths to
+ * other documents (e.g. to a DTD), these references are resolved based on the
+ * path set for this configuration.</p>
+ *
+ * <p>By inheriting from {@link AbstractConfiguration} this class
+ * provides some extended functionality, e.g. interpolation of property values.
+ * Like in {@link PropertiesConfiguration} property values can
+ * contain delimiter characters (the comma ',' per default) and are then split
+ * into multiple values. This works for XML attributes and text content of
+ * elements as well. The delimiter can be escaped by a backslash. As an example
+ * consider the following XML fragment:</p>
+ *
+ * <p>
+ * <pre>
+ * <config>
+ * <array>10,20,30,40</array>
+ * <scalar>3\,1415</scalar>
+ * <cite text="To be or not to be\, this is the question!"/>
+ * </config>
+ * </pre>
+ * </p>
+ * <p>Here the content of the {@code array} element will be split at
+ * the commas, so the {@code array} key will be assigned 4 values. In the
+ * {@code scalar} property and the {@code text} attribute of the
+ * {@code cite} element the comma is escaped, so that no splitting is
+ * performed.</p>
+ *
+ * <p>The configuration API allows setting multiple values for a single attribute,
+ * e.g. something like the following is legal (assuming that the default
+ * expression engine is used):
+ * <pre>
+ * XMLConfiguration config = new XMLConfiguration();
+ * config.addProperty("test.dir[@name]", "C:\\Temp\\");
+ * config.addProperty("test.dir[@name]", "D:\\Data\\");
+ * </pre></p>
+ *
+ * <p>Because in XML such a constellation is not directly supported (an attribute
+ * can appear only once for a single element), the values are concatenated to a
+ * single value. If delimiter parsing is enabled (refer to the
+ * {@link #setDelimiterParsingDisabled(boolean)} method), the
+ * current list delimiter character will be used as separator. Otherwise the
+ * pipe symbol ("|") will be used for this purpose. No matter which character is
+ * used as delimiter, it can always be escaped with a backslash. A backslash
+ * itself can also be escaped with another backslash. Consider the following
+ * example fragment from a configuration file:
+ * <pre>
+ * <directories names="C:\Temp\\|D:\Data\"/>
+ * </pre>
+ * Here the backslash after Temp is escaped. This is necessary because it
+ * would escape the list delimiter (the pipe symbol assuming that list delimiter
+ * parsing is disabled) otherwise. So this attribute would have two values.</p>
+ *
+ * <p>Note: You should ensure that the <em>delimiter parsing disabled</em>
+ * property is always consistent when you load and save a configuration file.
+ * Otherwise the values of properties can become corrupted.</p>
+ *
+ * <p>Whitespace in the content of XML documents is trimmed per default. In most
+ * cases this is desired. However, sometimes whitespace is indeed important and
+ * should be treated as part of the value of a property as in the following
+ * example:
+ * <pre>
+ * <indent> </indent>
+ * </pre></p>
+ *
+ * <p>Per default the spaces in the {@code indent} element will be trimmed
+ * resulting in an empty element. To tell {@code XMLConfiguration} that
+ * spaces are relevant the {@code xml:space} attribute can be used, which is
+ * defined in the <a href="http://www.w3.org/TR/REC-xml/#sec-white-space">XML
+ * specification</a>. This will look as follows:
+ * <pre>
+ * <indent <strong>xml:space="preserve"</strong>> </indent>
+ * </pre>
+ * The value of the {@code indent} property will now contain the spaces.</p>
+ *
+ * <p>{@code XMLConfiguration} implements the {@link FileConfiguration}
+ * interface and thus provides full support for loading XML documents from
+ * different sources like files, URLs, or streams. A full description of these
+ * features can be found in the documentation of
+ * {@link AbstractFileConfiguration}.</p>
+ *
+ * <p><em>Note:</em>Configuration objects of this type can be read concurrently
+ * by multiple threads. However if one of these threads modifies the object,
+ * synchronization has to be performed manually.</p>
+ *
+ * @since commons-configuration 1.0
+ *
+ * @author Jörg Schaible
+ * @version $Id: XMLConfiguration.java 1534429 2013-10-22 00:45:36Z henning $
+ */
+public class XMLConfiguration extends AbstractHierarchicalFileConfiguration
+ implements EntityResolver, EntityRegistry
+{
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = 2453781111653383552L;
+
+ /** Constant for the default root element name. */
+ private static final String DEFAULT_ROOT_NAME = "configuration";
+
+ /** Constant for the name of the space attribute.*/
+ private static final String ATTR_SPACE = "xml:space";
+
+ /** Constant for the xml:space value for preserving whitespace.*/
+ private static final String VALUE_PRESERVE = "preserve";
+
+ /** Constant for the delimiter for multiple attribute values.*/
+ private static final char ATTR_VALUE_DELIMITER = '|';
+
+ /** Schema Langauge key for the parser */
+ private static final String JAXP_SCHEMA_LANGUAGE =
+ "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
+
+ /** Schema Language for the parser */
+ private static final String W3C_XML_SCHEMA =
+ "http://www.w3.org/2001/XMLSchema";
+
+ /** The document from this configuration's data source. */
+ private Document document;
+
+ /** Stores the name of the root element. */
+ private String rootElementName;
+
+ /** Stores the public ID from the DOCTYPE.*/
+ private String publicID;
+
+ /** Stores the system ID from the DOCTYPE.*/
+ private String systemID;
+
+ /** Stores the document builder that should be used for loading.*/
+ private DocumentBuilder documentBuilder;
+
+ /** Stores a flag whether DTD or Schema validation should be performed.*/
+ private boolean validating;
+
+ /** Stores a flag whether DTD or Schema validation is used */
+ private boolean schemaValidation;
+
+ /** A flag whether attribute splitting is disabled.*/
+ private boolean attributeSplittingDisabled;
+
+ /** The EntityResolver to use */
+ private EntityResolver entityResolver = new DefaultEntityResolver();
+
+ /**
+ * Creates a new instance of {@code XMLConfiguration}.
+ */
+ public XMLConfiguration()
+ {
+ super();
+ setLogger(LogFactory.getLog(XMLConfiguration.class));
+ }
+
+ /**
+ * Creates a new instance of {@code XMLConfiguration} and copies the
+ * content of the passed in configuration into this object. Note that only
+ * the data of the passed in configuration will be copied. If, for instance,
+ * the other configuration is a {@code XMLConfiguration}, too,
+ * things like comments or processing instructions will be lost.
+ *
+ * @param c the configuration to copy
+ * @since 1.4
+ */
+ public XMLConfiguration(HierarchicalConfiguration c)
+ {
+ super(c);
+ clearReferences(getRootNode());
+ setRootElementName(getRootNode().getName());
+ setLogger(LogFactory.getLog(XMLConfiguration.class));
+ }
+
+ /**
+ * Creates a new instance of{@code XMLConfiguration}. The
+ * configuration is loaded from the specified file
+ *
+ * @param fileName the name of the file to load
+ * @throws ConfigurationException if the file cannot be loaded
+ */
+ public XMLConfiguration(String fileName) throws ConfigurationException
+ {
+ super(fileName);
+ setLogger(LogFactory.getLog(XMLConfiguration.class));
+ }
+
+ /**
+ * Creates a new instance of {@code XMLConfiguration}.
+ * The configuration is loaded from the specified file.
+ *
+ * @param file the file
+ * @throws ConfigurationException if an error occurs while loading the file
+ */
+ public XMLConfiguration(File file) throws ConfigurationException
+ {
+ super(file);
+ setLogger(LogFactory.getLog(XMLConfiguration.class));
+ }
+
+ /**
+ * Creates a new instance of {@code XMLConfiguration}.
+ * The configuration is loaded from the specified URL.
+ *
+ * @param url the URL
+ * @throws ConfigurationException if loading causes an error
+ */
+ public XMLConfiguration(URL url) throws ConfigurationException
+ {
+ super(url);
+ setLogger(LogFactory.getLog(XMLConfiguration.class));
+ }
+
+ /**
+ * Returns the name of the root element. If this configuration was loaded
+ * from a XML document, the name of this document's root element is
+ * returned. Otherwise it is possible to set a name for the root element
+ * that will be used when this configuration is stored.
+ *
+ * @return the name of the root element
+ */
+ public String getRootElementName()
+ {
+ if (getDocument() == null)
+ {
+ return (rootElementName == null) ? DEFAULT_ROOT_NAME : rootElementName;
+ }
+ else
+ {
+ return getDocument().getDocumentElement().getNodeName();
+ }
+ }
+
+ /**
+ * Sets the name of the root element. This name is used when this
+ * configuration object is stored in an XML file. Note that setting the name
+ * of the root element works only if this configuration has been newly
+ * created. If the configuration was loaded from an XML file, the name
+ * cannot be changed and an {@code UnsupportedOperationException}
+ * exception is thrown. Whether this configuration has been loaded from an
+ * XML document or not can be found out using the {@code getDocument()}
+ * method.
+ *
+ * @param name the name of the root element
+ */
+ public void setRootElementName(String name)
+ {
+ if (getDocument() != null)
+ {
+ throw new UnsupportedOperationException("The name of the root element "
+ + "cannot be changed when loaded from an XML document!");
+ }
+ rootElementName = name;
+ getRootNode().setName(name);
+ }
+
+ /**
+ * Returns the {@code DocumentBuilder} object that is used for
+ * loading documents. If no specific builder has been set, this method
+ * returns <b>null</b>.
+ *
+ * @return the {@code DocumentBuilder} for loading new documents
+ * @since 1.2
+ */
+ public DocumentBuilder getDocumentBuilder()
+ {
+ return documentBuilder;
+ }
+
+ /**
+ * Sets the {@code DocumentBuilder} object to be used for loading
+ * documents. This method makes it possible to specify the exact document
+ * builder. So an application can create a builder, configure it for its
+ * special needs, and then pass it to this method.
+ *
+ * @param documentBuilder the document builder to be used; if undefined, a
+ * default builder will be used
+ * @since 1.2
+ */
+ public void setDocumentBuilder(DocumentBuilder documentBuilder)
+ {
+ this.documentBuilder = documentBuilder;
+ }
+
+ /**
+ * Returns the public ID of the DOCTYPE declaration from the loaded XML
+ * document. This is <b>null</b> if no document has been loaded yet or if
+ * the document does not contain a DOCTYPE declaration with a public ID.
+ *
+ * @return the public ID
+ * @since 1.3
+ */
+ public String getPublicID()
+ {
+ return publicID;
+ }
+
+ /**
+ * Sets the public ID of the DOCTYPE declaration. When this configuration is
+ * saved, a DOCTYPE declaration will be constructed that contains this
+ * public ID.
+ *
+ * @param publicID the public ID
+ * @since 1.3
+ */
+ public void setPublicID(String publicID)
+ {
+ this.publicID = publicID;
+ }
+
+ /**
+ * Returns the system ID of the DOCTYPE declaration from the loaded XML
+ * document. This is <b>null</b> if no document has been loaded yet or if
+ * the document does not contain a DOCTYPE declaration with a system ID.
+ *
+ * @return the system ID
+ * @since 1.3
+ */
+ public String getSystemID()
+ {
+ return systemID;
+ }
+
+ /**
+ * Sets the system ID of the DOCTYPE declaration. When this configuration is
+ * saved, a DOCTYPE declaration will be constructed that contains this
+ * system ID.
+ *
+ * @param systemID the system ID
+ * @since 1.3
+ */
+ public void setSystemID(String systemID)
+ {
+ this.systemID = systemID;
+ }
+
+ /**
+ * Returns the value of the validating flag.
+ *
+ * @return the validating flag
+ * @since 1.2
+ */
+ public boolean isValidating()
+ {
+ return validating;
+ }
+
+ /**
+ * Sets the value of the validating flag. This flag determines whether
+ * DTD/Schema validation should be performed when loading XML documents. This
+ * flag is evaluated only if no custom {@code DocumentBuilder} was set.
+ *
+ * @param validating the validating flag
+ * @since 1.2
+ */
+ public void setValidating(boolean validating)
+ {
+ if (!schemaValidation)
+ {
+ this.validating = validating;
+ }
+ }
+
+
+ /**
+ * Returns the value of the schemaValidation flag.
+ *
+ * @return the schemaValidation flag
+ * @since 1.7
+ */
+ public boolean isSchemaValidation()
+ {
+ return schemaValidation;
+ }
+
+ /**
+ * Sets the value of the schemaValidation flag. This flag determines whether
+ * DTD or Schema validation should be used. This
+ * flag is evaluated only if no custom {@code DocumentBuilder} was set.
+ * If set to true the XML document must contain a schemaLocation definition
+ * that provides resolvable hints to the required schemas.
+ *
+ * @param schemaValidation the validating flag
+ * @since 1.7
+ */
+ public void setSchemaValidation(boolean schemaValidation)
+ {
+ this.schemaValidation = schemaValidation;
+ if (schemaValidation)
+ {
+ this.validating = true;
+ }
+ }
+
+ /**
+ * Sets a new EntityResolver. Setting this will cause RegisterEntityId to have no
+ * effect.
+ * @param resolver The EntityResolver to use.
+ * @since 1.7
+ */
+ public void setEntityResolver(EntityResolver resolver)
+ {
+ this.entityResolver = resolver;
+ }
+
+ /**
+ * Returns the EntityResolver.
+ * @return The EntityResolver.
+ * @since 1.7
+ */
+ public EntityResolver getEntityResolver()
+ {
+ return this.entityResolver;
+ }
+
+ /**
+ * Returns the flag whether attribute splitting is disabled.
+ *
+ * @return the flag whether attribute splitting is disabled
+ * @see #setAttributeSplittingDisabled(boolean)
+ * @since 1.6
+ */
+ public boolean isAttributeSplittingDisabled()
+ {
+ return attributeSplittingDisabled;
+ }
+
+ /**
+ * <p>
+ * Sets a flag whether attribute splitting is disabled.
+ * </p>
+ * <p>
+ * The Configuration API allows adding multiple values to an attribute. This
+ * is problematic when storing the configuration because in XML an attribute
+ * can appear only once with a single value. To solve this problem, per
+ * default multiple attribute values are concatenated using a special
+ * separator character and split again when the configuration is loaded. The
+ * separator character is either the list delimiter character (see
+ * {@link #setListDelimiter(char)}) or the pipe symbol ("|") if
+ * list delimiter parsing is disabled.
+ * </p>
+ * <p>
+ * In some constellations the splitting of attribute values can have
+ * undesired effects, especially if list delimiter parsing is disabled and
+ * attributes may contain the "|" character. In these cases it is
+ * possible to disable the attribute splitting mechanism by calling this
+ * method with a boolean value set to <b>false</b>. If attribute splitting
+ * is disabled, the values of attributes will not be processed, but stored
+ * as configuration properties exactly as they are returned by the XML
+ * parser.
+ * </p>
+ * <p>
+ * Note that in this mode multiple attribute values cannot be handled
+ * correctly. It is possible to create a {@code XMLConfiguration}
+ * object, add multiple values to an attribute and save it. When the
+ * configuration is loaded again and attribute splitting is disabled, the
+ * attribute will only have a single value, which is the concatenation of
+ * all values set before. So it lies in the responsibility of the
+ * application to carefully set the values of attributes.
+ * </p>
+ * <p>
+ * As is true for the {@link #setDelimiterParsingDisabled(boolean)} method,
+ * this method must be called before the configuration is loaded. So it
+ * can't be used together with one of the constructors expecting the
+ * specification of the file to load. Instead the default constructor has to
+ * be used, then {@code setAttributeSplittingDisabled(false)} has to be
+ * called, and finally the configuration can be loaded using one of its
+ * {@code load()} methods.
+ * </p>
+ *
+ * @param attributeSplittingDisabled <b>true</b> for disabling attribute
+ * splitting, <b>false</b> for enabling it
+ * @see #setDelimiterParsingDisabled(boolean)
+ * @since 1.6
+ */
+ public void setAttributeSplittingDisabled(boolean attributeSplittingDisabled)
+ {
+ this.attributeSplittingDisabled = attributeSplittingDisabled;
+ }
+
+ /**
+ * Returns the XML document this configuration was loaded from. The return
+ * value is <b>null</b> if this configuration was not loaded from a XML
+ * document.
+ *
+ * @return the XML document this configuration was loaded from
+ */
+ public Document getDocument()
+ {
+ return document;
+ }
+
+ /**
+ * Removes all properties from this configuration. If this configuration
+ * was loaded from a file, the associated DOM document is also cleared.
+ */
+ @Override
+ public void clear()
+ {
+ super.clear();
+ setRoot(new Node());
+ document = null;
+ }
+
+ /**
+ * Initializes this configuration from an XML document.
+ *
+ * @param document the document to be parsed
+ * @param elemRefs a flag whether references to the XML elements should be set
+ */
+ public void initProperties(Document document, boolean elemRefs)
+ {
+ if (document.getDoctype() != null)
+ {
+ setPublicID(document.getDoctype().getPublicId());
+ setSystemID(document.getDoctype().getSystemId());
+ }
+
+ constructHierarchy(getRoot(), document.getDocumentElement(), elemRefs, true);
+ getRootNode().setName(document.getDocumentElement().getNodeName());
+ if (elemRefs)
+ {
+ getRoot().setReference(document.getDocumentElement());
+ }
+ }
+
+ /**
+ * Helper method for building the internal storage hierarchy. The XML
+ * elements are transformed into node objects.
+ *
+ * @param node the actual node
+ * @param element the actual XML element
+ * @param elemRefs a flag whether references to the XML elements should be
+ * set
+ * @param trim a flag whether the text content of elements should be
+ * trimmed; this controls the whitespace handling
+ * @return a map with all attribute values extracted for the current node;
+ * this map also contains the value of the trim flag for this node
+ * under the key {@value #ATTR_SPACE}
+ */
+ private Map<String, Collection<String>> constructHierarchy(Node node,
+ Element element, boolean elemRefs, boolean trim)
+ {
+ boolean trimFlag = shouldTrim(element, trim);
+ Map<String, Collection<String>> attributes =
+ processAttributes(node, element, elemRefs);
+ attributes.put(ATTR_SPACE, Collections.singleton(String.valueOf(trimFlag)));
+ StringBuilder buffer = new StringBuilder();
+ NodeList list = element.getChildNodes();
+ for (int i = 0; i < list.getLength(); i++)
+ {
+ org.w3c.dom.Node w3cNode = list.item(i);
+ if (w3cNode instanceof Element)
+ {
+ Element child = (Element) w3cNode;
+ Node childNode = new XMLNode(child.getTagName(),
+ elemRefs ? child : null);
+ Map<String, Collection<String>> attrmap =
+ constructHierarchy(childNode, child, elemRefs, trimFlag);
+ node.addChild(childNode);
+ Collection<String> attrSpace = attrmap.remove(ATTR_SPACE);
+
+ Boolean childTrim = CollectionUtils.isEmpty(attrSpace)
+ ? Boolean.FALSE
+ : Boolean.valueOf(attrSpace.iterator().next());
+
+ handleDelimiters(node, childNode, childTrim.booleanValue(), attrmap);
+ }
+ else if (w3cNode instanceof Text)
+ {
+ Text data = (Text) w3cNode;
+ buffer.append(data.getData());
+ }
+ }
+
+ String text = determineValue(node, buffer.toString(), trimFlag);
+ if (text.length() > 0 || (!node.hasChildren() && node != getRoot()))
+ {
+ node.setValue(text);
+ }
+ return attributes;
+ }
+
+ /**
+ * Determines the value of a configuration node. This method mainly checks
+ * whether the text value is to be trimmed or not. This is normally defined
+ * by the trim flag. However, if the node has children and its content is
+ * only whitespace, then it makes no sense to store any value; this would
+ * only scramble layout when the configuration is saved again.
+ *
+ * @param node the current {@code ConfigurationNode}
+ * @param content the text content of this node
+ * @param trimFlag the trim flag
+ * @return the value to be stored for this node
+ */
+ private static String determineValue(ConfigurationNode node,
+ String content, boolean trimFlag)
+ {
+ boolean shouldTrim =
+ trimFlag
+ || (StringUtils.isBlank(content) && node
+ .getChildrenCount() > 0);
+ return shouldTrim ? content.trim() : content;
+ }
+
+ /**
+ * Helper method for constructing node objects for the attributes of the
+ * given XML element.
+ *
+ * @param node the current node
+ * @param element the actual XML element
+ * @param elemRefs a flag whether references to the XML elements should be set
+ * @return a map with all attribute values extracted for the current node
+ */
+ private Map<String, Collection<String>> processAttributes(Node node,
+ Element element, boolean elemRefs)
+ {
+ NamedNodeMap attributes = element.getAttributes();
+ Map<String, Collection<String>> attrmap = new HashMap<String, Collection<String>>();
+
+ for (int i = 0; i < attributes.getLength(); ++i)
+ {
+ org.w3c.dom.Node w3cNode = attributes.item(i);
+ if (w3cNode instanceof Attr)
+ {
+ Attr attr = (Attr) w3cNode;
+ List<String> values;
+ if (isAttributeSplittingDisabled())
+ {
+ values = Collections.singletonList(attr.getValue());
+ }
+ else
+ {
+ values = PropertyConverter.split(attr.getValue(),
+ isDelimiterParsingDisabled() ? ATTR_VALUE_DELIMITER
+ : getListDelimiter());
+ }
+
+ appendAttributes(node, element, elemRefs, attr.getName(), values);
+ attrmap.put(attr.getName(), values);
+ }
+ }
+
+ return attrmap;
+ }
+
+ /**
+ * Adds attribute nodes to the given node. For each attribute value, a new
+ * attribute node is created and added as child to the current node.
+ *
+ * @param node the current node
+ * @param element the corresponding XML element
+ * @param attr the name of the attribute
+ * @param values the attribute values
+ */
+ private void appendAttributes(Node node, Element element, boolean elemRefs,
+ String attr, Collection<String> values)
+ {
+ for (String value : values)
+ {
+ Node child = new XMLNode(attr, elemRefs ? element : null);
+ child.setValue(value);
+ node.addAttribute(child);
+ }
+ }
+
+ /**
+ * Deals with elements whose value is a list. In this case multiple child
+ * elements must be added.
+ *
+ * @param parent the parent element
+ * @param child the child element
+ * @param trim flag whether texts of elements should be trimmed
+ * @param attrmap a map with the attributes of the current node
+ */
+ private void handleDelimiters(Node parent, Node child, boolean trim,
+ Map<String, Collection<String>> attrmap)
+ {
+ if (child.getValue() != null)
+ {
+ List<String> values;
+ if (isDelimiterParsingDisabled())
+ {
+ values = new ArrayList<String>();
+ values.add(child.getValue().toString());
+ }
+ else
+ {
+ values = PropertyConverter.split(child.getValue().toString(),
+ getListDelimiter(), trim);
+ }
+
+ if (values.size() > 1)
+ {
+ Iterator<String> it = values.iterator();
+ // Create new node for the original child's first value
+ Node c = createNode(child.getName());
+ c.setValue(it.next());
+ // Copy original attributes to the new node
+ for (ConfigurationNode ndAttr : child.getAttributes())
+ {
+ ndAttr.setReference(null);
+ c.addAttribute(ndAttr);
+ }
+ parent.remove(child);
+ parent.addChild(c);
+
+ // add multiple new children
+ while (it.hasNext())
+ {
+ c = new XMLNode(child.getName(), null);
+ c.setValue(it.next());
+ for (Map.Entry<String, Collection<String>> e : attrmap
+ .entrySet())
+ {
+ appendAttributes(c, null, false, e.getKey(),
+ e.getValue());
+ }
+ parent.addChild(c);
+ }
+ }
+ else if (values.size() == 1)
+ {
+ // we will have to replace the value because it might
+ // contain escaped delimiters
+ child.setValue(values.get(0));
+ }
+ }
+ }
+
+ /**
+ * Checks whether the content of the current XML element should be trimmed.
+ * This method checks whether a {@code xml:space} attribute is
+ * present and evaluates its value. See <a
+ * href="http://www.w3.org/TR/REC-xml/#sec-white-space">
+ * http://www.w3.org/TR/REC-xml/#sec-white-space</a> for more details.
+ *
+ * @param element the current XML element
+ * @param currentTrim the current trim flag
+ * @return a flag whether the content of this element should be trimmed
+ */
+ private boolean shouldTrim(Element element, boolean currentTrim)
+ {
+ Attr attr = element.getAttributeNode(ATTR_SPACE);
+
+ if (attr == null)
+ {
+ return currentTrim;
+ }
+ else
+ {
+ return !VALUE_PRESERVE.equals(attr.getValue());
+ }
+ }
+
+ /**
+ * Creates the {@code DocumentBuilder} to be used for loading files.
+ * This implementation checks whether a specific
+ * {@code DocumentBuilder} has been set. If this is the case, this
+ * one is used. Otherwise a default builder is created. Depending on the
+ * value of the validating flag this builder will be a validating or a non
+ * validating {@code DocumentBuilder}.
+ *
+ * @return the {@code DocumentBuilder} for loading configuration
+ * files
+ * @throws ParserConfigurationException if an error occurs
+ * @since 1.2
+ */
+ protected DocumentBuilder createDocumentBuilder()
+ throws ParserConfigurationException
+ {
+ if (getDocumentBuilder() != null)
+ {
+ return getDocumentBuilder();
+ }
+ else
+ {
+ DocumentBuilderFactory factory = DocumentBuilderFactory
+ .newInstance();
+ if (isValidating())
+ {
+ factory.setValidating(true);
+ if (isSchemaValidation())
+ {
+ factory.setNamespaceAware(true);
+ factory.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
+ }
+ }
+
+ DocumentBuilder result = factory.newDocumentBuilder();
+ result.setEntityResolver(this.entityResolver);
+
+ if (isValidating())
+ {
+ // register an error handler which detects validation errors
+ result.setErrorHandler(new DefaultHandler()
+ {
+ @Override
+ public void error(SAXParseException ex) throws SAXException
+ {
+ throw ex;
+ }
+ });
+ }
+ return result;
+ }
+ }
+
+ /**
+ * Creates a DOM document from the internal tree of configuration nodes.
+ *
+ * @return the new document
+ * @throws ConfigurationException if an error occurs
+ */
+ protected Document createDocument() throws ConfigurationException
+ {
+ try
+ {
+ if (document == null)
+ {
+ DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+ Document newDocument = builder.newDocument();
+ Element rootElem = newDocument.createElement(getRootElementName());
+ newDocument.appendChild(rootElem);
+ document = newDocument;
+ }
+
+ XMLBuilderVisitor builder = new XMLBuilderVisitor(document,
+ isDelimiterParsingDisabled() ? (char) 0 : getListDelimiter(),
+ isAttributeSplittingDisabled());
+ builder.processDocument(getRoot());
+ initRootElementText(document, getRootNode().getValue());
+ return document;
+ }
+ catch (DOMException domEx)
+ {
+ throw new ConfigurationException(domEx);
+ }
+ catch (ParserConfigurationException pex)
+ {
+ throw new ConfigurationException(pex);
+ }
+ }
+
+ /**
+ * Sets the text of the root element of a newly created XML Document.
+ *
+ * @param doc the document
+ * @param value the new text to be set
+ */
+ private void initRootElementText(Document doc, Object value)
+ {
+ Element elem = doc.getDocumentElement();
+ NodeList children = elem.getChildNodes();
+
+ // Remove all existing text nodes
+ for (int i = 0; i < children.getLength(); i++)
+ {
+ org.w3c.dom.Node nd = children.item(i);
+ if (nd.getNodeType() == org.w3c.dom.Node.TEXT_NODE)
+ {
+ elem.removeChild(nd);
+ }
+ }
+
+ if (value != null)
+ {
+ // Add a new text node
+ elem.appendChild(doc.createTextNode(String.valueOf(value)));
+ }
+ }
+
+ /**
+ * Creates a new node object. This implementation returns an instance of the
+ * {@code XMLNode} class.
+ *
+ * @param name the node's name
+ * @return the new node
+ */
+ @Override
+ protected Node createNode(String name)
+ {
+ return new XMLNode(name, null);
+ }
+
+ /**
+ * Loads the configuration from the given input stream.
+ *
+ * @param in the input stream
+ * @throws ConfigurationException if an error occurs
+ */
+ @Override
+ public void load(InputStream in) throws ConfigurationException
+ {
+ load(new InputSource(in));
+ }
+
+ /**
+ * Load the configuration from the given reader.
+ * Note that the {@code clear()} method is not called, so
+ * the properties contained in the loaded file will be added to the
+ * actual set of properties.
+ *
+ * @param in An InputStream.
+ *
+ * @throws ConfigurationException if an error occurs
+ */
+ public void load(Reader in) throws ConfigurationException
+ {
+ load(new InputSource(in));
+ }
+
+ /**
+ * Loads a configuration file from the specified input source.
+ * @param source the input source
+ * @throws ConfigurationException if an error occurs
+ */
+ private void load(InputSource source) throws ConfigurationException
+ {
+ try
+ {
+ URL sourceURL = getDelegate().getURL();
+ if (sourceURL != null)
+ {
+ source.setSystemId(sourceURL.toString());
+ }
+
+ DocumentBuilder builder = createDocumentBuilder();
+ Document newDocument = builder.parse(source);
+ Document oldDocument = document;
+ document = null;
+ initProperties(newDocument, oldDocument == null);
+ document = (oldDocument == null) ? newDocument : oldDocument;
+ }
+ catch (SAXParseException spe)
+ {
+ throw new ConfigurationException("Error parsing " + source.getSystemId(), spe);
+ }
+ catch (Exception e)
+ {
+ this.getLogger().debug("Unable to load the configuraton", e);
+ throw new ConfigurationException("Unable to load the configuration", e);
+ }
+ }
+
+ /**
+ * Saves the configuration to the specified writer.
+ *
+ * @param writer the writer used to save the configuration
+ * @throws ConfigurationException if an error occurs
+ */
+ public void save(Writer writer) throws ConfigurationException
+ {
+ try
+ {
+ Transformer transformer = createTransformer();
+ Source source = new DOMSource(createDocument());
+ Result result = new StreamResult(writer);
+ transformer.transform(source, result);
+ }
+ catch (TransformerException e)
+ {
+ throw new ConfigurationException("Unable to save the configuration", e);
+ }
+ catch (TransformerFactoryConfigurationError e)
+ {
+ throw new ConfigurationException("Unable to save the configuration", e);
+ }
+ }
+
+ /**
+ * Validate the document against the Schema.
+ * @throws ConfigurationException if the validation fails.
+ */
+ public void validate() throws ConfigurationException
+ {
+ try
+ {
+ Transformer transformer = createTransformer();
+ Source source = new DOMSource(createDocument());
+ StringWriter writer = new StringWriter();
+ Result result = new StreamResult(writer);
+ transformer.transform(source, result);
+ Reader reader = new StringReader(writer.getBuffer().toString());
+ DocumentBuilder builder = createDocumentBuilder();
+ builder.parse(new InputSource(reader));
+ }
+ catch (SAXException e)
+ {
+ throw new ConfigurationException("Validation failed", e);
+ }
+ catch (IOException e)
+ {
+ throw new ConfigurationException("Validation failed", e);
+ }
+ catch (TransformerException e)
+ {
+ throw new ConfigurationException("Validation failed", e);
+ }
+ catch (ParserConfigurationException pce)
+ {
+ throw new ConfigurationException("Validation failed", pce);
+ }
+ }
+
+ /**
+ * Creates and initializes the transformer used for save operations. This
+ * base implementation initializes all of the default settings like
+ * indention mode and the DOCTYPE. Derived classes may overload this method
+ * if they have specific needs.
+ *
+ * @return the transformer to use for a save operation
+ * @throws TransformerException if an error occurs
+ * @since 1.3
+ */
+ protected Transformer createTransformer() throws TransformerException
+ {
+ Transformer transformer = TransformerFactory.newInstance()
+ .newTransformer();
+
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ if (getEncoding() != null)
+ {
+ transformer.setOutputProperty(OutputKeys.ENCODING, getEncoding());
+ }
+ if (getPublicID() != null)
+ {
+ transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
+ getPublicID());
+ }
+ if (getSystemID() != null)
+ {
+ transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
+ getSystemID());
+ }
+
+ return transformer;
+ }
+
+ /**
+ * Creates a copy of this object. The new configuration object will contain
+ * the same properties as the original, but it will lose any connection to a
+ * source document (if one exists). This is to avoid race conditions if both
+ * the original and the copy are modified and then saved.
+ *
+ * @return the copy
+ */
+ @Override
+ public Object clone()
+ {
+ XMLConfiguration copy = (XMLConfiguration) super.clone();
+
+ // clear document related properties
+ copy.document = null;
+ copy.setDelegate(copy.createDelegate());
+ // clear all references in the nodes, too
+ clearReferences(copy.getRootNode());
+
+ return copy;
+ }
+
+ /**
+ * Creates the file configuration delegate for this object. This implementation
+ * will return an instance of a class derived from {@code FileConfigurationDelegate}
+ * that deals with some specialties of {@code XMLConfiguration}.
+ * @return the delegate for this object
+ */
+ @Override
+ protected FileConfigurationDelegate createDelegate()
+ {
+ return new XMLFileConfigurationDelegate();
+ }
+
+ /**
+ * Adds a collection of nodes directly to this configuration. This
+ * implementation ensures that the nodes to be added are of the correct node
+ * type (they have to be converted to {@code XMLNode} if necessary).
+ *
+ * @param key the key where the nodes are to be added
+ * @param nodes the collection with the new nodes
+ * @since 1.5
+ */
+ @Override
+ public void addNodes(String key, Collection<? extends ConfigurationNode> nodes)
+ {
+ if (nodes != null && !nodes.isEmpty())
+ {
+ Collection<XMLNode> xmlNodes;
+ xmlNodes = new ArrayList<XMLNode>(nodes.size());
+ for (ConfigurationNode node : nodes)
+ {
+ xmlNodes.add(convertToXMLNode(node));
+ }
+ super.addNodes(key, xmlNodes);
+ }
+ else
+ {
+ super.addNodes(key, nodes);
+ }
+ }
+
+ /**
+ * Converts the specified node into a {@code XMLNode} if necessary.
+ * This is required for nodes that are directly added, e.g. by
+ * {@code addNodes()}. If the passed in node is already an instance
+ * of {@code XMLNode}, it is directly returned, and conversion
+ * stops. Otherwise a new {@code XMLNode} is created, and the
+ * children are also converted.
+ *
+ * @param node the node to be converted
+ * @return the converted node
+ */
+ private XMLNode convertToXMLNode(ConfigurationNode node)
+ {
+ if (node instanceof XMLNode)
+ {
+ return (XMLNode) node;
+ }
+
+ XMLNode nd = (XMLNode) createNode(node.getName());
+ nd.setValue(node.getValue());
+ nd.setAttribute(node.isAttribute());
+ for (ConfigurationNode child : node.getChildren())
+ {
+ nd.addChild(convertToXMLNode(child));
+ }
+ for (ConfigurationNode attr : node.getAttributes())
+ {
+ nd.addAttribute(convertToXMLNode(attr));
+ }
+ return nd;
+ }
+
+ /**
+ * <p>
+ * Registers the specified DTD URL for the specified public identifier.
+ * </p>
+ * <p>
+ * {@code XMLConfiguration} contains an internal
+ * {@code EntityResolver} implementation. This maps
+ * {@code PUBLICID}'s to URLs (from which the resource will be
+ * loaded). A common use case for this method is to register local URLs
+ * (possibly computed at runtime by a class loader) for DTDs. This allows
+ * the performance advantage of using a local version without having to
+ * ensure every {@code SYSTEM} URI on every processed XML document is
+ * local. This implementation provides only basic functionality. If more
+ * sophisticated features are required, using
+ * {@link #setDocumentBuilder(DocumentBuilder)} to set a custom
+ * {@code DocumentBuilder} (which also can be initialized with a
+ * custom {@code EntityResolver}) is recommended.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> This method will have no effect when a custom
+ * {@code DocumentBuilder} has been set. (Setting a custom
+ * {@code DocumentBuilder} overrides the internal implementation.)
+ * </p>
+ * <p>
+ * <strong>Note:</strong> This method must be called before the
+ * configuration is loaded. So the default constructor of
+ * {@code XMLConfiguration} should be used, the location of the
+ * configuration file set, {@code registerEntityId()} called, and
+ * finally the {@code load()} method can be invoked.
+ * </p>
+ *
+ * @param publicId Public identifier of the DTD to be resolved
+ * @param entityURL The URL to use for reading this DTD
+ * @throws IllegalArgumentException if the public ID is undefined
+ * @since 1.5
+ */
+ public void registerEntityId(String publicId, URL entityURL)
+ {
+ if (entityResolver instanceof EntityRegistry)
+ {
+ ((EntityRegistry) entityResolver).registerEntityId(publicId, entityURL);
+ }
+ }
+
+ /**
+ * Resolves the requested external entity. This is the default
+ * implementation of the {@code EntityResolver} interface. It checks
+ * the passed in public ID against the registered entity IDs and uses a
+ * local URL if possible.
+ *
+ * @param publicId the public identifier of the entity being referenced
+ * @param systemId the system identifier of the entity being referenced
+ * @return an input source for the specified entity
+ * @throws SAXException if a parsing exception occurs
+ * @since 1.5
+ * @deprecated Use getEntityResolver().resolveEntity()
+ */
+ @Deprecated
+ public InputSource resolveEntity(String publicId, String systemId)
+ throws SAXException
+ {
+ try
+ {
+ return entityResolver.resolveEntity(publicId, systemId);
+ }
+ catch (IOException e)
+ {
+ throw new SAXException(e);
+ }
+ }
+
+ /**
+ * Returns a map with the entity IDs that have been registered using the
+ * {@code registerEntityId()} method.
+ *
+ * @return a map with the registered entity IDs
+ */
+ public Map<String, URL> getRegisteredEntities()
+ {
+ if (entityResolver instanceof EntityRegistry)
+ {
+ return ((EntityRegistry) entityResolver).getRegisteredEntities();
+ }
+ return new HashMap<String, URL>();
+ }
+
+ /**
+ * A specialized {@code Node} class that is connected with an XML
+ * element. Changes on a node are also performed on the associated element.
+ */
+ class XMLNode extends Node
+ {
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = -4133988932174596562L;
+
+ /**
+ * Creates a new instance of {@code XMLNode} and initializes it
+ * with a name and the corresponding XML element.
+ *
+ * @param name the node's name
+ * @param elem the XML element
+ */
+ public XMLNode(String name, Element elem)
+ {
+ super(name);
+ setReference(elem);
+ }
+
+ /**
+ * Sets the value of this node. If this node is associated with an XML
+ * element, this element will be updated, too.
+ *
+ * @param value the node's new value
+ */
+ @Override
+ public void setValue(Object value)
+ {
+ super.setValue(value);
+
+ if (getReference() != null && document != null)
+ {
+ if (isAttribute())
+ {
+ updateAttribute();
+ }
+ else
+ {
+ updateElement(value);
+ }
+ }
+ }
+
+ /**
+ * Updates the associated XML elements when a node is removed.
+ */
+ @Override
+ protected void removeReference()
+ {
+ if (getReference() != null)
+ {
+ Element element = (Element) getReference();
+ if (isAttribute())
+ {
+ updateAttribute();
+ }
+ else
+ {
+ org.w3c.dom.Node parentElem = element.getParentNode();
+ if (parentElem != null)
+ {
+ parentElem.removeChild(element);
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the node's value if it represents an element node.
+ *
+ * @param value the new value
+ */
+ private void updateElement(Object value)
+ {
+ Text txtNode = findTextNodeForUpdate();
+ if (value == null)
+ {
+ // remove text
+ if (txtNode != null)
+ {
+ ((Element) getReference()).removeChild(txtNode);
+ }
+ }
+ else
+ {
+ if (txtNode == null)
+ {
+ String newValue = isDelimiterParsingDisabled() ? value.toString()
+ : PropertyConverter.escapeDelimiters(value.toString(), getListDelimiter());
+ txtNode = document.createTextNode(newValue);
+ if (((Element) getReference()).getFirstChild() != null)
+ {
+ ((Element) getReference()).insertBefore(txtNode,
+ ((Element) getReference()).getFirstChild());
+ }
+ else
+ {
+ ((Element) getReference()).appendChild(txtNode);
+ }
+ }
+ else
+ {
+ String newValue = isDelimiterParsingDisabled() ? value.toString()
+ : PropertyConverter.escapeDelimiters(value.toString(), getListDelimiter());
+ txtNode.setNodeValue(newValue);
+ }
+ }
+ }
+
+ /**
+ * Updates the node's value if it represents an attribute.
+ *
+ */
+ private void updateAttribute()
+ {
+ XMLBuilderVisitor.updateAttribute(getParent(), getName(), getListDelimiter(),
+ isAttributeSplittingDisabled());
+ }
+
+ /**
+ * Returns the only text node of this element for update. This method is
+ * called when the element's text changes. Then all text nodes except
+ * for the first are removed. A reference to the first is returned or
+ * <b>null </b> if there is no text node at all.
+ *
+ * @return the first and only text node
+ */
+ private Text findTextNodeForUpdate()
+ {
+ Text result = null;
+ Element elem = (Element) getReference();
+ // Find all Text nodes
+ NodeList children = elem.getChildNodes();
+ Collection<org.w3c.dom.Node> textNodes = new ArrayList<org.w3c.dom.Node>();
+ for (int i = 0; i < children.getLength(); i++)
+ {
+ org.w3c.dom.Node nd = children.item(i);
+ if (nd instanceof Text)
+ {
+ if (result == null)
+ {
+ result = (Text) nd;
+ }
+ else
+ {
+ textNodes.add(nd);
+ }
+ }
+ }
+
+ // We don't want CDATAs
+ if (result instanceof CDATASection)
+ {
+ textNodes.add(result);
+ result = null;
+ }
+
+ // Remove all but the first Text node
+ for (org.w3c.dom.Node tn : textNodes)
+ {
+ elem.removeChild(tn);
+ }
+ return result;
+ }
+ }
+
+ /**
+ * A concrete {@code BuilderVisitor} that can construct XML
+ * documents.
+ */
+ static class XMLBuilderVisitor extends BuilderVisitor
+ {
+ /** Stores the document to be constructed. */
+ private Document document;
+
+ /** Stores the list delimiter.*/
+ private final char listDelimiter;
+
+ /** True if attributes should not be split */
+ private boolean isAttributeSplittingDisabled;
+
+ /**
+ * Creates a new instance of {@code XMLBuilderVisitor}.
+ *
+ * @param doc the document to be created
+ * @param listDelimiter the delimiter for attribute properties with multiple values
+ * @param isAttributeSplittingDisabled true if attribute splitting is disabled.
+ */
+ public XMLBuilderVisitor(Document doc, char listDelimiter, boolean isAttributeSplittingDisabled)
+ {
+ document = doc;
+ this.listDelimiter = listDelimiter;
+ this.isAttributeSplittingDisabled = isAttributeSplittingDisabled;
+ }
+
+ /**
+ * Processes the node hierarchy and adds new nodes to the document.
+ *
+ * @param rootNode the root node
+ */
+ public void processDocument(Node rootNode)
+ {
+ rootNode.visit(this, null);
+ }
+
+ /**
+ * Inserts a new node. This implementation ensures that the correct
+ * XML element is created and inserted between the given siblings.
+ *
+ * @param newNode the node to insert
+ * @param parent the parent node
+ * @param sibling1 the first sibling
+ * @param sibling2 the second sibling
+ * @return the new node
+ */
+ @Override
+ protected Object insert(Node newNode, Node parent, Node sibling1, Node sibling2)
+ {
+ if (newNode.isAttribute())
+ {
+ updateAttribute(parent, getElement(parent), newNode.getName(), listDelimiter,
+ isAttributeSplittingDisabled);
+ return null;
+ }
+
+ else
+ {
+ Element elem = document.createElement(newNode.getName());
+ if (newNode.getValue() != null)
+ {
+ String txt = newNode.getValue().toString();
+ if (listDelimiter != 0)
+ {
+ txt = PropertyConverter.escapeListDelimiter(txt, listDelimiter);
+ }
+ elem.appendChild(document.createTextNode(txt));
+ }
+ if (sibling2 == null)
+ {
+ getElement(parent).appendChild(elem);
+ }
+ else if (sibling1 != null)
+ {
+ getElement(parent).insertBefore(elem, getElement(sibling1).getNextSibling());
+ }
+ else
+ {
+ getElement(parent).insertBefore(elem, getElement(parent).getFirstChild());
+ }
+ return elem;
+ }
+ }
+
+ /**
+ * Helper method for updating the value of the specified node's
+ * attribute with the given name.
+ *
+ * @param node the affected node
+ * @param elem the element that is associated with this node
+ * @param name the name of the affected attribute
+ * @param listDelimiter the delimiter for attributes with multiple values
+ * @param isAttributeSplittingDisabled true if attribute splitting is disabled.
+ */
+ private static void updateAttribute(Node node, Element elem, String name, char listDelimiter,
+ boolean isAttributeSplittingDisabled)
+ {
+ if (node != null && elem != null)
+ {
+ boolean hasAttribute = false;
+ List<ConfigurationNode> attrs = node.getAttributes(name);
+ StringBuilder buf = new StringBuilder();
+ char delimiter = (listDelimiter != 0) ? listDelimiter : ATTR_VALUE_DELIMITER;
+ for (ConfigurationNode attr : attrs)
+ {
+ if (attr.getValue() != null)
+ {
+ hasAttribute = true;
+ if (buf.length() > 0)
+ {
+ buf.append(delimiter);
+ }
+ String value = isAttributeSplittingDisabled ? attr.getValue().toString()
+ : PropertyConverter.escapeDelimiters(attr.getValue().toString(),
+ delimiter);
+ buf.append(value);
+ }
+ attr.setReference(elem);
+ }
+
+ if (!hasAttribute)
+ {
+ elem.removeAttribute(name);
+ }
+ else
+ {
+ elem.setAttribute(name, buf.toString());
+ }
+ }
+ }
+
+ /**
+ * Updates the value of the specified attribute of the given node.
+ * Because there can be multiple child nodes representing this attribute
+ * the new value is determined by iterating over all those child nodes.
+ *
+ * @param node the affected node
+ * @param name the name of the attribute
+ * @param listDelimiter the delimiter for attributes with multiple values
+ * @param isAttributeSplittingDisabled true if attributes splitting is disabled.
+ */
+ static void updateAttribute(Node node, String name, char listDelimiter,
+ boolean isAttributeSplittingDisabled)
+ {
+ if (node != null)
+ {
+ updateAttribute(node, (Element) node.getReference(), name, listDelimiter,
+ isAttributeSplittingDisabled);
+ }
+ }
+
+ /**
+ * Helper method for accessing the element of the specified node.
+ *
+ * @param node the node
+ * @return the element of this node
+ */
+ private Element getElement(Node node)
+ {
+ // special treatment for root node of the hierarchy
+ return (node.getName() != null && node.getReference() != null) ? (Element) node
+ .getReference()
+ : document.getDocumentElement();
+ }
+ }
+
+ /**
+ * A special implementation of the {@code FileConfiguration} interface that is
+ * used internally to implement the {@code FileConfiguration} methods
+ * for {@code XMLConfiguration}, too.
+ */
+ private class XMLFileConfigurationDelegate extends FileConfigurationDelegate
+ {
+ @Override
+ public void load(InputStream in) throws ConfigurationException
+ {
+ XMLConfiguration.this.load(in);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/XMLPropertiesConfiguration.java b/src/main/java/org/apache/commons/configuration/XMLPropertiesConfiguration.java
new file mode 100644
index 0000000..63f55eb
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/XMLPropertiesConfiguration.java
@@ -0,0 +1,425 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URL;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.lang.StringUtils;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.Attributes;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * This configuration implements the XML properties format introduced in Java
+ * 5.0, see http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html.
+ * An XML properties file looks like this:
+ *
+ * <pre>
+ * <?xml version="1.0"?>
+ * <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
+ * <properties>
+ * <comment>Description of the property list</comment>
+ * <entry key="key1">value1</entry>
+ * <entry key="key2">value2</entry>
+ * <entry key="key3">value3</entry>
+ * </properties>
+ * </pre>
+ *
+ * The Java 5.0 runtime is not required to use this class. The default encoding
+ * for this configuration format is UTF-8. Note that unlike
+ * {@code PropertiesConfiguration}, {@code XMLPropertiesConfiguration}
+ * does not support includes.
+ *
+ * <em>Note:</em>Configuration objects of this type can be read concurrently
+ * by multiple threads. However if one of these threads modifies the object,
+ * synchronization has to be performed manually.
+ *
+ * @author Emmanuel Bourg
+ * @author Alistair Young
+ * @version $Id: XMLPropertiesConfiguration.java 1534399 2013-10-21 22:25:03Z henning $
+ * @since 1.1
+ */
+public class XMLPropertiesConfiguration extends PropertiesConfiguration
+{
+ /**
+ * The default encoding (UTF-8 as specified by http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html)
+ */
+ private static final String DEFAULT_ENCODING = "UTF-8";
+
+ /**
+ * Default string used when the XML is malformed
+ */
+ private static final String MALFORMED_XML_EXCEPTION = "Malformed XML";
+
+ // initialization block to set the encoding before loading the file in the constructors
+ {
+ setEncoding(DEFAULT_ENCODING);
+ }
+
+ /**
+ * Creates an empty XMLPropertyConfiguration object which can be
+ * used to synthesize a new Properties file by adding values and
+ * then saving(). An object constructed by this C'tor can not be
+ * tickled into loading included files because it cannot supply a
+ * base for relative includes.
+ */
+ public XMLPropertiesConfiguration()
+ {
+ super();
+ }
+
+ /**
+ * Creates and loads the xml properties from the specified file.
+ * The specified file can contain "include" properties which then
+ * are loaded and merged into the properties.
+ *
+ * @param fileName The name of the properties file to load.
+ * @throws ConfigurationException Error while loading the properties file
+ */
+ public XMLPropertiesConfiguration(String fileName) throws ConfigurationException
+ {
+ super(fileName);
+ }
+
+ /**
+ * Creates and loads the xml properties from the specified file.
+ * The specified file can contain "include" properties which then
+ * are loaded and merged into the properties.
+ *
+ * @param file The properties file to load.
+ * @throws ConfigurationException Error while loading the properties file
+ */
+ public XMLPropertiesConfiguration(File file) throws ConfigurationException
+ {
+ super(file);
+ }
+
+ /**
+ * Creates and loads the xml properties from the specified URL.
+ * The specified file can contain "include" properties which then
+ * are loaded and merged into the properties.
+ *
+ * @param url The location of the properties file to load.
+ * @throws ConfigurationException Error while loading the properties file
+ */
+ public XMLPropertiesConfiguration(URL url) throws ConfigurationException
+ {
+ super(url);
+ }
+
+ /**
+ * Creates and loads the xml properties from the specified DOM node.
+ *
+ * @param element The DOM element
+ * @throws ConfigurationException Error while loading the properties file
+ * @since 2.0
+ */
+ public XMLPropertiesConfiguration(Element element) throws ConfigurationException
+ {
+ super();
+ this.load(element);
+ }
+
+ @Override
+ public void load(Reader in) throws ConfigurationException
+ {
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ factory.setNamespaceAware(false);
+ factory.setValidating(true);
+
+ try
+ {
+ SAXParser parser = factory.newSAXParser();
+
+ XMLReader xmlReader = parser.getXMLReader();
+ xmlReader.setEntityResolver(new EntityResolver()
+ {
+ public InputSource resolveEntity(String publicId, String systemId)
+ {
+ return new InputSource(getClass().getClassLoader().getResourceAsStream("properties.dtd"));
+ }
+ });
+ xmlReader.setContentHandler(new XMLPropertiesHandler());
+ xmlReader.parse(new InputSource(in));
+ }
+ catch (Exception e)
+ {
+ throw new ConfigurationException("Unable to parse the configuration file", e);
+ }
+
+ // todo: support included properties ?
+ }
+
+ /**
+ * Parses a DOM element containing the properties. The DOM element has to follow
+ * the XML properties format introduced in Java 5.0,
+ * see http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html
+ *
+ * @param element The DOM element
+ * @throws ConfigurationException Error while interpreting the DOM
+ * @since 2.0
+ */
+ public void load(Element element) throws ConfigurationException
+ {
+ if (!element.getNodeName().equals("properties"))
+ {
+ throw new ConfigurationException(MALFORMED_XML_EXCEPTION);
+ }
+ NodeList childNodes = element.getChildNodes();
+ for (int i = 0; i < childNodes.getLength(); i++)
+ {
+ Node item = childNodes.item(i);
+ if (item instanceof Element)
+ {
+ if (item.getNodeName().equals("comment"))
+ {
+ setHeader(item.getTextContent());
+ }
+ else if (item.getNodeName().equals("entry"))
+ {
+ String key = ((Element) item).getAttribute("key");
+ addProperty(key, item.getTextContent());
+ }
+ else
+ {
+ throw new ConfigurationException(MALFORMED_XML_EXCEPTION);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void save(Writer out) throws ConfigurationException
+ {
+ PrintWriter writer = new PrintWriter(out);
+
+ String encoding = getEncoding() != null ? getEncoding() : DEFAULT_ENCODING;
+ writer.println("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>");
+ writer.println("<!DOCTYPE properties SYSTEM \"http://java.sun.com/dtd/properties.dtd\">");
+ writer.println("<properties>");
+
+ if (getHeader() != null)
+ {
+ writer.println(" <comment>" + StringEscapeUtils.escapeXml(getHeader()) + "</comment>");
+ }
+
+ Iterator<String> keys = getKeys();
+ while (keys.hasNext())
+ {
+ String key = keys.next();
+ Object value = getProperty(key);
+
+ if (value instanceof List)
+ {
+ writeProperty(writer, key, (List<?>) value);
+ }
+ else
+ {
+ writeProperty(writer, key, value);
+ }
+ }
+
+ writer.println("</properties>");
+ writer.flush();
+ }
+
+ /**
+ * Write a property.
+ *
+ * @param out the output stream
+ * @param key the key of the property
+ * @param value the value of the property
+ */
+ private void writeProperty(PrintWriter out, String key, Object value)
+ {
+ // escape the key
+ String k = StringEscapeUtils.escapeXml(key);
+
+ if (value != null)
+ {
+ // escape the value
+ String v = StringEscapeUtils.escapeXml(String.valueOf(value));
+ v = StringUtils.replace(v, String.valueOf(getListDelimiter()), "\\" + getListDelimiter());
+
+ out.println(" <entry key=\"" + k + "\">" + v + "</entry>");
+ }
+ else
+ {
+ out.println(" <entry key=\"" + k + "\"/>");
+ }
+ }
+
+ /**
+ * Write a list property.
+ *
+ * @param out the output stream
+ * @param key the key of the property
+ * @param values a list with all property values
+ */
+ private void writeProperty(PrintWriter out, String key, List<?> values)
+ {
+ for (Object value : values)
+ {
+ writeProperty(out, key, value);
+ }
+ }
+
+ /**
+ * Writes the configuration as child to the given DOM node
+ *
+ * @param document The DOM document to add the configuration to
+ * @param parent The DOM parent node
+ * @since 2.0
+ */
+ public void save(Document document, Node parent)
+ {
+ Element properties = document.createElement("properties");
+ parent.appendChild(properties);
+ if (getHeader() != null)
+ {
+ Element comment = document.createElement("comment");
+ properties.appendChild(comment);
+ comment.setTextContent(StringEscapeUtils.escapeXml(getHeader()));
+ }
+
+ Iterator<String> keys = getKeys();
+ while (keys.hasNext())
+ {
+ String key = keys.next();
+ Object value = getProperty(key);
+
+ if (value instanceof List)
+ {
+ writeProperty(document, properties, key, (List<?>) value);
+ }
+ else
+ {
+ writeProperty(document, properties, key, value);
+ }
+ }
+ }
+
+ private void writeProperty(Document document, Node properties, String key, Object value)
+ {
+ Element entry = document.createElement("entry");
+ properties.appendChild(entry);
+
+ // escape the key
+ String k = StringEscapeUtils.escapeXml(key);
+ entry.setAttribute("key", k);
+
+ if (value != null)
+ {
+ // escape the value
+ String v = StringEscapeUtils.escapeXml(String.valueOf(value));
+ v = StringUtils.replace(v, String.valueOf(getListDelimiter()), "\\" + getListDelimiter());
+ entry.setTextContent(v);
+ }
+ }
+
+ private void writeProperty(Document document, Node properties, String key, List<?> values)
+ {
+ for (Object value : values)
+ {
+ writeProperty(document, properties, key, value);
+ }
+ }
+
+ /**
+ * SAX Handler to parse a XML properties file.
+ *
+ * @author Alistair Young
+ * @since 1.2
+ */
+ private class XMLPropertiesHandler extends DefaultHandler
+ {
+ /** The key of the current entry being parsed. */
+ private String key;
+
+ /** The value of the current entry being parsed. */
+ private StringBuilder value = new StringBuilder();
+
+ /** Indicates that a comment is being parsed. */
+ private boolean inCommentElement;
+
+ /** Indicates that an entry is being parsed. */
+ private boolean inEntryElement;
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attrs)
+ {
+ if ("comment".equals(qName))
+ {
+ inCommentElement = true;
+ }
+
+ if ("entry".equals(qName))
+ {
+ key = attrs.getValue("key");
+ inEntryElement = true;
+ }
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName)
+ {
+ if (inCommentElement)
+ {
+ // We've just finished a <comment> element so set the header
+ setHeader(value.toString());
+ inCommentElement = false;
+ }
+
+ if (inEntryElement)
+ {
+ // We've just finished an <entry> element, so add the key/value pair
+ addProperty(key, value.toString());
+ inEntryElement = false;
+ }
+
+ // Clear the element value buffer
+ value = new StringBuilder();
+ }
+
+ @Override
+ public void characters(char[] chars, int start, int length)
+ {
+ /**
+ * We're currently processing an element. All character data from now until
+ * the next endElement() call will be the data for this element.
+ */
+ value.append(chars, start, length);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/beanutils/BeanDeclaration.java b/src/main/java/org/apache/commons/configuration/beanutils/BeanDeclaration.java
new file mode 100644
index 0000000..47d0bd6
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/beanutils/BeanDeclaration.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.beanutils;
+
+import java.util.Map;
+
+/**
+ * <p>
+ * Definition of an interface for declaring a bean in a configuration file.
+ * </p>
+ * <p>
+ * Commons Configurations allows to define beans (i.e. simple Java objects) in
+ * configuration files, which can be created at runtime. This is especially
+ * useful if you program against interfaces and want to define the concrete
+ * implementation class is a configuration file.
+ * </p>
+ * <p>
+ * This interface defines methods for retrieving all information about a bean
+ * that should be created from a configuration file, e.g. the bean's properties
+ * or the factory to use for creating the instance. With different
+ * implementations different "layouts" of bean declarations can be
+ * supported. For instance if an XML configuration file is used, all features of
+ * XML (e.g. attributes, nested elements) can be used to define the bean. In a
+ * properties file the declaration format is more limited. The purpose of this
+ * interface is to abstract from the concrete declaration format.
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: BeanDeclaration.java 1208756 2011-11-30 20:37:32Z oheger $
+ */
+public interface BeanDeclaration
+{
+ /**
+ * Returns the name of the {@code BeanFactory} that should be used
+ * for creating the bean instance. This can be <b>null</b>, then a default
+ * factory will be used.
+ *
+ * @return the name of the bean factory
+ */
+ String getBeanFactoryName();
+
+ /**
+ * Here an arbitrary object can be returned that will be passed to the bean
+ * factory. Its meaning is not further specified. The purpose of this
+ * additional parameter is to support a further configuration of the bean
+ * factory that can be placed directly at the bean declaration.
+ *
+ * @return a parameter for the bean factory
+ */
+ Object getBeanFactoryParameter();
+
+ /**
+ * Returns the name of the bean class, from which an instance is to be
+ * created. This value must be defined unless a default class is provided
+ * for the bean creation operation.
+ *
+ * @return the name of the bean class
+ */
+ String getBeanClassName();
+
+ /**
+ * Returns a map with properties that should be initialized on the newly
+ * created bean. The map's keys are the names of the properties; the
+ * corresponding values are the properties' values. The return value can be
+ * <b>null</b> if no properties should be set.
+ *
+ * @return a map with properties to be initialized
+ */
+ Map<String, Object> getBeanProperties();
+
+ /**
+ * Returns a map with declarations for beans that should be set as
+ * properties of the newly created bean. This allows for complex
+ * initialization scenarios: a bean for a bean that contains complex
+ * properties (e.g. other beans) can have nested declarations for defining
+ * these complex properties. The returned map's key are the names of the
+ * properties to initialize. The values are either {@code BeanDeclaration}
+ * implementations or collections thereof. They will be treated like this
+ * declaration (in a* recursive manner), and the resulting beans are
+ * assigned to the corresponding properties.
+ *
+ * @return a map with nested bean declarations
+ */
+ Map<String, Object> getNestedBeanDeclarations();
+}
diff --git a/src/main/java/org/apache/commons/configuration/beanutils/BeanFactory.java b/src/main/java/org/apache/commons/configuration/beanutils/BeanFactory.java
new file mode 100644
index 0000000..3762fcc
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/beanutils/BeanFactory.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.beanutils;
+
+/**
+ * <p>
+ * Definition of an interface for bean factories.
+ * </p>
+ * <p>
+ * Beans defined in configuration files are not directly created, but by so
+ * called <em>bean factories</em>. This additional level of indirection
+ * provides for high flexibility in the creation process. For instance one
+ * implementation of this interface could be very simple and create a new
+ * instance of the specified class for each invocation. A different
+ * implementation could cache already created beans and ensure that always the
+ * same bean of the given class will be returned - this would be an easy mean
+ * for creating singleton objects.
+ * </p>
+ * <p>
+ * The interface itself is quite simple. There is a single method for creating a
+ * bean of a given class. All necessary parameters are obtained from an also
+ * passed in {@link BeanDeclaration} object. It is also possible
+ * (but optional) for a bean factory to declare the default class of the bean it
+ * creates. Then it is not necessary to specify a bean class in the bean
+ * declaration.
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: BeanFactory.java 1208757 2011-11-30 20:38:15Z oheger $
+ */
+public interface BeanFactory
+{
+ /**
+ * Returns a bean instance for the given class. The bean will be initialized
+ * from the specified bean declaration object. It is up to a concrete
+ * implementation how the bean will be created and initialized.
+ *
+ * @param beanClass the class for the bean
+ * @param data the bean declaration object containing all data about the
+ * bean to be created
+ * @param param an additional parameter that may be passed by calling code;
+ * it is up to a concrete implementation how this parameter is evaluated
+ * @return the new bean instance (should not be <b>null</b>)
+ * @throws Exception if an error occurs (the helper classes for creating
+ * beans will catch this unspecific exception and wrap it in a configuration
+ * exception)
+ */
+ Object createBean(Class<?> beanClass, BeanDeclaration data, Object param)
+ throws Exception;
+
+ /**
+ * Returns the default bean class of this bean factory. If an implementation
+ * here returns a non <b>null</b> value, bean declarations using this
+ * factory do not need to provide the name of the bean class. In such a case
+ * an instance of the default class will be created.
+ *
+ * @return the default class of this factory or <b>null</b> if there is
+ * none
+ */
+ Class<?> getDefaultBeanClass();
+}
diff --git a/src/main/java/org/apache/commons/configuration/beanutils/BeanHelper.java b/src/main/java/org/apache/commons/configuration/beanutils/BeanHelper.java
new file mode 100644
index 0000000..53f1c2c
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/beanutils/BeanHelper.java
@@ -0,0 +1,521 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.beanutils;
+
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.commons.beanutils.BeanUtils;
+import org.apache.commons.beanutils.PropertyUtils;
+import org.apache.commons.configuration.ConfigurationRuntimeException;
+import org.apache.commons.lang.ClassUtils;
+
+/**
+ * <p>
+ * A helper class for creating bean instances that are defined in configuration
+ * files.
+ * </p>
+ * <p>
+ * This class provides static utility methods related to bean creation
+ * operations. These methods simplify such operations because a client need not
+ * deal with all involved interfaces. Usually, if a bean declaration has already
+ * been obtained, a single method call is necessary to create a new bean
+ * instance.
+ * </p>
+ * <p>
+ * This class also supports the registration of custom bean factories.
+ * Implementations of the {@link BeanFactory} interface can be
+ * registered under a symbolic name using the {@code registerBeanFactory()}
+ * method. In the configuration file the name of the bean factory can be
+ * specified in the bean declaration. Then this factory will be used to create
+ * the bean.
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: BeanHelper.java 1534393 2013-10-21 22:02:27Z henning $
+ */
+public final class BeanHelper
+{
+ /** Stores a map with the registered bean factories. */
+ private static final Map<String, BeanFactory> BEAN_FACTORIES = Collections
+ .synchronizedMap(new HashMap<String, BeanFactory>());
+
+ /**
+ * Stores the default bean factory, which will be used if no other factory
+ * is provided.
+ */
+ private static BeanFactory defaultBeanFactory = DefaultBeanFactory.INSTANCE;
+
+ /**
+ * Private constructor, so no instances can be created.
+ */
+ private BeanHelper()
+ {
+ }
+
+ /**
+ * Register a bean factory under a symbolic name. This factory object can
+ * then be specified in bean declarations with the effect that this factory
+ * will be used to obtain an instance for the corresponding bean
+ * declaration.
+ *
+ * @param name the name of the factory
+ * @param factory the factory to be registered
+ */
+ public static void registerBeanFactory(String name, BeanFactory factory)
+ {
+ if (name == null)
+ {
+ throw new IllegalArgumentException(
+ "Name for bean factory must not be null!");
+ }
+ if (factory == null)
+ {
+ throw new IllegalArgumentException("Bean factory must not be null!");
+ }
+
+ BEAN_FACTORIES.put(name, factory);
+ }
+
+ /**
+ * Deregisters the bean factory with the given name. After that this factory
+ * cannot be used any longer.
+ *
+ * @param name the name of the factory to be deregistered
+ * @return the factory that was registered under this name; <b>null</b> if
+ * there was no such factory
+ */
+ public static BeanFactory deregisterBeanFactory(String name)
+ {
+ return BEAN_FACTORIES.remove(name);
+ }
+
+ /**
+ * Returns a set with the names of all currently registered bean factories.
+ *
+ * @return a set with the names of the registered bean factories
+ */
+ public static Set<String> registeredFactoryNames()
+ {
+ return BEAN_FACTORIES.keySet();
+ }
+
+ /**
+ * Returns the default bean factory.
+ *
+ * @return the default bean factory
+ */
+ public static BeanFactory getDefaultBeanFactory()
+ {
+ return defaultBeanFactory;
+ }
+
+ /**
+ * Sets the default bean factory. This factory will be used for all create
+ * operations, for which no special factory is provided in the bean
+ * declaration.
+ *
+ * @param factory the default bean factory (must not be <b>null</b>)
+ */
+ public static void setDefaultBeanFactory(BeanFactory factory)
+ {
+ if (factory == null)
+ {
+ throw new IllegalArgumentException(
+ "Default bean factory must not be null!");
+ }
+ defaultBeanFactory = factory;
+ }
+
+ /**
+ * Initializes the passed in bean. This method will obtain all the bean's
+ * properties that are defined in the passed in bean declaration. These
+ * properties will be set on the bean. If necessary, further beans will be
+ * created recursively.
+ *
+ * @param bean the bean to be initialized
+ * @param data the bean declaration
+ * @throws ConfigurationRuntimeException if a property cannot be set
+ */
+ public static void initBean(Object bean, BeanDeclaration data)
+ throws ConfigurationRuntimeException
+ {
+ initBeanProperties(bean, data);
+
+ Map<String, Object> nestedBeans = data.getNestedBeanDeclarations();
+ if (nestedBeans != null)
+ {
+ if (bean instanceof Collection)
+ {
+ // This is safe because the collection stores the values of the
+ // nested beans.
+ @SuppressWarnings("unchecked")
+ Collection<Object> coll = (Collection<Object>) bean;
+ if (nestedBeans.size() == 1)
+ {
+ Map.Entry<String, Object> e = nestedBeans.entrySet().iterator().next();
+ String propName = e.getKey();
+ Class<?> defaultClass = getDefaultClass(bean, propName);
+ if (e.getValue() instanceof List)
+ {
+ // This is safe, provided that the bean declaration is implemented
+ // correctly.
+ @SuppressWarnings("unchecked")
+ List<BeanDeclaration> decls = (List<BeanDeclaration>) e.getValue();
+ for (BeanDeclaration decl : decls)
+ {
+ coll.add(createBean(decl, defaultClass));
+ }
+ }
+ else
+ {
+ BeanDeclaration decl = (BeanDeclaration) e.getValue();
+ coll.add(createBean(decl, defaultClass));
+ }
+ }
+ }
+ else
+ {
+ for (Map.Entry<String, Object> e : nestedBeans.entrySet())
+ {
+ String propName = e.getKey();
+ Class<?> defaultClass = getDefaultClass(bean, propName);
+
+ Object prop = e.getValue();
+
+ if (prop instanceof Collection)
+ {
+ Collection<Object> beanCollection =
+ createPropertyCollection(propName, defaultClass);
+
+ for (Object elemDef : (Collection<?>) prop)
+ {
+ beanCollection
+ .add(createBean((BeanDeclaration) elemDef));
+ }
+
+ initProperty(bean, propName, beanCollection);
+ }
+ else
+ {
+ initProperty(bean, propName, createBean(
+ (BeanDeclaration) e.getValue(), defaultClass));
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Initializes the beans properties.
+ *
+ * @param bean the bean to be initialized
+ * @param data the bean declaration
+ * @throws ConfigurationRuntimeException if a property cannot be set
+ */
+ public static void initBeanProperties(Object bean, BeanDeclaration data)
+ throws ConfigurationRuntimeException
+ {
+ Map<String, Object> properties = data.getBeanProperties();
+ if (properties != null)
+ {
+ for (Map.Entry<String, Object> e : properties.entrySet())
+ {
+ String propName = e.getKey();
+ initProperty(bean, propName, e.getValue());
+ }
+ }
+ }
+
+ /**
+ * Return the Class of the property if it can be determined.
+ * @param bean The bean containing the property.
+ * @param propName The name of the property.
+ * @return The class associated with the property or null.
+ */
+ private static Class<?> getDefaultClass(Object bean, String propName)
+ {
+ try
+ {
+ PropertyDescriptor desc = PropertyUtils.getPropertyDescriptor(bean, propName);
+ if (desc == null)
+ {
+ return null;
+ }
+ return desc.getPropertyType();
+ }
+ catch (Exception ex)
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Sets a property on the given bean using Common Beanutils.
+ *
+ * @param bean the bean
+ * @param propName the name of the property
+ * @param value the property's value
+ * @throws ConfigurationRuntimeException if the property is not writeable or
+ * an error occurred
+ */
+ private static void initProperty(Object bean, String propName, Object value)
+ throws ConfigurationRuntimeException
+ {
+ if (!PropertyUtils.isWriteable(bean, propName))
+ {
+ throw new ConfigurationRuntimeException("Property " + propName
+ + " cannot be set on " + bean.getClass().getName());
+ }
+
+ try
+ {
+ BeanUtils.setProperty(bean, propName, value);
+ }
+ catch (IllegalAccessException iaex)
+ {
+ throw new ConfigurationRuntimeException(iaex);
+ }
+ catch (InvocationTargetException itex)
+ {
+ throw new ConfigurationRuntimeException(itex);
+ }
+ }
+
+ /**
+ * Creates a concrete collection instance to populate a property of type
+ * collection. This method tries to guess an appropriate collection type.
+ * Mostly the type of the property will be one of the collection interfaces
+ * rather than a concrete class; so we have to create a concrete equivalent.
+ *
+ * @param propName the name of the collection property
+ * @param propertyClass the type of the property
+ * @return the newly created collection
+ */
+ private static Collection<Object> createPropertyCollection(String propName,
+ Class<?> propertyClass)
+ {
+ Collection<Object> beanCollection = null;
+
+ if (List.class.isAssignableFrom(propertyClass))
+ {
+ beanCollection = new ArrayList<Object>();
+ }
+ else if (Set.class.isAssignableFrom(propertyClass))
+ {
+ beanCollection = new TreeSet<Object>();
+ }
+ else
+ {
+ throw new UnsupportedOperationException(
+ "Unable to handle collection of type : "
+ + propertyClass.getName() + " for property "
+ + propName);
+ }
+ return beanCollection;
+ }
+
+ /**
+ * Set a property on the bean only if the property exists
+ *
+ * @param bean the bean
+ * @param propName the name of the property
+ * @param value the property's value
+ * @throws ConfigurationRuntimeException if the property is not writeable or
+ * an error occurred
+ */
+ public static void setProperty(Object bean, String propName, Object value)
+ {
+ if (PropertyUtils.isWriteable(bean, propName))
+ {
+ initProperty(bean, propName, value);
+ }
+ }
+
+ /**
+ * The main method for creating and initializing beans from a configuration.
+ * This method will return an initialized instance of the bean class
+ * specified in the passed in bean declaration. If this declaration does not
+ * contain the class of the bean, the passed in default class will be used.
+ * From the bean declaration the factory to be used for creating the bean is
+ * queried. The declaration may here return <b>null</b>, then a default
+ * factory is used. This factory is then invoked to perform the create
+ * operation.
+ *
+ * @param data the bean declaration
+ * @param defaultClass the default class to use
+ * @param param an additional parameter that will be passed to the bean
+ * factory; some factories may support parameters and behave different
+ * depending on the value passed in here
+ * @return the new bean
+ * @throws ConfigurationRuntimeException if an error occurs
+ */
+ public static Object createBean(BeanDeclaration data, Class<?> defaultClass,
+ Object param) throws ConfigurationRuntimeException
+ {
+ if (data == null)
+ {
+ throw new IllegalArgumentException(
+ "Bean declaration must not be null!");
+ }
+
+ BeanFactory factory = fetchBeanFactory(data);
+ try
+ {
+ return factory.createBean(fetchBeanClass(data, defaultClass,
+ factory), data, param);
+ }
+ catch (Exception ex)
+ {
+ throw new ConfigurationRuntimeException(ex);
+ }
+ }
+
+ /**
+ * Returns a bean instance for the specified declaration. This method is a
+ * short cut for {@code createBean(data, null, null);}.
+ *
+ * @param data the bean declaration
+ * @param defaultClass the class to be used when in the declaration no class
+ * is specified
+ * @return the new bean
+ * @throws ConfigurationRuntimeException if an error occurs
+ */
+ public static Object createBean(BeanDeclaration data, Class<?> defaultClass)
+ throws ConfigurationRuntimeException
+ {
+ return createBean(data, defaultClass, null);
+ }
+
+ /**
+ * Returns a bean instance for the specified declaration. This method is a
+ * short cut for {@code createBean(data, null);}.
+ *
+ * @param data the bean declaration
+ * @return the new bean
+ * @throws ConfigurationRuntimeException if an error occurs
+ */
+ public static Object createBean(BeanDeclaration data)
+ throws ConfigurationRuntimeException
+ {
+ return createBean(data, null);
+ }
+
+ /**
+ * Returns a {@code java.lang.Class} object for the specified name.
+ * Because class loading can be tricky in some environments the code for
+ * retrieving a class by its name was extracted into this helper method. So
+ * if changes are necessary, they can be made at a single place.
+ *
+ * @param name the name of the class to be loaded
+ * @param callingClass the calling class
+ * @return the class object for the specified name
+ * @throws ClassNotFoundException if the class cannot be loaded
+ */
+ static Class<?> loadClass(String name, Class<?> callingClass)
+ throws ClassNotFoundException
+ {
+ return ClassUtils.getClass(name);
+ }
+
+ /**
+ * Determines the class of the bean to be created. If the bean declaration
+ * contains a class name, this class is used. Otherwise it is checked
+ * whether a default class is provided. If this is not the case, the
+ * factory's default class is used. If this class is undefined, too, an
+ * exception is thrown.
+ *
+ * @param data the bean declaration
+ * @param defaultClass the default class
+ * @param factory the bean factory to use
+ * @return the class of the bean to be created
+ * @throws ConfigurationRuntimeException if the class cannot be determined
+ */
+ private static Class<?> fetchBeanClass(BeanDeclaration data,
+ Class<?> defaultClass, BeanFactory factory)
+ throws ConfigurationRuntimeException
+ {
+ String clsName = data.getBeanClassName();
+ if (clsName != null)
+ {
+ try
+ {
+ return loadClass(clsName, factory.getClass());
+ }
+ catch (ClassNotFoundException cex)
+ {
+ throw new ConfigurationRuntimeException(cex);
+ }
+ }
+
+ if (defaultClass != null)
+ {
+ return defaultClass;
+ }
+
+ Class<?> clazz = factory.getDefaultBeanClass();
+ if (clazz == null)
+ {
+ throw new ConfigurationRuntimeException(
+ "Bean class is not specified!");
+ }
+ return clazz;
+ }
+
+ /**
+ * Obtains the bean factory to use for creating the specified bean. This
+ * method will check whether a factory is specified in the bean declaration.
+ * If this is not the case, the default bean factory will be used.
+ *
+ * @param data the bean declaration
+ * @return the bean factory to use
+ * @throws ConfigurationRuntimeException if the factory cannot be determined
+ */
+ private static BeanFactory fetchBeanFactory(BeanDeclaration data)
+ throws ConfigurationRuntimeException
+ {
+ String factoryName = data.getBeanFactoryName();
+ if (factoryName != null)
+ {
+ BeanFactory factory = BEAN_FACTORIES.get(factoryName);
+ if (factory == null)
+ {
+ throw new ConfigurationRuntimeException(
+ "Unknown bean factory: " + factoryName);
+ }
+ else
+ {
+ return factory;
+ }
+ }
+ else
+ {
+ return getDefaultBeanFactory();
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/beanutils/ConfigurationDynaBean.java b/src/main/java/org/apache/commons/configuration/beanutils/ConfigurationDynaBean.java
new file mode 100644
index 0000000..3475b87
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/beanutils/ConfigurationDynaBean.java
@@ -0,0 +1,244 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.beanutils;
+
+import java.lang.reflect.Array;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.commons.beanutils.DynaBean;
+import org.apache.commons.beanutils.DynaClass;
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.configuration.ConfigurationMap;
+import org.apache.commons.configuration.SubsetConfiguration;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * The <tt>ConfigurationDynaBean</tt> dynamically reads and writes
+ * configurations properties from a wrapped configuration-collection
+ * {@link org.apache.commons.configuration.Configuration} instance. It also
+ * implements a {@link java.util.Map} interface so that it can be used in
+ * JSP 2.0 Expression Language expressions.
+ *
+ * <p>The {@code ConfigurationDynaBean} maps nested and mapped properties
+ * to the appropriate {@code Configuration} subset using the
+ * {@link org.apache.commons.configuration.Configuration#subset}
+ * method. Similarly, indexed properties reference lists of configuration
+ * properties using the
+ * {@link org.apache.commons.configuration.Configuration#getList(String)}
+ * method. Setting an indexed property is supported, too.</p>
+ *
+ * <p>Note: Some of the methods expect that a dot (".") is used as
+ * property delimiter for the wrapped configuration. This is true for most of
+ * the default configurations. Hierarchical configurations, for which a specific
+ * expression engine is set, may cause problems.</p>
+ *
+ * @author <a href="mailto:ricardo.gladwell at btinternet.com">Ricardo Gladwell</a>
+ * @version $Id: ConfigurationDynaBean.java 1366932 2012-07-29 20:06:31Z oheger $
+ * @since 1.0-rc1
+ */
+public class ConfigurationDynaBean extends ConfigurationMap implements DynaBean
+{
+ /** Constant for the property delimiter.*/
+ private static final String PROPERTY_DELIMITER = ".";
+
+ /** The logger.*/
+ private static final Log LOG = LogFactory.getLog(ConfigurationDynaBean.class);
+
+ /**
+ * Creates a new instance of {@code ConfigurationDynaBean} and sets
+ * the configuration this bean is associated with.
+ *
+ * @param configuration the configuration
+ */
+ public ConfigurationDynaBean(Configuration configuration)
+ {
+ super(configuration);
+ if (LOG.isTraceEnabled())
+ {
+ LOG.trace("ConfigurationDynaBean(" + configuration + ")");
+ }
+ }
+
+ public void set(String name, Object value)
+ {
+ if (LOG.isTraceEnabled())
+ {
+ LOG.trace("set(" + name + "," + value + ")");
+ }
+
+ if (value == null)
+ {
+ throw new NullPointerException("Error trying to set property to null.");
+ }
+
+ if (value instanceof Collection)
+ {
+ Collection<?> collection = (Collection<?>) value;
+ for (Object v : collection)
+ {
+ getConfiguration().addProperty(name, v);
+ }
+ }
+ else if (value.getClass().isArray())
+ {
+ int length = Array.getLength(value);
+ for (int i = 0; i < length; i++)
+ {
+ getConfiguration().addProperty(name, Array.get(value, i));
+ }
+ }
+ else
+ {
+ getConfiguration().setProperty(name, value);
+ }
+ }
+
+ public Object get(String name)
+ {
+ if (LOG.isTraceEnabled())
+ {
+ LOG.trace("get(" + name + ")");
+ }
+
+ // get configuration property
+ Object result = getConfiguration().getProperty(name);
+ if (result == null)
+ {
+ // otherwise attempt to create bean from configuration subset
+ Configuration subset = new SubsetConfiguration(getConfiguration(), name, PROPERTY_DELIMITER);
+ if (!subset.isEmpty())
+ {
+ result = new ConfigurationDynaBean(subset);
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug(name + "=[" + result + "]");
+ }
+
+ if (result == null)
+ {
+ throw new IllegalArgumentException("Property '" + name + "' does not exist.");
+ }
+ return result;
+ }
+
+ public boolean contains(String name, String key)
+ {
+ Configuration subset = getConfiguration().subset(name);
+ if (subset == null)
+ {
+ throw new IllegalArgumentException("Mapped property '" + name + "' does not exist.");
+ }
+
+ return subset.containsKey(key);
+ }
+
+ public Object get(String name, int index)
+ {
+ if (!checkIndexedProperty(name))
+ {
+ throw new IllegalArgumentException("Property '" + name
+ + "' is not indexed.");
+ }
+
+ List<Object> list = getConfiguration().getList(name);
+ return list.get(index);
+ }
+
+ public Object get(String name, String key)
+ {
+ Configuration subset = getConfiguration().subset(name);
+ if (subset == null)
+ {
+ throw new IllegalArgumentException("Mapped property '" + name + "' does not exist.");
+ }
+
+ return subset.getProperty(key);
+ }
+
+ public DynaClass getDynaClass()
+ {
+ return new ConfigurationDynaClass(getConfiguration());
+ }
+
+ public void remove(String name, String key)
+ {
+ Configuration subset = new SubsetConfiguration(getConfiguration(), name, PROPERTY_DELIMITER);
+ subset.setProperty(key, null);
+ }
+
+ public void set(String name, int index, Object value)
+ {
+ if (!checkIndexedProperty(name) && index > 0)
+ {
+ throw new IllegalArgumentException("Property '" + name
+ + "' is not indexed.");
+ }
+
+ Object property = getConfiguration().getProperty(name);
+
+ if (property instanceof List)
+ {
+ // This is safe because multiple values of a configuration property
+ // are always stored as lists of type Object.
+ @SuppressWarnings("unchecked")
+ List<Object> list = (List<Object>) property;
+ list.set(index, value);
+ getConfiguration().setProperty(name, list);
+ }
+ else if (property.getClass().isArray())
+ {
+ Array.set(property, index, value);
+ }
+ else if (index == 0)
+ {
+ getConfiguration().setProperty(name, value);
+ }
+ }
+
+ public void set(String name, String key, Object value)
+ {
+ getConfiguration().setProperty(name + "." + key, value);
+ }
+
+ /**
+ * Tests whether the given name references an indexed property. This
+ * implementation tests for properties of type list or array. If the
+ * property does not exist, an exception is thrown.
+ *
+ * @param name the name of the property to check
+ * @return a flag whether this is an indexed property
+ * @throws IllegalArgumentException if the property does not exist
+ */
+ private boolean checkIndexedProperty(String name)
+ {
+ Object property = getConfiguration().getProperty(name);
+
+ if (property == null)
+ {
+ throw new IllegalArgumentException("Property '" + name
+ + "' does not exist.");
+ }
+
+ return (property instanceof List) || property.getClass().isArray();
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/beanutils/ConfigurationDynaClass.java b/src/main/java/org/apache/commons/configuration/beanutils/ConfigurationDynaClass.java
new file mode 100644
index 0000000..5bdb1c7
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/beanutils/ConfigurationDynaClass.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.beanutils;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.commons.beanutils.DynaBean;
+import org.apache.commons.beanutils.DynaClass;
+import org.apache.commons.beanutils.DynaProperty;
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * The <tt>ConfigurationDynaClass</tt> dynamically determines properties for
+ * a {@code ConfigurationDynaBean} from a wrapped configuration-collection
+ * {@link org.apache.commons.configuration.Configuration} instance.
+ *
+ * @author <a href="mailto:ricardo.gladwell at btinternet.com">Ricardo Gladwell</a>
+ * @version $Id: ConfigurationDynaClass.java 1366932 2012-07-29 20:06:31Z oheger $
+ * @since 1.0-rc1
+ */
+public class ConfigurationDynaClass implements DynaClass
+{
+ /** The logger.*/
+ private static final Log LOG = LogFactory.getLog(ConfigurationDynaClass.class);
+
+ /** Stores the associated configuration.*/
+ private final Configuration configuration;
+
+ /**
+ * Construct an instance of a {@code ConfigurationDynaClass}
+ * wrapping the specified {@code Configuration} instance.
+ * @param configuration {@code Configuration} instance.
+ */
+ public ConfigurationDynaClass(Configuration configuration)
+ {
+ super();
+ if (LOG.isTraceEnabled())
+ {
+ LOG.trace("ConfigurationDynaClass(" + configuration + ")");
+ }
+ this.configuration = configuration;
+ }
+
+ public DynaProperty getDynaProperty(String name)
+ {
+ if (LOG.isTraceEnabled())
+ {
+ LOG.trace("getDynaProperty(" + name + ")");
+ }
+
+ if (name == null)
+ {
+ throw new IllegalArgumentException("Property name must not be null!");
+ }
+
+ Object value = configuration.getProperty(name);
+ if (value == null)
+ {
+ return null;
+ }
+ else
+ {
+ Class<?> type = value.getClass();
+
+ if (type == Byte.class)
+ {
+ type = Byte.TYPE;
+ }
+ if (type == Character.class)
+ {
+ type = Character.TYPE;
+ }
+ else if (type == Boolean.class)
+ {
+ type = Boolean.TYPE;
+ }
+ else if (type == Double.class)
+ {
+ type = Double.TYPE;
+ }
+ else if (type == Float.class)
+ {
+ type = Float.TYPE;
+ }
+ else if (type == Integer.class)
+ {
+ type = Integer.TYPE;
+ }
+ else if (type == Long.class)
+ {
+ type = Long.TYPE;
+ }
+ else if (type == Short.class)
+ {
+ type = Short.TYPE;
+ }
+
+ return new DynaProperty(name, type);
+ }
+ }
+
+ public DynaProperty[] getDynaProperties()
+ {
+ if (LOG.isTraceEnabled())
+ {
+ LOG.trace("getDynaProperties()");
+ }
+
+ Iterator<String> keys = configuration.getKeys();
+ List<DynaProperty> properties = new ArrayList<DynaProperty>();
+ while (keys.hasNext())
+ {
+ String key = keys.next();
+ DynaProperty property = getDynaProperty(key);
+ properties.add(property);
+ }
+
+ DynaProperty[] propertyArray = new DynaProperty[properties.size()];
+ properties.toArray(propertyArray);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Found " + properties.size() + " properties.");
+ }
+
+ return propertyArray;
+ }
+
+ public String getName()
+ {
+ return ConfigurationDynaBean.class.getName();
+ }
+
+ public DynaBean newInstance() throws IllegalAccessException, InstantiationException
+ {
+ return new ConfigurationDynaBean(configuration);
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/beanutils/DefaultBeanFactory.java b/src/main/java/org/apache/commons/configuration/beanutils/DefaultBeanFactory.java
new file mode 100644
index 0000000..a3a501c
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/beanutils/DefaultBeanFactory.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.beanutils;
+
+/**
+ * <p>
+ * The default implementation of the {@code BeanFactory} interface.
+ * </p>
+ * <p>
+ * This class creates beans of arbitrary types using reflection. Each time the
+ * {@code createBean()} method is invoked, a new bean instance is
+ * created. A default bean class is not supported.
+ * </p>
+ * <p>
+ * An instance of this factory class will be set as the default bean factory for
+ * the {@link BeanHelper} class. This means that if not bean
+ * factory is specified in a {@link BeanDeclaration}, this
+ * default instance will be used.
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: DefaultBeanFactory.java 1208758 2011-11-30 20:38:59Z oheger $
+ */
+public class DefaultBeanFactory implements BeanFactory
+{
+ /** Stores the default instance of this class. */
+ public static final DefaultBeanFactory INSTANCE = new DefaultBeanFactory();
+
+ /**
+ * Creates a new bean instance. This implementation delegates to the
+ * protected methods {@code createBeanInstance()} and
+ * {@code initBeanInstance()} for creating and initializing the bean.
+ * This makes it easier for derived classes that need to change specific
+ * functionality of the base class.
+ *
+ * @param beanClass the class of the bean, from which an instance is to be
+ * created
+ * @param data the bean declaration object
+ * @param parameter an additional parameter (ignored by this implementation)
+ * @return the new bean instance
+ * @throws Exception if an error occurs
+ */
+ public Object createBean(Class<?> beanClass, BeanDeclaration data,
+ Object parameter) throws Exception
+ {
+ Object result = createBeanInstance(beanClass, data);
+ initBeanInstance(result, data);
+ return result;
+ }
+
+ /**
+ * Returns the default bean class used by this factory. This is always
+ * <b>null</b> for this implementation.
+ *
+ * @return the default bean class
+ */
+ public Class<?> getDefaultBeanClass()
+ {
+ return null;
+ }
+
+ /**
+ * Creates the bean instance. This method is called by
+ * {@code createBean()}. It uses reflection to create a new instance
+ * of the specified class.
+ *
+ * @param beanClass the class of the bean to be created
+ * @param data the bean declaration
+ * @return the new bean instance
+ * @throws Exception if an error occurs
+ */
+ protected Object createBeanInstance(Class<?> beanClass, BeanDeclaration data)
+ throws Exception
+ {
+ return beanClass.newInstance();
+ }
+
+ /**
+ * Initializes the newly created bean instance. This method is called by
+ * {@code createBean()}. It calls the
+ * {@link BeanHelper#initBean(Object, BeanDeclaration) initBean()}
+ * of {@link BeanHelper} for performing the initialization.
+ *
+ * @param bean the newly created bean instance
+ * @param data the bean declaration object
+ * @throws Exception if an error occurs
+ */
+ protected void initBeanInstance(Object bean, BeanDeclaration data)
+ throws Exception
+ {
+ BeanHelper.initBean(bean, data);
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/beanutils/XMLBeanDeclaration.java b/src/main/java/org/apache/commons/configuration/beanutils/XMLBeanDeclaration.java
new file mode 100644
index 0000000..e3fb5de
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/beanutils/XMLBeanDeclaration.java
@@ -0,0 +1,428 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.beanutils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.configuration.ConfigurationRuntimeException;
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.apache.commons.configuration.PropertyConverter;
+import org.apache.commons.configuration.SubnodeConfiguration;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.DefaultConfigurationNode;
+
+/**
+ * <p>
+ * An implementation of the {@code BeanDeclaration} interface that is
+ * suitable for XML configuration files.
+ * </p>
+ * <p>
+ * This class defines the standard layout of a bean declaration in an XML
+ * configuration file. Such a declaration must look like the following example
+ * fragment:
+ * </p>
+ * <p>
+ *
+ * <pre>
+ * ...
+ * <personBean config-class="my.model.PersonBean"
+ * lastName="Doe" firstName="John">
+ * <address config-class="my.model.AddressBean"
+ * street="21st street 11" zip="1234"
+ * city="TestCity"/>
+ * </personBean>
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * The bean declaration can be contained in an arbitrary element. Here it is the
+ * {@code personBean} element. In the attributes of this element
+ * there can occur some reserved attributes, which have the following meaning:
+ * <dl>
+ * <dt>{@code config-class}</dt>
+ * <dd>Here the full qualified name of the bean's class can be specified. An
+ * instance of this class will be created. If this attribute is not specified,
+ * the bean class must be provided in another way, e.g. as the
+ * {@code defaultClass} passed to the {@code BeanHelper} class.</dd>
+ * <dt>{@code config-factory}</dt>
+ * <dd>This attribute can contain the name of the
+ * {@link BeanFactory} that should be used for creating the bean.
+ * If it is defined, a factory with this name must have been registered at the
+ * {@code BeanHelper} class. If this attribute is missing, the default
+ * bean factory will be used.</dd>
+ * <dt>{@code config-factoryParam}</dt>
+ * <dd>With this attribute a parameter can be specified that will be passed to
+ * the bean factory. This may be useful for custom bean factories.</dd>
+ * </dl>
+ * </p>
+ * <p>
+ * All further attributes starting with the {@code config-} prefix are
+ * considered as meta data and will be ignored. All other attributes are treated
+ * as properties of the bean to be created, i.e. corresponding setter methods of
+ * the bean will be invoked with the values specified here.
+ * </p>
+ * <p>
+ * If the bean to be created has also some complex properties (which are itself
+ * beans), their values cannot be initialized from attributes. For this purpose
+ * nested elements can be used. The example listing shows how an address bean
+ * can be initialized. This is done in a nested element whose name must match
+ * the name of a property of the enclosing bean declaration. The format of this
+ * nested element is exactly the same as for the bean declaration itself, i.e.
+ * it can have attributes defining meta data or bean properties and even further
+ * nested elements for complex bean properties.
+ * </p>
+ * <p>
+ * A {@code XMLBeanDeclaration} object is usually created from a
+ * {@code HierarchicalConfiguration}. From this it will derive a
+ * {@code SubnodeConfiguration}, which is used to access the needed
+ * properties. This subnode configuration can be obtained using the
+ * {@link #getConfiguration()} method. All of its properties can
+ * be accessed in the usual way. To ensure that the property keys used by this
+ * class are understood by the configuration, the default expression engine will
+ * be set.
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: XMLBeanDeclaration.java 1301959 2012-03-17 16:43:18Z oheger $
+ */
+public class XMLBeanDeclaration implements BeanDeclaration
+{
+ /** Constant for the prefix of reserved attributes. */
+ public static final String RESERVED_PREFIX = "config-";
+
+ /** Constant for the prefix for reserved attributes.*/
+ public static final String ATTR_PREFIX = "[@" + RESERVED_PREFIX;
+
+ /** Constant for the bean class attribute. */
+ public static final String ATTR_BEAN_CLASS = ATTR_PREFIX + "class]";
+
+ /** Constant for the bean factory attribute. */
+ public static final String ATTR_BEAN_FACTORY = ATTR_PREFIX + "factory]";
+
+ /** Constant for the bean factory parameter attribute. */
+ public static final String ATTR_FACTORY_PARAM = ATTR_PREFIX
+ + "factoryParam]";
+
+ /** Stores the associated configuration. */
+ private final SubnodeConfiguration configuration;
+
+ /** Stores the configuration node that contains the bean declaration. */
+ private final ConfigurationNode node;
+
+ /**
+ * Creates a new instance of {@code XMLBeanDeclaration} and
+ * initializes it from the given configuration. The passed in key points to
+ * the bean declaration.
+ *
+ * @param config the configuration
+ * @param key the key to the bean declaration (this key must point to
+ * exactly one bean declaration or a {@code IllegalArgumentException}
+ * exception will be thrown)
+ */
+ public XMLBeanDeclaration(HierarchicalConfiguration config, String key)
+ {
+ this(config, key, false);
+ }
+
+ /**
+ * Creates a new instance of {@code XMLBeanDeclaration} and
+ * initializes it from the given configuration. The passed in key points to
+ * the bean declaration. If the key does not exist and the boolean argument
+ * is <b>true</b>, the declaration is initialized with an empty
+ * configuration. It is possible to create objects from such an empty
+ * declaration if a default class is provided. If the key on the other hand
+ * has multiple values or is undefined and the boolean argument is <b>false</b>,
+ * a {@code IllegalArgumentException} exception will be thrown.
+ *
+ * @param config the configuration
+ * @param key the key to the bean declaration
+ * @param optional a flag whether this declaration is optional; if set to
+ * <b>true</b>, no exception will be thrown if the passed in key is
+ * undefined
+ */
+ public XMLBeanDeclaration(HierarchicalConfiguration config, String key,
+ boolean optional)
+ {
+ if (config == null)
+ {
+ throw new IllegalArgumentException(
+ "Configuration must not be null!");
+ }
+
+ SubnodeConfiguration tmpconfiguration = null;
+ ConfigurationNode tmpnode = null;
+ try
+ {
+ tmpconfiguration = config.configurationAt(key);
+ tmpnode = tmpconfiguration.getRootNode();
+ }
+ catch (IllegalArgumentException iex)
+ {
+ // If we reach this block, the key does not have exactly one value
+ if (!optional || config.getMaxIndex(key) > 0)
+ {
+ throw iex;
+ }
+ tmpconfiguration = config.configurationAt(null);
+ tmpnode = new DefaultConfigurationNode();
+ }
+ this.node = tmpnode;
+ this.configuration = tmpconfiguration;
+ initSubnodeConfiguration(getConfiguration());
+ }
+
+ /**
+ * Creates a new instance of {@code XMLBeanDeclaration} and
+ * initializes it from the given configuration. The configuration's root
+ * node must contain the bean declaration.
+ *
+ * @param config the configuration with the bean declaration
+ */
+ public XMLBeanDeclaration(HierarchicalConfiguration config)
+ {
+ this(config, (String) null);
+ }
+
+ /**
+ * Creates a new instance of {@code XMLBeanDeclaration} and
+ * initializes it with the configuration node that contains the bean
+ * declaration.
+ *
+ * @param config the configuration
+ * @param node the node with the bean declaration.
+ */
+ public XMLBeanDeclaration(SubnodeConfiguration config,
+ ConfigurationNode node)
+ {
+ if (config == null)
+ {
+ throw new IllegalArgumentException(
+ "Configuration must not be null!");
+ }
+ if (node == null)
+ {
+ throw new IllegalArgumentException("Node must not be null!");
+ }
+
+ this.node = node;
+ configuration = config;
+ initSubnodeConfiguration(config);
+ }
+
+ /**
+ * Returns the configuration object this bean declaration is based on.
+ *
+ * @return the associated configuration
+ */
+ public SubnodeConfiguration getConfiguration()
+ {
+ return configuration;
+ }
+
+ /**
+ * Returns the node that contains the bean declaration.
+ *
+ * @return the configuration node this bean declaration is based on
+ */
+ public ConfigurationNode getNode()
+ {
+ return node;
+ }
+
+ /**
+ * Returns the name of the bean factory. This information is fetched from
+ * the {@code config-factory} attribute.
+ *
+ * @return the name of the bean factory
+ */
+ public String getBeanFactoryName()
+ {
+ return getConfiguration().getString(ATTR_BEAN_FACTORY);
+ }
+
+ /**
+ * Returns a parameter for the bean factory. This information is fetched
+ * from the {@code config-factoryParam} attribute.
+ *
+ * @return the parameter for the bean factory
+ */
+ public Object getBeanFactoryParameter()
+ {
+ return getConfiguration().getProperty(ATTR_FACTORY_PARAM);
+ }
+
+ /**
+ * Returns the name of the class of the bean to be created. This information
+ * is obtained from the {@code config-class} attribute.
+ *
+ * @return the name of the bean's class
+ */
+ public String getBeanClassName()
+ {
+ return getConfiguration().getString(ATTR_BEAN_CLASS);
+ }
+
+ /**
+ * Returns a map with the bean's (simple) properties. The properties are
+ * collected from all attribute nodes, which are not reserved.
+ *
+ * @return a map with the bean's properties
+ */
+ public Map<String, Object> getBeanProperties()
+ {
+ Map<String, Object> props = new HashMap<String, Object>();
+ for (ConfigurationNode attr : getNode().getAttributes())
+ {
+ if (!isReservedNode(attr))
+ {
+ props.put(attr.getName(), interpolate(attr .getValue()));
+ }
+ }
+
+ return props;
+ }
+
+ /**
+ * Returns a map with bean declarations for the complex properties of the
+ * bean to be created. These declarations are obtained from the child nodes
+ * of this declaration's root node.
+ *
+ * @return a map with bean declarations for complex properties
+ */
+ public Map<String, Object> getNestedBeanDeclarations()
+ {
+ Map<String, Object> nested = new HashMap<String, Object>();
+ for (ConfigurationNode child : getNode().getChildren())
+ {
+ if (!isReservedNode(child))
+ {
+ if (nested.containsKey(child.getName()))
+ {
+ Object obj = nested.get(child.getName());
+ List<BeanDeclaration> list;
+ if (obj instanceof List)
+ {
+ // Safe because we created the lists ourselves.
+ @SuppressWarnings("unchecked")
+ List<BeanDeclaration> tmpList = (List<BeanDeclaration>) obj;
+ list = tmpList;
+ }
+ else
+ {
+ list = new ArrayList<BeanDeclaration>();
+ list.add((BeanDeclaration) obj);
+ nested.put(child.getName(), list);
+ }
+ list.add(createBeanDeclaration(child));
+ }
+ else
+ {
+ nested.put(child.getName(), createBeanDeclaration(child));
+ }
+ }
+ }
+
+ return nested;
+ }
+
+ /**
+ * Performs interpolation for the specified value. This implementation will
+ * interpolate against the current subnode configuration's parent. If sub
+ * classes need a different interpolation mechanism, they should override
+ * this method.
+ *
+ * @param value the value that is to be interpolated
+ * @return the interpolated value
+ */
+ protected Object interpolate(Object value)
+ {
+ return PropertyConverter.interpolate(value, getConfiguration()
+ .getParent());
+ }
+
+ /**
+ * Checks if the specified node is reserved and thus should be ignored. This
+ * method is called when the maps for the bean's properties and complex
+ * properties are collected. It checks whether the given node is an
+ * attribute node and if its name starts with the reserved prefix.
+ *
+ * @param nd the node to be checked
+ * @return a flag whether this node is reserved (and does not point to a
+ * property)
+ */
+ protected boolean isReservedNode(ConfigurationNode nd)
+ {
+ return nd.isAttribute()
+ && (nd.getName() == null || nd.getName().startsWith(
+ RESERVED_PREFIX));
+ }
+
+ /**
+ * Creates a new {@code BeanDeclaration} for a child node of the
+ * current configuration node. This method is called by
+ * {@code getNestedBeanDeclarations()} for all complex sub properties
+ * detected by this method. Derived classes can hook in if they need a
+ * specific initialization. This base implementation creates a
+ * {@code XMLBeanDeclaration} that is properly initialized from the
+ * passed in node.
+ *
+ * @param node the child node, for which a {@code BeanDeclaration} is
+ * to be created
+ * @return the {@code BeanDeclaration} for this child node
+ * @since 1.6
+ */
+ protected BeanDeclaration createBeanDeclaration(ConfigurationNode node)
+ {
+ List<HierarchicalConfiguration> list = getConfiguration().configurationsAt(node.getName());
+ if (list.size() == 1)
+ {
+ return new XMLBeanDeclaration((SubnodeConfiguration) list.get(0), node);
+ }
+ else
+ {
+ Iterator<HierarchicalConfiguration> iter = list.iterator();
+ while (iter.hasNext())
+ {
+ SubnodeConfiguration config = (SubnodeConfiguration) iter.next();
+ if (config.getRootNode().equals(node))
+ {
+ return new XMLBeanDeclaration(config, node);
+ }
+ }
+ throw new ConfigurationRuntimeException("Unable to match node for " + node.getName());
+ }
+ }
+
+ /**
+ * Initializes the internally managed subnode configuration. This method
+ * will set some default values for some properties.
+ *
+ * @param conf the configuration to initialize
+ */
+ private void initSubnodeConfiguration(SubnodeConfiguration conf)
+ {
+ conf.setThrowExceptionOnMissing(false);
+ conf.setExpressionEngine(null);
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/beanutils/package.html b/src/main/java/org/apache/commons/configuration/beanutils/package.html
new file mode 100644
index 0000000..433cead
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/beanutils/package.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+<head>
+</head>
+<body>
+
+<p>
+In this package a <code>Configuration</code> implementation can be found that
+implements the <code>DynaBean</code> interface. It allows to access or modify
+a configuration using the classes from the Commons Beanutils package.
+There are also classes for declaring beans in configuration files, from which
+then instances can be created.
+</p>
+<p>
+<font size="-2">$Id: package.html 439648 2006-09-02 20:42:10Z oheger $</font>
+</p>
+
+</body>
+</html>
diff --git a/src/main/java/org/apache/commons/configuration/event/ConfigurationErrorEvent.java b/src/main/java/org/apache/commons/configuration/event/ConfigurationErrorEvent.java
new file mode 100644
index 0000000..1979f72
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/event/ConfigurationErrorEvent.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.event;
+
+/**
+ * <p>
+ * An event class that is used for reporting errors that occurred while
+ * processing configuration properties.
+ * </p>
+ * <p>
+ * Some configuration implementations (e.g.
+ * {@link org.apache.commons.configuration.DatabaseConfiguration}
+ * or {@link org.apache.commons.configuration.JNDIConfiguration}
+ * use an underlying storage that can throw an exception on each property
+ * access. In earlier versions of this library such exceptions were logged and
+ * then silently ignored. This makes it impossible for a client to find out that
+ * something went wrong.
+ * </p>
+ * <p>
+ * To give clients better control over the handling of errors that occur during
+ * access of a configuration object a new event listener mechanism specific for
+ * exceptions is introduced: Clients can register itself at a configuration
+ * object as an <em>error listener</em> and are then notified about all
+ * internal errors related to the source configuration object.
+ * </p>
+ * <p>
+ * By inheriting from {@code ConfigurationEvent} this event class
+ * supports all properties that describe an operation on a configuration
+ * instance. In addition a {@code Throwable} object is available
+ * representing the occurred error. The event's type determines the operation
+ * that caused the error. Note that depending on the event type and the occurred
+ * exception not all of the other properties (e.g. name of the affected property
+ * or its value) may be available.
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationErrorEvent.java 1207610 2011-11-28 21:06:22Z oheger $
+ * @since 1.4
+ * @see ConfigurationEvent
+ */
+public class ConfigurationErrorEvent extends ConfigurationEvent
+{
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = -7433184493062648409L;
+
+ /** Stores the exception that caused this event. */
+ private Throwable cause;
+
+ /**
+ * Creates a new instance of {@code ConfigurationErrorEvent} and
+ * initializes it.
+ *
+ * @param source the event source
+ * @param type the event's type
+ * @param propertyName the name of the affected property
+ * @param propertyValue the value of the affected property
+ * @param cause the exception object that caused this event
+ */
+ public ConfigurationErrorEvent(Object source, int type,
+ String propertyName, Object propertyValue, Throwable cause)
+ {
+ super(source, type, propertyName, propertyValue, true);
+ this.cause = cause;
+ }
+
+ /**
+ * Returns the cause of this error event. This is the {@code Throwable}
+ * object that caused this event to be fired.
+ *
+ * @return the cause of this error event
+ */
+ public Throwable getCause()
+ {
+ return cause;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/event/ConfigurationErrorListener.java b/src/main/java/org/apache/commons/configuration/event/ConfigurationErrorListener.java
new file mode 100644
index 0000000..2766891
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/event/ConfigurationErrorListener.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.event;
+
+/**
+ * <p>
+ * An event listener interface to be implemented by observers that are
+ * interested in internal errors caused by processing of configuration
+ * properties.
+ * </p>
+ * <p>
+ * Some configuration classes use an underlying storage where each access of a
+ * property can cause an exception. In earlier versions of this library such
+ * exceptions were typically ignored. By implementing this interface and
+ * registering at a configuration object as an error listener it is now possible
+ * for clients to receive notifications about those internal problems.
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationErrorListener.java 1207610 2011-11-28 21:06:22Z oheger $
+ * @since 1.4
+ * @see ConfigurationErrorEvent
+ */
+public interface ConfigurationErrorListener
+{
+ /**
+ * Notifies this listener that in an observed configuration an error
+ * occurred. All information available about this error, including the
+ * causing {@code Throwable} object, can be obtained from the passed
+ * in event object.
+ *
+ * @param event the event object with information about the error
+ */
+ void configurationError(ConfigurationErrorEvent event);
+}
diff --git a/src/main/java/org/apache/commons/configuration/event/ConfigurationEvent.java b/src/main/java/org/apache/commons/configuration/event/ConfigurationEvent.java
new file mode 100644
index 0000000..2042137
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/event/ConfigurationEvent.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.event;
+
+import java.util.EventObject;
+
+/**
+ * <p>
+ * An event class for reporting updates on a configuration object.
+ * </p>
+ * <p>
+ * Event objects of this type are used for "raw" events, i.e.
+ * unfiltered modifications of any kind. A level with semantically higher events
+ * (e.g. for property changes) may be built on top of this fundamental event
+ * mechanism.
+ * </p>
+ * <p>
+ * Each event can contain the following data:
+ * <ul>
+ * <li>A source object, which is usually the configuration object that was
+ * modified.</li>
+ * <li>The event's type. This is a numeric value that corresponds to constant
+ * declarations in concrete configuration classes. It describes what exactly has
+ * happended.</li>
+ * <li>If available, the name of the property whose modification caused the
+ * event.</li>
+ * <li>If available, the value of the property that caused this event.</li>
+ * <li>A flag whether this event was generated before or after the update of
+ * the source configuration. A modification of a configuration typically causes
+ * two events: one event before and one event after the modification is
+ * performed. This allows event listeners to react at the correct point of time.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * The following standard events are generated by typical configuration
+ * implementations (the constants for the event types are defined in
+ * {@link org.apache.commons.configuration.AbstractConfiguration}):
+ * <dl>
+ * <dt>EVENT_ADD_PROPERTY</dt>
+ * <dd>This event is triggered for each call of the {@code addProperty()}
+ * method of a configuration object. It contains the name of the property, to
+ * which new data is added, and the value object that is added to this property
+ * (this may be an array or a list if multiple values are added).</dd>
+ * <dt>EVENT_SET_PROPERTY</dt>
+ * <dd>Calling the {@code setProperty()} method triggers this event. The
+ * event object stores the name of the affected property and its new value.</dd>
+ * <dt>EVENT_CLEAR_PROPERTY</dt>
+ * <dd>If a property is removed from a configuration (by calling the
+ * {@code clearProperty()} method), an event of this type is fired. In
+ * this case the event object only stores the name of the removed property, the
+ * value is <b>null</b>.</dd>
+ * <dt>EVENT_CLEAR</dt>
+ * <dd>This event is fired when the whole configuration is cleared. The
+ * corresponding event object contains no additional data.</dd>
+ * </dl>
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationEvent.java 1207610 2011-11-28 21:06:22Z oheger $
+ * @since 1.3
+ */
+public class ConfigurationEvent extends EventObject
+{
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = 3277238219073504136L;
+
+ /** Stores the event type. */
+ private int type;
+
+ /** Stores the property name. */
+ private String propertyName;
+
+ /** Stores the property value. */
+ private Object propertyValue;
+
+ /** Stores the before update flag. */
+ private boolean beforeUpdate;
+
+ /**
+ * Creates a new instance of {@code ConfigurationEvent} and
+ * initializes it.
+ *
+ * @param source the event source
+ * @param type the event's type
+ * @param propertyName the name of the affected property
+ * @param propertyValue the value of the affected property
+ * @param beforeUpdate the before update flag
+ */
+ public ConfigurationEvent(Object source, int type, String propertyName,
+ Object propertyValue, boolean beforeUpdate)
+ {
+ super(source);
+ this.type = type;
+ this.propertyName = propertyName;
+ this.propertyValue = propertyValue;
+ this.beforeUpdate = beforeUpdate;
+ }
+
+ /**
+ * Returns the name of the affected property. This can be <b>null</b> if no
+ * property change has lead to this event.
+ *
+ * @return the name of the property
+ */
+ public String getPropertyName()
+ {
+ return propertyName;
+ }
+
+ /**
+ * Returns the value of the affected property if available.
+ *
+ * @return the value of the property; can be <b>null</b>
+ */
+ public Object getPropertyValue()
+ {
+ return propertyValue;
+ }
+
+ /**
+ * Returns the type of this event. This describes the update process that
+ * caused this event.
+ *
+ * @return the event's type
+ */
+ public int getType()
+ {
+ return type;
+ }
+
+ /**
+ * Returns a flag if this event was generated before or after an update.
+ *
+ * @return <b>true</b> if this event was generated before an update;
+ * <b>false</b> otherwise
+ */
+ public boolean isBeforeUpdate()
+ {
+ return beforeUpdate;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/event/ConfigurationListener.java b/src/main/java/org/apache/commons/configuration/event/ConfigurationListener.java
new file mode 100644
index 0000000..9e2e907
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/event/ConfigurationListener.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.event;
+
+/**
+ * <p>
+ * A simple event listener interface for configuration observers.
+ * </p>
+ * <p>
+ * This interface can be implemented by classes that are interested in
+ * "raw" events caused by configuration objects. Each manipulation on
+ * a configuration object will generate such an event. There is only a single
+ * method that is invoked when an event occurs.
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationListener.java 561230 2007-07-31 04:17:09Z rahul $
+ * @since 1.3
+ */
+public interface ConfigurationListener
+{
+ /**
+ * Notifies this listener about a manipulation on a monitored configuration
+ * object.
+ *
+ * @param event the event describing the manipulation
+ */
+ void configurationChanged(ConfigurationEvent event);
+}
diff --git a/src/main/java/org/apache/commons/configuration/event/EventSource.java b/src/main/java/org/apache/commons/configuration/event/EventSource.java
new file mode 100644
index 0000000..7074c5c
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/event/EventSource.java
@@ -0,0 +1,364 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.event;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * <p>
+ * A base class for objects that can generate configuration events.
+ * </p>
+ * <p>
+ * This class implements functionality for managing a set of event listeners
+ * that can be notified when an event occurs. It can be extended by
+ * configuration classes that support the event mechanism. In this case these
+ * classes only need to call the {@code fireEvent()} method when an event
+ * is to be delivered to the registered listeners.
+ * </p>
+ * <p>
+ * Adding and removing event listeners can happen concurrently to manipulations
+ * on a configuration that cause events. The operations are synchronized.
+ * </p>
+ * <p>
+ * With the {@code detailEvents} property the number of detail events can
+ * be controlled. Some methods in configuration classes are implemented in a way
+ * that they call other methods that can generate their own events. One example
+ * is the {@code setProperty()} method that can be implemented as a
+ * combination of {@code clearProperty()} and {@code addProperty()}.
+ * With {@code detailEvents} set to <b>true</b>, all involved methods
+ * will generate events (i.e. listeners will receive property set events,
+ * property clear events, and property add events). If this mode is turned off
+ * (which is the default), detail events are suppressed, so only property set
+ * events will be received. Note that the number of received detail events may
+ * differ for different configuration implementations.
+ * {@link org.apache.commons.configuration.HierarchicalConfiguration HierarchicalConfiguration}
+ * for instance has a custom implementation of {@code setProperty()},
+ * which does not generate any detail events.
+ * </p>
+ * <p>
+ * In addition to "normal" events, error events are supported. Such
+ * events signal an internal problem that occurred during access of properties.
+ * For them a special listener interface exists:
+ * {@link ConfigurationErrorListener}. There is another set of
+ * methods dealing with event listeners of this type. The
+ * {@code fireError()} method can be used by derived classes to send
+ * notifications about errors to registered observers.
+ * </p>
+ *
+ * @author <a href="http://commons.apache.org/configuration/team-list.html">Commons Configuration team</a>
+ * @version $Id: EventSource.java 1234617 2012-01-22 21:31:01Z oheger $
+ * @since 1.3
+ */
+public class EventSource
+{
+ /** A collection for the registered event listeners. */
+ private Collection<ConfigurationListener> listeners;
+
+ /** A collection for the registered error listeners.*/
+ private Collection<ConfigurationErrorListener> errorListeners;
+
+ /** A lock object for guarding access to the detail events counter. */
+ private final Object lockDetailEventsCount = new Object();
+
+ /** A counter for the detail events. */
+ private int detailEvents;
+
+ /**
+ * Creates a new instance of {@code EventSource}.
+ */
+ public EventSource()
+ {
+ initListeners();
+ }
+
+ /**
+ * Adds a configuration listener to this object.
+ *
+ * @param l the listener to add
+ */
+ public void addConfigurationListener(ConfigurationListener l)
+ {
+ checkListener(l);
+ listeners.add(l);
+ }
+
+ /**
+ * Removes the specified event listener so that it does not receive any
+ * further events caused by this object.
+ *
+ * @param l the listener to be removed
+ * @return a flag whether the event listener was found
+ */
+ public boolean removeConfigurationListener(ConfigurationListener l)
+ {
+ return listeners.remove(l);
+ }
+
+ /**
+ * Returns a collection with all configuration event listeners that are
+ * currently registered at this object.
+ *
+ * @return a collection with the registered
+ * {@code ConfigurationListener}s (this collection is a snapshot
+ * of the currently registered listeners; manipulating it has no effect
+ * on this event source object)
+ */
+ public Collection<ConfigurationListener> getConfigurationListeners()
+ {
+ return Collections.unmodifiableCollection(new ArrayList<ConfigurationListener>(listeners));
+ }
+
+ /**
+ * Removes all registered configuration listeners.
+ */
+ public void clearConfigurationListeners()
+ {
+ listeners.clear();
+ }
+
+ /**
+ * Returns a flag whether detail events are enabled.
+ *
+ * @return a flag if detail events are generated
+ */
+ public boolean isDetailEvents()
+ {
+ return checkDetailEvents(0);
+ }
+
+ /**
+ * Determines whether detail events should be generated. If enabled, some
+ * methods can generate multiple update events. Note that this method
+ * records the number of calls, i.e. if for instance
+ * {@code setDetailEvents(false)} was called three times, you will
+ * have to invoke the method as often to enable the details.
+ *
+ * @param enable a flag if detail events should be enabled or disabled
+ */
+ public void setDetailEvents(boolean enable)
+ {
+ synchronized (lockDetailEventsCount)
+ {
+ if (enable)
+ {
+ detailEvents++;
+ }
+ else
+ {
+ detailEvents--;
+ }
+ }
+ }
+
+ /**
+ * Adds a new configuration error listener to this object. This listener
+ * will then be notified about internal problems.
+ *
+ * @param l the listener to register (must not be <b>null</b>)
+ * @since 1.4
+ */
+ public void addErrorListener(ConfigurationErrorListener l)
+ {
+ checkListener(l);
+ errorListeners.add(l);
+ }
+
+ /**
+ * Removes the specified error listener so that it does not receive any
+ * further events caused by this object.
+ *
+ * @param l the listener to remove
+ * @return a flag whether the listener could be found and removed
+ * @since 1.4
+ */
+ public boolean removeErrorListener(ConfigurationErrorListener l)
+ {
+ return errorListeners.remove(l);
+ }
+
+ /**
+ * Removes all registered error listeners.
+ *
+ * @since 1.4
+ */
+ public void clearErrorListeners()
+ {
+ errorListeners.clear();
+ }
+
+ /**
+ * Returns a collection with all configuration error listeners that are
+ * currently registered at this object.
+ *
+ * @return a collection with the registered
+ * {@code ConfigurationErrorListener}s (this collection is a
+ * snapshot of the currently registered listeners; it cannot be manipulated)
+ * @since 1.4
+ */
+ public Collection<ConfigurationErrorListener> getErrorListeners()
+ {
+ return Collections.unmodifiableCollection(new ArrayList<ConfigurationErrorListener>(errorListeners));
+ }
+
+ /**
+ * Creates an event object and delivers it to all registered event
+ * listeners. The method will check first if sending an event is allowed
+ * (making use of the {@code detailEvents} property), and if
+ * listeners are registered.
+ *
+ * @param type the event's type
+ * @param propName the name of the affected property (can be <b>null</b>)
+ * @param propValue the value of the affected property (can be <b>null</b>)
+ * @param before the before update flag
+ */
+ protected void fireEvent(int type, String propName, Object propValue, boolean before)
+ {
+ if (checkDetailEvents(-1))
+ {
+ Iterator<ConfigurationListener> it = listeners.iterator();
+ if (it.hasNext())
+ {
+ ConfigurationEvent event =
+ createEvent(type, propName, propValue, before);
+ while (it.hasNext())
+ {
+ it.next().configurationChanged(event);
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a {@code ConfigurationEvent} object based on the passed in
+ * parameters. This is called by {@code fireEvent()} if it decides
+ * that an event needs to be generated.
+ *
+ * @param type the event's type
+ * @param propName the name of the affected property (can be <b>null</b>)
+ * @param propValue the value of the affected property (can be <b>null</b>)
+ * @param before the before update flag
+ * @return the newly created event object
+ */
+ protected ConfigurationEvent createEvent(int type, String propName, Object propValue, boolean before)
+ {
+ return new ConfigurationEvent(this, type, propName, propValue, before);
+ }
+
+ /**
+ * Creates an error event object and delivers it to all registered error
+ * listeners.
+ *
+ * @param type the event's type
+ * @param propName the name of the affected property (can be <b>null</b>)
+ * @param propValue the value of the affected property (can be <b>null</b>)
+ * @param ex the {@code Throwable} object that caused this error event
+ * @since 1.4
+ */
+ protected void fireError(int type, String propName, Object propValue, Throwable ex)
+ {
+ Iterator<ConfigurationErrorListener> it = errorListeners.iterator();
+ if (it.hasNext())
+ {
+ ConfigurationErrorEvent event =
+ createErrorEvent(type, propName, propValue, ex);
+ while (it.hasNext())
+ {
+ it.next().configurationError(event);
+ }
+ }
+ }
+
+ /**
+ * Creates a {@code ConfigurationErrorEvent} object based on the
+ * passed in parameters. This is called by {@code fireError()} if it
+ * decides that an event needs to be generated.
+ *
+ * @param type the event's type
+ * @param propName the name of the affected property (can be <b>null</b>)
+ * @param propValue the value of the affected property (can be <b>null</b>)
+ * @param ex the {@code Throwable} object that caused this error
+ * event
+ * @return the event object
+ * @since 1.4
+ */
+ protected ConfigurationErrorEvent createErrorEvent(int type, String propName, Object propValue, Throwable ex)
+ {
+ return new ConfigurationErrorEvent(this, type, propName, propValue, ex);
+ }
+
+ /**
+ * Overrides the {@code clone()} method to correctly handle so far
+ * registered event listeners. This implementation ensures that the clone
+ * will have empty event listener lists, i.e. the listeners registered at an
+ * {@code EventSource} object will not be copied.
+ *
+ * @return the cloned object
+ * @throws CloneNotSupportedException if cloning is not allowed
+ * @since 1.4
+ */
+ @Override
+ protected Object clone() throws CloneNotSupportedException
+ {
+ EventSource copy = (EventSource) super.clone();
+ copy.initListeners();
+ return copy;
+ }
+
+ /**
+ * Checks whether the specified event listener is not <b>null</b>. If this
+ * is the case, an {@code IllegalArgumentException} exception is thrown.
+ *
+ * @param l the listener to be checked
+ * @throws IllegalArgumentException if the listener is <b>null</b>
+ */
+ private static void checkListener(Object l)
+ {
+ if (l == null)
+ {
+ throw new IllegalArgumentException("Listener must not be null!");
+ }
+ }
+
+ /**
+ * Initializes the collections for storing registered event listeners.
+ */
+ private void initListeners()
+ {
+ listeners = new CopyOnWriteArrayList<ConfigurationListener>();
+ errorListeners = new CopyOnWriteArrayList<ConfigurationErrorListener>();
+ }
+
+ /**
+ * Helper method for checking the current counter for detail events. This
+ * method checks whether the counter is greater than the passed in limit.
+ *
+ * @param limit the limit to be compared to
+ * @return <b>true</b> if the counter is greater than the limit,
+ * <b>false</b> otherwise
+ */
+ private boolean checkDetailEvents(int limit)
+ {
+ synchronized (lockDetailEventsCount)
+ {
+ return detailEvents > limit;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/event/package.html b/src/main/java/org/apache/commons/configuration/event/package.html
new file mode 100644
index 0000000..bf29ee8
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/event/package.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+<head>
+</head>
+<body>
+
+<p>
+This package contains interfaces and classes for receiving notifications
+about changes at configurations.
+</p>
+<p>
+<font size="-2">$Id: package.html 439648 2006-09-02 20:42:10Z oheger $</font>
+</p>
+
+</body>
+</html>
diff --git a/src/main/java/org/apache/commons/configuration/interpol/ConfigurationInterpolator.java b/src/main/java/org/apache/commons/configuration/interpol/ConfigurationInterpolator.java
new file mode 100644
index 0000000..c22d866
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/interpol/ConfigurationInterpolator.java
@@ -0,0 +1,389 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.interpol;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang.text.StrLookup;
+
+/**
+ * <p>
+ * A class that handles interpolation (variable substitution) for configuration
+ * objects.
+ * </p>
+ * <p>
+ * Each instance of {@code AbstractConfiguration} is associated with an
+ * object of this class. All interpolation tasks are delegated to this object.
+ * </p>
+ * <p>
+ * {@code ConfigurationInterpolator} works together with the
+ * {@code StrSubstitutor} class from <a
+ * href="http://commons.apache.org/lang">Commons Lang</a>. By extending
+ * {@code StrLookup} it is able to provide values for variables that
+ * appear in expressions.
+ * </p>
+ * <p>
+ * The basic idea of this class is that it can maintain a set of primitive
+ * {@code StrLookup} objects, each of which is identified by a special
+ * prefix. The variables to be processed have the form
+ * <code>${prefix:name}</code>. {@code ConfigurationInterpolator} will
+ * extract the prefix and determine, which primitive lookup object is registered
+ * for it. Then the name of the variable is passed to this object to obtain the
+ * actual value. It is also possible to define a default lookup object, which
+ * will be used for variables that do not have a prefix or that cannot be
+ * resolved by their associated lookup object.
+ * </p>
+ * <p>
+ * When a new instance of this class is created it is initialized with a default
+ * set of primitive lookup objects. This set can be customized using the static
+ * methods {@code registerGlobalLookup()} and
+ * {@code deregisterGlobalLookup()}. Per default it contains the
+ * following standard lookup objects:
+ * </p>
+ * <p>
+ * <table border="1">
+ * <tr>
+ * <th>Prefix</th>
+ * <th>Lookup object</th>
+ * </tr>
+ * <tr>
+ * <td valign="top">sys</td>
+ * <td>With this prefix a lookup object is associated that is able to resolve
+ * system properties.</td>
+ * </tr>
+ * <tr>
+ * <td valign="top">const</td>
+ * <td>The {@code const} prefix indicates that a variable is to be
+ * interpreted as a constant member field of a class (i.e. a field with the
+ * <b>static final</b> modifiers). The name of the variable must be of the form
+ * {@code <full qualified class name>.<field name>}, e.g.
+ * {@code org.apache.commons.configuration.interpol.ConfigurationInterpolator.PREFIX_CONSTANTS}.
+ * </td>
+ * </tr>
+ * </table>
+ * </p>
+ * <p>
+ * After an instance has been created the current set of lookup objects can be
+ * modified using the {@code registerLookup()} and
+ * {@code deregisterLookup()} methods. The default lookup object (that is
+ * invoked for variables without a prefix) can be set with the
+ * {@code setDefaultLookup()} method. (If a
+ * {@code ConfigurationInterpolator} instance is created by a
+ * configuration object, this lookup points to the configuration itself, so that
+ * variables are resolved using the configuration's properties. This ensures
+ * backward compatibility to earlier version of Commons Configuration.)
+ * </p>
+ * <p>
+ * Implementation node: Instances of this class are not thread-safe related to
+ * modifications of their current set of registered lookup objects. It is
+ * intended that each instance is associated with a single
+ * {@code Configuration} object and used for its interpolation tasks.
+ * </p>
+ *
+ * @version $Id: ConfigurationInterpolator.java 1295276 2012-02-29 21:11:35Z oheger $
+ * @since 1.4
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ */
+public class ConfigurationInterpolator extends StrLookup
+{
+ /**
+ * Constant for the prefix of the standard lookup object for resolving
+ * system properties.
+ */
+ public static final String PREFIX_SYSPROPERTIES = "sys";
+
+ /**
+ * Constant for the prefix of the standard lookup object for resolving
+ * constant values.
+ */
+ public static final String PREFIX_CONSTANTS = "const";
+
+ /**
+ * Constant for the prefix of the standard lookup object for resolving
+ * environment properties.
+ * @since 1.7
+ */
+ public static final String PREFIX_ENVIRONMENT = "env";
+
+ /** Constant for the prefix separator. */
+ private static final char PREFIX_SEPARATOR = ':';
+
+ /** A map with the globally registered lookup objects. */
+ private static Map<String, StrLookup> globalLookups;
+
+ /** A map with the locally registered lookup objects. */
+ private Map<String, StrLookup> localLookups;
+
+ /** Stores the default lookup object. */
+ private StrLookup defaultLookup;
+
+ /** Stores a parent interpolator objects if the interpolator is nested hierarchically. */
+ private ConfigurationInterpolator parentInterpolator;
+
+ /**
+ * Creates a new instance of {@code ConfigurationInterpolator}.
+ */
+ public ConfigurationInterpolator()
+ {
+ synchronized (globalLookups)
+ {
+ localLookups = new HashMap<String, StrLookup>(globalLookups);
+ }
+ }
+
+ /**
+ * Registers the given lookup object for the specified prefix globally. This
+ * means that all instances that are created later will use this lookup
+ * object for this prefix. If for this prefix a lookup object is already
+ * registered, the new lookup object will replace the old one. Note that the
+ * lookup objects registered here will be shared between multiple clients.
+ * So they should be thread-safe.
+ *
+ * @param prefix the variable prefix (must not be <b>null</b>)
+ * @param lookup the lookup object to be used for this prefix (must not be
+ * <b>null</b>)
+ */
+ public static void registerGlobalLookup(String prefix, StrLookup lookup)
+ {
+ if (prefix == null)
+ {
+ throw new IllegalArgumentException(
+ "Prefix for lookup object must not be null!");
+ }
+ if (lookup == null)
+ {
+ throw new IllegalArgumentException(
+ "Lookup object must not be null!");
+ }
+ synchronized (globalLookups)
+ {
+ globalLookups.put(prefix, lookup);
+ }
+ }
+
+ /**
+ * Deregisters the global lookup object for the specified prefix. This means
+ * that this lookup object won't be available for later created instances
+ * any more. For already existing instances this operation does not have any
+ * impact.
+ *
+ * @param prefix the variable prefix
+ * @return a flag whether for this prefix a lookup object had been
+ * registered
+ */
+ public static boolean deregisterGlobalLookup(String prefix)
+ {
+ synchronized (globalLookups)
+ {
+ return globalLookups.remove(prefix) != null;
+ }
+ }
+
+ /**
+ * Registers the given lookup object for the specified prefix at this
+ * instance. From now on this lookup object will be used for variables that
+ * have the specified prefix.
+ *
+ * @param prefix the variable prefix (must not be <b>null</b>)
+ * @param lookup the lookup object to be used for this prefix (must not be
+ * <b>null</b>)
+ */
+ public void registerLookup(String prefix, StrLookup lookup)
+ {
+ if (prefix == null)
+ {
+ throw new IllegalArgumentException(
+ "Prefix for lookup object must not be null!");
+ }
+ if (lookup == null)
+ {
+ throw new IllegalArgumentException(
+ "Lookup object must not be null!");
+ }
+ localLookups.put(prefix, lookup);
+ }
+
+ /**
+ * Deregisters the lookup object for the specified prefix at this instance.
+ * It will be removed from this instance.
+ *
+ * @param prefix the variable prefix
+ * @return a flag whether for this prefix a lookup object had been
+ * registered
+ */
+ public boolean deregisterLookup(String prefix)
+ {
+ return localLookups.remove(prefix) != null;
+ }
+
+ /**
+ * Returns a set with the prefixes, for which lookup objects are registered
+ * at this instance. This means that variables with these prefixes can be
+ * processed.
+ *
+ * @return a set with the registered variable prefixes
+ */
+ public Set<String> prefixSet()
+ {
+ return localLookups.keySet();
+ }
+
+ /**
+ * Returns the default lookup object.
+ *
+ * @return the default lookup object
+ */
+ public StrLookup getDefaultLookup()
+ {
+ return defaultLookup;
+ }
+
+ /**
+ * Sets the default lookup object. This lookup object will be used for all
+ * variables without a special prefix. If it is set to <b>null</b>, such
+ * variables won't be processed.
+ *
+ * @param defaultLookup the new default lookup object
+ */
+ public void setDefaultLookup(StrLookup defaultLookup)
+ {
+ this.defaultLookup = defaultLookup;
+ }
+
+ /**
+ * Resolves the specified variable. This implementation will try to extract
+ * a variable prefix from the given variable name (the first colon (':') is
+ * used as prefix separator). It then passes the name of the variable with
+ * the prefix stripped to the lookup object registered for this prefix. If
+ * no prefix can be found or if the associated lookup object cannot resolve
+ * this variable, the default lookup object will be used.
+ *
+ * @param var the name of the variable whose value is to be looked up
+ * @return the value of this variable or <b>null</b> if it cannot be
+ * resolved
+ */
+ @Override
+ public String lookup(String var)
+ {
+ if (var == null)
+ {
+ return null;
+ }
+
+ int prefixPos = var.indexOf(PREFIX_SEPARATOR);
+ if (prefixPos >= 0)
+ {
+ String prefix = var.substring(0, prefixPos);
+ String name = var.substring(prefixPos + 1);
+ String value = fetchLookupForPrefix(prefix).lookup(name);
+ if (value == null && getParentInterpolator() != null)
+ {
+ value = getParentInterpolator().lookup(name);
+ }
+ if (value != null)
+ {
+ return value;
+ }
+ }
+ String value = fetchNoPrefixLookup().lookup(var);
+ if (value == null && getParentInterpolator() != null)
+ {
+ value = getParentInterpolator().lookup(var);
+ }
+ return value;
+ }
+
+ /**
+ * Returns the lookup object to be used for variables without a prefix. This
+ * implementation will check whether a default lookup object was set. If
+ * this is the case, it will be returned. Otherwise a <b>null</b> lookup
+ * object will be returned (never <b>null</b>).
+ *
+ * @return the lookup object to be used for variables without a prefix
+ */
+ protected StrLookup fetchNoPrefixLookup()
+ {
+ return (getDefaultLookup() != null) ? getDefaultLookup() : StrLookup.noneLookup();
+ }
+
+ /**
+ * Obtains the lookup object for the specified prefix. This method is called
+ * by the {@code lookup()} method. This implementation will check
+ * whether a lookup object is registered for the given prefix. If not, a
+ * <b>null</b> lookup object will be returned (never <b>null</b>).
+ *
+ * @param prefix the prefix
+ * @return the lookup object to be used for this prefix
+ */
+ protected StrLookup fetchLookupForPrefix(String prefix)
+ {
+ StrLookup lookup = localLookups.get(prefix);
+ if (lookup == null)
+ {
+ lookup = StrLookup.noneLookup();
+ }
+ return lookup;
+ }
+
+ /**
+ * Registers the local lookup instances for the given interpolator.
+ *
+ * @param interpolator the instance receiving the local lookups
+ * @since upcoming
+ */
+ public void registerLocalLookups(ConfigurationInterpolator interpolator)
+ {
+ interpolator.localLookups.putAll(localLookups);
+ }
+
+ /**
+ * Sets the parent interpolator. This object is used if the interpolation is nested
+ * hierarchically and the current interpolation object cannot resolve a variable.
+ *
+ * @param parentInterpolator the parent interpolator object or <b>null</b>
+ * @since upcoming
+ */
+ public void setParentInterpolator(ConfigurationInterpolator parentInterpolator)
+ {
+ this.parentInterpolator = parentInterpolator;
+ }
+
+ /**
+ * Requests the parent interpolator. This object is used if the interpolation is nested
+ * hierarchically and the current interpolation
+ *
+ * @return the parent interpolator or <b>null</b>
+ * @since upcoming
+ */
+ public ConfigurationInterpolator getParentInterpolator()
+ {
+ return this.parentInterpolator;
+ }
+
+ // static initializer, sets up the map with the standard lookups
+ static
+ {
+ globalLookups = new HashMap<String, StrLookup>();
+ globalLookups.put(PREFIX_SYSPROPERTIES, StrLookup.systemPropertiesLookup());
+ globalLookups.put(PREFIX_CONSTANTS, new ConstantLookup());
+ globalLookups.put(PREFIX_ENVIRONMENT, new EnvironmentLookup());
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/interpol/ConstantLookup.java b/src/main/java/org/apache/commons/configuration/interpol/ConstantLookup.java
new file mode 100644
index 0000000..9d9c189
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/interpol/ConstantLookup.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.interpol;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.lang.ClassUtils;
+import org.apache.commons.lang.text.StrLookup;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <p>
+ * A specialized lookup implementation that allows access to constant fields of
+ * classes.
+ * </p>
+ * <p>
+ * Sometimes it is necessary in a configuration file to refer to a constant
+ * defined in a class. This can be done with this lookup implementation.
+ * Variable names passed in must be of the form
+ * {@code mypackage.MyClass.FIELD}. The {@code lookup()} method
+ * will split the passed in string at the last dot, separating the fully
+ * qualified class name and the name of the constant (i.e. <strong>static final</strong>)
+ * member field. Then the class is loaded and the field's value is obtained
+ * using reflection.
+ * </p>
+ * <p>
+ * Once retrieved values are cached for fast access. This class is thread-safe.
+ * It can be used as a standard (i.e. global) lookup object and serve multiple
+ * clients concurrently.
+ * </p>
+ *
+ * @version $Id: ConstantLookup.java 1210218 2011-12-04 20:57:48Z oheger $
+ * @since 1.4
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ */
+public class ConstantLookup extends StrLookup
+{
+ /** Constant for the field separator. */
+ private static final char FIELD_SEPRATOR = '.';
+
+ /** An internally used cache for already retrieved values. */
+ private static Map<String, String> constantCache = new HashMap<String, String>();
+
+ /** The logger. */
+ private Log log = LogFactory.getLog(getClass());
+
+ /**
+ * Tries to resolve the specified variable. The passed in variable name is
+ * interpreted as the name of a <b>static final</b> member field of a
+ * class. If the value has already been obtained, it can be retrieved from
+ * an internal cache. Otherwise this method will invoke the
+ * {@code resolveField()} method and pass in the name of the class
+ * and the field.
+ *
+ * @param var the name of the variable to be resolved
+ * @return the value of this variable or <b>null</b> if it cannot be
+ * resolved
+ */
+ @Override
+ public String lookup(String var)
+ {
+ if (var == null)
+ {
+ return null;
+ }
+
+ String result;
+ synchronized (constantCache)
+ {
+ result = constantCache.get(var);
+ }
+ if (result != null)
+ {
+ return result;
+ }
+
+ int fieldPos = var.lastIndexOf(FIELD_SEPRATOR);
+ if (fieldPos < 0)
+ {
+ return null;
+ }
+ try
+ {
+ Object value = resolveField(var.substring(0, fieldPos), var
+ .substring(fieldPos + 1));
+ if (value != null)
+ {
+ synchronized (constantCache)
+ {
+ // In worst case, the value will be fetched multiple times
+ // because of this lax synchronisation, but for constant
+ // values this shouldn't be a problem.
+ constantCache.put(var, String.valueOf(value));
+ }
+ result = value.toString();
+ }
+ }
+ catch (Exception ex)
+ {
+ log.warn("Could not obtain value for variable " + var, ex);
+ }
+
+ return result;
+ }
+
+ /**
+ * Clears the shared cache with the so far resolved constants.
+ */
+ public static void clear()
+ {
+ synchronized (constantCache)
+ {
+ constantCache.clear();
+ }
+ }
+
+ /**
+ * Determines the value of the specified constant member field of a class.
+ * This implementation will call {@code fetchClass()} to obtain the
+ * {@code java.lang.Class} object for the target class. Then it will
+ * use reflection to obtain the field's value. For this to work the field
+ * must be accessable.
+ *
+ * @param className the name of the class
+ * @param fieldName the name of the member field of that class to read
+ * @return the field's value
+ * @throws Exception if an error occurs
+ */
+ protected Object resolveField(String className, String fieldName)
+ throws Exception
+ {
+ Class<?> clazz = fetchClass(className);
+ Field field = clazz.getField(fieldName);
+ return field.get(null);
+ }
+
+ /**
+ * Loads the class with the specified name. If an application has special
+ * needs regarding the class loaders to be used, it can hook in here. This
+ * implementation delegates to the {@code getClass()} method of
+ * Commons Lang's
+ * <code><a href="http://commons.apache.org/lang/api-release/org/apache/commons/lang/ClassUtils.html">
+ * ClassUtils</a></code>.
+ *
+ * @param className the name of the class to be loaded
+ * @return the corresponding class object
+ * @throws ClassNotFoundException if the class cannot be loaded
+ */
+ protected Class<?> fetchClass(String className) throws ClassNotFoundException
+ {
+ return ClassUtils.getClass(className);
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/interpol/EnvironmentLookup.java b/src/main/java/org/apache/commons/configuration/interpol/EnvironmentLookup.java
new file mode 100644
index 0000000..85fcedc
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/interpol/EnvironmentLookup.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.interpol;
+
+import org.apache.commons.configuration.EnvironmentConfiguration;
+import org.apache.commons.lang.text.StrLookup;
+
+/**
+ * <p>
+ * A specialized lookup implementation that allows access to environment
+ * variables.
+ * </p>
+ * <p>
+ * This implementation relies on {@link EnvironmentConfiguration} to resolve
+ * environment variables. It can be used for referencing environment variables
+ * in configuration files in an easy way, for instance:
+ *
+ * <pre>
+ * java.home = ${env:JAVA_HOME}
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * {@code EnvironmentLookup} is one of the standard lookups that is
+ * registered per default for each configuration.
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @since 1.7
+ * @version $Id: EnvironmentLookup.java 1210620 2011-12-05 20:57:31Z oheger $
+ */
+public class EnvironmentLookup extends StrLookup
+{
+ /** Stores the underlying {@code EnvironmentConfiguration}. */
+ private final EnvironmentConfiguration environmentConfig = new EnvironmentConfiguration();
+
+ /**
+ * Performs a lookup for the specified variable. This implementation
+ * directly delegates to a {@code EnvironmentConfiguration}.
+ *
+ * @param key the key to lookup
+ * @return the value of this key or <b>null</b> if it cannot be resolved
+ */
+ @Override
+ public String lookup(String key)
+ {
+ return environmentConfig.getString(key);
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/interpol/ExprLookup.java b/src/main/java/org/apache/commons/configuration/interpol/ExprLookup.java
new file mode 100644
index 0000000..88e4dbd
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/interpol/ExprLookup.java
@@ -0,0 +1,334 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.interpol;
+
+import java.util.ArrayList;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.ConfigurationRuntimeException;
+import org.apache.commons.jexl2.Expression;
+import org.apache.commons.jexl2.JexlContext;
+import org.apache.commons.jexl2.JexlEngine;
+import org.apache.commons.jexl2.MapContext;
+import org.apache.commons.lang.ClassUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.text.StrLookup;
+import org.apache.commons.lang.text.StrSubstitutor;
+
+/**
+ * Lookup that allows expressions to be evaluated.
+ *
+ * <pre>
+ * ExprLookup.Variables vars = new ExprLookup.Variables();
+ * vars.add(new ExprLookup.Variable("String", org.apache.commons.lang.StringUtils.class));
+ * vars.add(new ExprLookup.Variable("Util", new Utility("Hello")));
+ * vars.add(new ExprLookup.Variable("System", "Class:java.lang.System"));
+ * XMLConfiguration config = new XMLConfiguration(TEST_FILE);
+ * config.setLogger(log);
+ * ExprLookup lookup = new ExprLookup(vars);
+ * lookup.setConfiguration(config);
+ * String str = lookup.lookup("'$[element] ' + String.trimToEmpty('$[space.description]')");
+ * </pre>
+ *
+ * In the example above TEST_FILE contains xml that looks like:
+ * <pre>
+ * <configuration>
+ * <element>value</element>
+ * <space xml:space="preserve">
+ * <description xml:space="default"> Some text </description>
+ * </space>
+ * </configuration>
+ * </pre>
+ *
+ * The result will be "value Some text".
+ *
+ * This lookup uses Apache Commons Jexl and requires that the dependency be added to any
+ * projects which use this.
+ *
+ * @since 1.7
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons Configuration team</a>
+ * @version $Id: ExprLookup.java 1234539 2012-01-22 16:19:15Z oheger $
+ */
+public class ExprLookup extends StrLookup
+{
+ /** Prefix to identify a Java Class object */
+ private static final String CLASS = "Class:";
+
+ /** The default prefix for subordinate lookup expressions */
+ private static final String DEFAULT_PREFIX = "$[";
+
+ /** The default suffix for subordinate lookup expressions */
+ private static final String DEFAULT_SUFFIX = "]";
+
+ /** Configuration being operated on */
+ private AbstractConfiguration configuration;
+
+ /** The engine. */
+ private final JexlEngine engine = new JexlEngine();
+
+ /** The variables maintained by this object. */
+ private Variables variables;
+
+ /** The String to use to start subordinate lookup expressions */
+ private String prefixMatcher = DEFAULT_PREFIX;
+
+ /** The String to use to terminate subordinate lookup expressions */
+ private String suffixMatcher = DEFAULT_SUFFIX;
+
+ /**
+ * The default constructor. Will get used when the Lookup is constructed via
+ * configuration.
+ */
+ public ExprLookup()
+ {
+ }
+
+ /**
+ * Constructor for use by applications.
+ * @param list The list of objects to be accessible in expressions.
+ */
+ public ExprLookup(Variables list)
+ {
+ setVariables(list);
+ }
+
+ /**
+ * Constructor for use by applications.
+ * @param list The list of objects to be accessible in expressions.
+ * @param prefix The prefix to use for subordinate lookups.
+ * @param suffix The suffix to use for subordinate lookups.
+ */
+ public ExprLookup(Variables list, String prefix, String suffix)
+ {
+ this(list);
+ setVariablePrefixMatcher(prefix);
+ setVariableSuffixMatcher(suffix);
+ }
+
+ /**
+ * Set the prefix to use to identify subordinate expressions. This cannot be the
+ * same as the prefix used for the primary expression.
+ * @param prefix The String identifying the beginning of the expression.
+ */
+ public void setVariablePrefixMatcher(String prefix)
+ {
+ prefixMatcher = prefix;
+ }
+
+
+ /**
+ * Set the suffix to use to identify subordinate expressions. This cannot be the
+ * same as the suffix used for the primary expression.
+ * @param suffix The String identifying the end of the expression.
+ */
+ public void setVariableSuffixMatcher(String suffix)
+ {
+ suffixMatcher = suffix;
+ }
+
+ /**
+ * Add the Variables that will be accessible within expressions.
+ * @param list The list of Variables.
+ */
+ public void setVariables(Variables list)
+ {
+ variables = new Variables(list);
+ }
+
+ /**
+ * Returns the list of Variables that are accessible within expressions.
+ * @return the List of Variables that are accessible within expressions.
+ */
+ public Variables getVariables()
+ {
+ return null;
+ }
+
+ /**
+ * Set the configuration to be used to interpolate subordinate expressions.
+ * @param config The Configuration.
+ */
+ public void setConfiguration(AbstractConfiguration config)
+ {
+ this.configuration = config;
+ }
+
+ /**
+ * Evaluates the expression.
+ * @param var The expression.
+ * @return The String result of the expression.
+ */
+ @Override
+ public String lookup(String var)
+ {
+ ConfigurationInterpolator interp = configuration.getInterpolator();
+ StrSubstitutor subst = new StrSubstitutor(interp, prefixMatcher, suffixMatcher,
+ StrSubstitutor.DEFAULT_ESCAPE);
+
+ String result = subst.replace(var);
+
+ try
+ {
+ Expression exp = engine.createExpression(result);
+ result = (String) exp.evaluate(createContext());
+ }
+ catch (Exception e)
+ {
+ configuration.getLogger().debug("Error encountered evaluating " + result, e);
+ }
+
+ return result;
+ }
+
+ /**
+ * Creates a new {@code JexlContext} and initializes it with the variables
+ * managed by this Lookup object.
+ *
+ * @return the newly created context
+ */
+ private JexlContext createContext()
+ {
+ JexlContext ctx = new MapContext();
+ initializeContext(ctx);
+ return ctx;
+ }
+
+ /**
+ * Initializes the specified context with the variables managed by this
+ * Lookup object.
+ *
+ * @param ctx the context to be initialized
+ */
+ private void initializeContext(JexlContext ctx)
+ {
+ for (Variable var : variables)
+ {
+ ctx.set(var.getName(), var.getValue());
+ }
+ }
+
+ /**
+ * List wrapper used to allow the Variables list to be created as beans in
+ * DefaultConfigurationBuilder.
+ */
+ public static class Variables extends ArrayList<Variable>
+ {
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = 20111205L;
+
+ /**
+ * Creates a new empty instance of {@code Variables}.
+ */
+ public Variables()
+ {
+ super();
+ }
+
+ /**
+ * Creates a new instance of {@code Variables} and copies the content of
+ * the given object.
+ *
+ * @param vars the {@code Variables} object to be copied
+ */
+ public Variables(Variables vars)
+ {
+ super(vars);
+ }
+
+ public Variable getVariable()
+ {
+ if (size() > 0)
+ {
+ return get(size() - 1);
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ }
+
+ /**
+ * The key and corresponding object that will be made available to the
+ * JexlContext for use in expressions.
+ */
+ public static class Variable
+ {
+ /** The name to be used in expressions */
+ private String key;
+
+ /** The object to be accessed in expressions */
+ private Object value;
+
+ public Variable()
+ {
+ }
+
+ public Variable(String name, Object value)
+ {
+ setName(name);
+ setValue(value);
+ }
+
+ public String getName()
+ {
+ return key;
+ }
+
+ public void setName(String name)
+ {
+ this.key = name;
+ }
+
+ public Object getValue()
+ {
+ return value;
+ }
+
+ public void setValue(Object value) throws ConfigurationRuntimeException
+ {
+ try
+ {
+ if (!(value instanceof String))
+ {
+ this.value = value;
+ return;
+ }
+ String val = (String) value;
+ String name = StringUtils.removeStartIgnoreCase(val, CLASS);
+ Class<?> clazz = ClassUtils.getClass(name);
+ if (name.length() == val.length())
+ {
+ this.value = clazz.newInstance();
+ }
+ else
+ {
+ this.value = clazz;
+ }
+ }
+ catch (Exception e)
+ {
+ throw new ConfigurationRuntimeException("Unable to create " + value, e);
+ }
+
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/interpol/package.html b/src/main/java/org/apache/commons/configuration/interpol/package.html
new file mode 100644
index 0000000..d960e99
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/interpol/package.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+<head>
+</head>
+<body>
+
+<p>
+A package with helper classes used for interpolation (variable substitution).
+</p>
+<p>
+<font size="-2">$Id: package.html 952622 2010-06-08 11:56:30Z sebb $</font>
+</p>
+
+</body>
+</html>
diff --git a/src/main/java/org/apache/commons/configuration/package.html b/src/main/java/org/apache/commons/configuration/package.html
new file mode 100644
index 0000000..c385132
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/package.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+<head>
+</head>
+
+<body>
+<p>
+The Configuration main package. It contains the definition of the
+<code>Configuration</code> interface and frequently used implementations
+like <code>PropertiesConfiguration</code> (dealing with <code>.properties</code>
+files) or <code>XMLConfiguration</code> that can load XML documents.
+</p>
+<p>
+<font size="-2">$Id: package.html 439648 2006-09-02 20:42:10Z oheger $</font>
+</p>
+</body>
+</html>
diff --git a/src/main/java/org/apache/commons/configuration/plist/PropertyListConfiguration.java b/src/main/java/org/apache/commons/configuration/plist/PropertyListConfiguration.java
new file mode 100644
index 0000000..114dc29
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/plist/PropertyListConfiguration.java
@@ -0,0 +1,713 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.plist;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.apache.commons.configuration.MapConfiguration;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * NeXT / OpenStep style configuration. This configuration can read and write
+ * ASCII plist files. It supports the GNUStep extension to specify date objects.
+ * <p>
+ * References:
+ * <ul>
+ * <li><a
+ * href="http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html">
+ * Apple Documentation - Old-Style ASCII Property Lists</a></li>
+ * <li><a
+ * href="http://www.gnustep.org/resources/documentation/Developer/Base/Reference/NSPropertyList.html">
+ * GNUStep Documentation</a></li>
+ * </ul>
+ *
+ * <p>Example:</p>
+ * <pre>
+ * {
+ * foo = "bar";
+ *
+ * array = ( value1, value2, value3 );
+ *
+ * data = <4f3e0145ab>;
+ *
+ * date = <*D2007-05-05 20:05:00 +0100>;
+ *
+ * nested =
+ * {
+ * key1 = value1;
+ * key2 = value;
+ * nested =
+ * {
+ * foo = bar
+ * }
+ * }
+ * }
+ * </pre>
+ *
+ * @since 1.2
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: PropertyListConfiguration.java 1210637 2011-12-05 21:12:12Z oheger $
+ */
+public class PropertyListConfiguration extends AbstractHierarchicalFileConfiguration
+{
+ /** Constant for the separator parser for the date part. */
+ private static final DateComponentParser DATE_SEPARATOR_PARSER = new DateSeparatorParser(
+ "-");
+
+ /** Constant for the separator parser for the time part. */
+ private static final DateComponentParser TIME_SEPARATOR_PARSER = new DateSeparatorParser(
+ ":");
+
+ /** Constant for the separator parser for blanks between the parts. */
+ private static final DateComponentParser BLANK_SEPARATOR_PARSER = new DateSeparatorParser(
+ " ");
+
+ /** An array with the component parsers for dealing with dates. */
+ private static final DateComponentParser[] DATE_PARSERS =
+ {new DateSeparatorParser("<*D"), new DateFieldParser(Calendar.YEAR, 4),
+ DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.MONTH, 2, 1),
+ DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.DATE, 2),
+ BLANK_SEPARATOR_PARSER,
+ new DateFieldParser(Calendar.HOUR_OF_DAY, 2),
+ TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.MINUTE, 2),
+ TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.SECOND, 2),
+ BLANK_SEPARATOR_PARSER, new DateTimeZoneParser(),
+ new DateSeparatorParser(">")};
+
+ /** Constant for the ID prefix for GMT time zones. */
+ private static final String TIME_ZONE_PREFIX = "GMT";
+
+ /** The serial version UID. */
+ private static final long serialVersionUID = 3227248503779092127L;
+
+ /** Constant for the milliseconds of a minute.*/
+ private static final int MILLIS_PER_MINUTE = 1000 * 60;
+
+ /** Constant for the minutes per hour.*/
+ private static final int MINUTES_PER_HOUR = 60;
+
+ /** Size of the indentation for the generated file. */
+ private static final int INDENT_SIZE = 4;
+
+ /** Constant for the length of a time zone.*/
+ private static final int TIME_ZONE_LENGTH = 5;
+
+ /** Constant for the padding character in the date format.*/
+ private static final char PAD_CHAR = '0';
+
+ /**
+ * Creates an empty PropertyListConfiguration object which can be
+ * used to synthesize a new plist file by adding values and
+ * then saving().
+ */
+ public PropertyListConfiguration()
+ {
+ }
+
+ /**
+ * Creates a new instance of {@code PropertyListConfiguration} and
+ * copies the content of the specified configuration into this object.
+ *
+ * @param c the configuration to copy
+ * @since 1.4
+ */
+ public PropertyListConfiguration(HierarchicalConfiguration c)
+ {
+ super(c);
+ }
+
+ /**
+ * Creates and loads the property list from the specified file.
+ *
+ * @param fileName The name of the plist file to load.
+ * @throws ConfigurationException Error while loading the plist file
+ */
+ public PropertyListConfiguration(String fileName) throws ConfigurationException
+ {
+ super(fileName);
+ }
+
+ /**
+ * Creates and loads the property list from the specified file.
+ *
+ * @param file The plist file to load.
+ * @throws ConfigurationException Error while loading the plist file
+ */
+ public PropertyListConfiguration(File file) throws ConfigurationException
+ {
+ super(file);
+ }
+
+ /**
+ * Creates and loads the property list from the specified URL.
+ *
+ * @param url The location of the plist file to load.
+ * @throws ConfigurationException Error while loading the plist file
+ */
+ public PropertyListConfiguration(URL url) throws ConfigurationException
+ {
+ super(url);
+ }
+
+ @Override
+ public void setProperty(String key, Object value)
+ {
+ // special case for byte arrays, they must be stored as is in the configuration
+ if (value instanceof byte[])
+ {
+ fireEvent(EVENT_SET_PROPERTY, key, value, true);
+ setDetailEvents(false);
+ try
+ {
+ clearProperty(key);
+ addPropertyDirect(key, value);
+ }
+ finally
+ {
+ setDetailEvents(true);
+ }
+ fireEvent(EVENT_SET_PROPERTY, key, value, false);
+ }
+ else
+ {
+ super.setProperty(key, value);
+ }
+ }
+
+ @Override
+ public void addProperty(String key, Object value)
+ {
+ if (value instanceof byte[])
+ {
+ fireEvent(EVENT_ADD_PROPERTY, key, value, true);
+ addPropertyDirect(key, value);
+ fireEvent(EVENT_ADD_PROPERTY, key, value, false);
+ }
+ else
+ {
+ super.addProperty(key, value);
+ }
+ }
+
+ public void load(Reader in) throws ConfigurationException
+ {
+ PropertyListParser parser = new PropertyListParser(in);
+ try
+ {
+ HierarchicalConfiguration config = parser.parse();
+ setRoot(config.getRoot());
+ }
+ catch (ParseException e)
+ {
+ throw new ConfigurationException(e);
+ }
+ }
+
+ public void save(Writer out) throws ConfigurationException
+ {
+ PrintWriter writer = new PrintWriter(out);
+ printNode(writer, 0, getRoot());
+ writer.flush();
+ }
+
+ /**
+ * Append a node to the writer, indented according to a specific level.
+ */
+ private void printNode(PrintWriter out, int indentLevel, ConfigurationNode node)
+ {
+ String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
+
+ if (node.getName() != null)
+ {
+ out.print(padding + quoteString(node.getName()) + " = ");
+ }
+
+ List<ConfigurationNode> children = new ArrayList<ConfigurationNode>(node.getChildren());
+ if (!children.isEmpty())
+ {
+ // skip a line, except for the root dictionary
+ if (indentLevel > 0)
+ {
+ out.println();
+ }
+
+ out.println(padding + "{");
+
+ // display the children
+ Iterator<ConfigurationNode> it = children.iterator();
+ while (it.hasNext())
+ {
+ ConfigurationNode child = it.next();
+
+ printNode(out, indentLevel + 1, child);
+
+ // add a semi colon for elements that are not dictionaries
+ Object value = child.getValue();
+ if (value != null && !(value instanceof Map) && !(value instanceof Configuration))
+ {
+ out.println(";");
+ }
+
+ // skip a line after arrays and dictionaries
+ if (it.hasNext() && (value == null || value instanceof List))
+ {
+ out.println();
+ }
+ }
+
+ out.print(padding + "}");
+
+ // line feed if the dictionary is not in an array
+ if (node.getParentNode() != null)
+ {
+ out.println();
+ }
+ }
+ else if (node.getValue() == null)
+ {
+ out.println();
+ out.print(padding + "{ };");
+
+ // line feed if the dictionary is not in an array
+ if (node.getParentNode() != null)
+ {
+ out.println();
+ }
+ }
+ else
+ {
+ // display the leaf value
+ Object value = node.getValue();
+ printValue(out, indentLevel, value);
+ }
+ }
+
+ /**
+ * Append a value to the writer, indented according to a specific level.
+ */
+ private void printValue(PrintWriter out, int indentLevel, Object value)
+ {
+ String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
+
+ if (value instanceof List)
+ {
+ out.print("( ");
+ Iterator<?> it = ((List<?>) value).iterator();
+ while (it.hasNext())
+ {
+ printValue(out, indentLevel + 1, it.next());
+ if (it.hasNext())
+ {
+ out.print(", ");
+ }
+ }
+ out.print(" )");
+ }
+ else if (value instanceof HierarchicalConfiguration)
+ {
+ printNode(out, indentLevel, ((HierarchicalConfiguration) value).getRoot());
+ }
+ else if (value instanceof Configuration)
+ {
+ // display a flat Configuration as a dictionary
+ out.println();
+ out.println(padding + "{");
+
+ Configuration config = (Configuration) value;
+ Iterator<String> it = config.getKeys();
+ while (it.hasNext())
+ {
+ String key = it.next();
+ Node node = new Node(key);
+ node.setValue(config.getProperty(key));
+
+ printNode(out, indentLevel + 1, node);
+ out.println(";");
+ }
+ out.println(padding + "}");
+ }
+ else if (value instanceof Map)
+ {
+ // display a Map as a dictionary
+ Map<String, Object> map = transformMap((Map<?, ?>) value);
+ printValue(out, indentLevel, new MapConfiguration(map));
+ }
+ else if (value instanceof byte[])
+ {
+ out.print("<" + new String(Hex.encodeHex((byte[]) value)) + ">");
+ }
+ else if (value instanceof Date)
+ {
+ out.print(formatDate((Date) value));
+ }
+ else if (value != null)
+ {
+ out.print(quoteString(String.valueOf(value)));
+ }
+ }
+
+ /**
+ * Quote the specified string if necessary, that's if the string contains:
+ * <ul>
+ * <li>a space character (' ', '\t', '\r', '\n')</li>
+ * <li>a quote '"'</li>
+ * <li>special characters in plist files ('(', ')', '{', '}', '=', ';', ',')</li>
+ * </ul>
+ * Quotes within the string are escaped.
+ *
+ * <p>Examples:</p>
+ * <ul>
+ * <li>abcd -> abcd</li>
+ * <li>ab cd -> "ab cd"</li>
+ * <li>foo"bar -> "foo\"bar"</li>
+ * <li>foo;bar -> "foo;bar"</li>
+ * </ul>
+ */
+ String quoteString(String s)
+ {
+ if (s == null)
+ {
+ return null;
+ }
+
+ if (s.indexOf(' ') != -1
+ || s.indexOf('\t') != -1
+ || s.indexOf('\r') != -1
+ || s.indexOf('\n') != -1
+ || s.indexOf('"') != -1
+ || s.indexOf('(') != -1
+ || s.indexOf(')') != -1
+ || s.indexOf('{') != -1
+ || s.indexOf('}') != -1
+ || s.indexOf('=') != -1
+ || s.indexOf(',') != -1
+ || s.indexOf(';') != -1)
+ {
+ s = s.replaceAll("\"", "\\\\\\\"");
+ s = "\"" + s + "\"";
+ }
+
+ return s;
+ }
+
+ /**
+ * Parses a date in a format like
+ * {@code <*D2002-03-22 11:30:00 +0100>}.
+ *
+ * @param s the string with the date to be parsed
+ * @return the parsed date
+ * @throws ParseException if an error occurred while parsing the string
+ */
+ static Date parseDate(String s) throws ParseException
+ {
+ Calendar cal = Calendar.getInstance();
+ cal.clear();
+ int index = 0;
+
+ for (DateComponentParser parser : DATE_PARSERS)
+ {
+ index += parser.parseComponent(s, index, cal);
+ }
+
+ return cal.getTime();
+ }
+
+ /**
+ * Returns a string representation for the date specified by the given
+ * calendar.
+ *
+ * @param cal the calendar with the initialized date
+ * @return a string for this date
+ */
+ static String formatDate(Calendar cal)
+ {
+ StringBuilder buf = new StringBuilder();
+
+ for (int i = 0; i < DATE_PARSERS.length; i++)
+ {
+ DATE_PARSERS[i].formatComponent(buf, cal);
+ }
+
+ return buf.toString();
+ }
+
+ /**
+ * Returns a string representation for the specified date.
+ *
+ * @param date the date
+ * @return a string for this date
+ */
+ static String formatDate(Date date)
+ {
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(date);
+ return formatDate(cal);
+ }
+
+ /**
+ * Transform a map of arbitrary types into a map with string keys and object
+ * values. All keys of the source map which are not of type String are
+ * dropped.
+ *
+ * @param src the map to be converted
+ * @return the resulting map
+ */
+ private static Map<String, Object> transformMap(Map<?, ?> src)
+ {
+ Map<String, Object> dest = new HashMap<String, Object>();
+ for (Map.Entry<?, ?> e : src.entrySet())
+ {
+ if (e.getKey() instanceof String)
+ {
+ dest.put((String) e.getKey(), e.getValue());
+ }
+ }
+ return dest;
+ }
+
+ /**
+ * A helper class for parsing and formatting date literals. Usually we would
+ * use {@code SimpleDateFormat} for this purpose, but in Java 1.3 the
+ * functionality of this class is limited. So we have a hierarchy of parser
+ * classes instead that deal with the different components of a date
+ * literal.
+ */
+ private abstract static class DateComponentParser
+ {
+ /**
+ * Parses a component from the given input string.
+ *
+ * @param s the string to be parsed
+ * @param index the current parsing position
+ * @param cal the calendar where to store the result
+ * @return the length of the processed component
+ * @throws ParseException if the component cannot be extracted
+ */
+ public abstract int parseComponent(String s, int index, Calendar cal)
+ throws ParseException;
+
+ /**
+ * Formats a date component. This method is used for converting a date
+ * in its internal representation into a string literal.
+ *
+ * @param buf the target buffer
+ * @param cal the calendar with the current date
+ */
+ public abstract void formatComponent(StringBuilder buf, Calendar cal);
+
+ /**
+ * Checks whether the given string has at least {@code length}
+ * characters starting from the given parsing position. If this is not
+ * the case, an exception will be thrown.
+ *
+ * @param s the string to be tested
+ * @param index the current index
+ * @param length the minimum length after the index
+ * @throws ParseException if the string is too short
+ */
+ protected void checkLength(String s, int index, int length)
+ throws ParseException
+ {
+ int len = (s == null) ? 0 : s.length();
+ if (index + length > len)
+ {
+ throw new ParseException("Input string too short: " + s
+ + ", index: " + index);
+ }
+ }
+
+ /**
+ * Adds a number to the given string buffer and adds leading '0'
+ * characters until the given length is reached.
+ *
+ * @param buf the target buffer
+ * @param num the number to add
+ * @param length the required length
+ */
+ protected void padNum(StringBuilder buf, int num, int length)
+ {
+ buf.append(StringUtils.leftPad(String.valueOf(num), length,
+ PAD_CHAR));
+ }
+ }
+
+ /**
+ * A specialized date component parser implementation that deals with
+ * numeric calendar fields. The class is able to extract fields from a
+ * string literal and to format a literal from a calendar.
+ */
+ private static class DateFieldParser extends DateComponentParser
+ {
+ /** Stores the calendar field to be processed. */
+ private int calendarField;
+
+ /** Stores the length of this field. */
+ private int length;
+
+ /** An optional offset to add to the calendar field. */
+ private int offset;
+
+ /**
+ * Creates a new instance of {@code DateFieldParser}.
+ *
+ * @param calFld the calendar field code
+ * @param len the length of this field
+ */
+ public DateFieldParser(int calFld, int len)
+ {
+ this(calFld, len, 0);
+ }
+
+ /**
+ * Creates a new instance of {@code DateFieldParser} and fully
+ * initializes it.
+ *
+ * @param calFld the calendar field code
+ * @param len the length of this field
+ * @param ofs an offset to add to the calendar field
+ */
+ public DateFieldParser(int calFld, int len, int ofs)
+ {
+ calendarField = calFld;
+ length = len;
+ offset = ofs;
+ }
+
+ @Override
+ public void formatComponent(StringBuilder buf, Calendar cal)
+ {
+ padNum(buf, cal.get(calendarField) + offset, length);
+ }
+
+ @Override
+ public int parseComponent(String s, int index, Calendar cal)
+ throws ParseException
+ {
+ checkLength(s, index, length);
+ try
+ {
+ cal.set(calendarField, Integer.parseInt(s.substring(index,
+ index + length))
+ - offset);
+ return length;
+ }
+ catch (NumberFormatException nfex)
+ {
+ throw new ParseException("Invalid number: " + s + ", index "
+ + index);
+ }
+ }
+ }
+
+ /**
+ * A specialized date component parser implementation that deals with
+ * separator characters.
+ */
+ private static class DateSeparatorParser extends DateComponentParser
+ {
+ /** Stores the separator. */
+ private String separator;
+
+ /**
+ * Creates a new instance of {@code DateSeparatorParser} and sets
+ * the separator string.
+ *
+ * @param sep the separator string
+ */
+ public DateSeparatorParser(String sep)
+ {
+ separator = sep;
+ }
+
+ @Override
+ public void formatComponent(StringBuilder buf, Calendar cal)
+ {
+ buf.append(separator);
+ }
+
+ @Override
+ public int parseComponent(String s, int index, Calendar cal)
+ throws ParseException
+ {
+ checkLength(s, index, separator.length());
+ if (!s.startsWith(separator, index))
+ {
+ throw new ParseException("Invalid input: " + s + ", index "
+ + index + ", expected " + separator);
+ }
+ return separator.length();
+ }
+ }
+
+ /**
+ * A specialized date component parser implementation that deals with the
+ * time zone part of a date component.
+ */
+ private static class DateTimeZoneParser extends DateComponentParser
+ {
+ @Override
+ public void formatComponent(StringBuilder buf, Calendar cal)
+ {
+ TimeZone tz = cal.getTimeZone();
+ int ofs = tz.getRawOffset() / MILLIS_PER_MINUTE;
+ if (ofs < 0)
+ {
+ buf.append('-');
+ ofs = -ofs;
+ }
+ else
+ {
+ buf.append('+');
+ }
+ int hour = ofs / MINUTES_PER_HOUR;
+ int min = ofs % MINUTES_PER_HOUR;
+ padNum(buf, hour, 2);
+ padNum(buf, min, 2);
+ }
+
+ @Override
+ public int parseComponent(String s, int index, Calendar cal)
+ throws ParseException
+ {
+ checkLength(s, index, TIME_ZONE_LENGTH);
+ TimeZone tz = TimeZone.getTimeZone(TIME_ZONE_PREFIX
+ + s.substring(index, index + TIME_ZONE_LENGTH));
+ cal.setTimeZone(tz);
+ return TIME_ZONE_LENGTH;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/plist/XMLPropertyListConfiguration.java b/src/main/java/org/apache/commons/configuration/plist/XMLPropertyListConfiguration.java
new file mode 100644
index 0000000..b3c397d
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/plist/XMLPropertyListConfiguration.java
@@ -0,0 +1,804 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.plist;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.URL;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.apache.commons.configuration.MapConfiguration;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.lang.StringUtils;
+import org.xml.sax.Attributes;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Property list file (plist) in XML FORMAT as used by Mac OS X (http://www.apple.com/DTDs/PropertyList-1.0.dtd).
+ * This configuration doesn't support the binary FORMAT used in OS X 10.4.
+ *
+ * <p>Example:</p>
+ * <pre>
+ * <?xml version="1.0"?>
+ * <!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
+ * <plist version="1.0">
+ * <dict>
+ * <key>string</key>
+ * <string>value1</string>
+ *
+ * <key>integer</key>
+ * <integer>12345</integer>
+ *
+ * <key>real</key>
+ * <real>-123.45E-1</real>
+ *
+ * <key>boolean</key>
+ * <true/>
+ *
+ * <key>date</key>
+ * <date>2005-01-01T12:00:00Z</date>
+ *
+ * <key>data</key>
+ * <data>RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==</data>
+ *
+ * <key>array</key>
+ * <array>
+ * <string>value1</string>
+ * <string>value2</string>
+ * <string>value3</string>
+ * </array>
+ *
+ * <key>dictionnary</key>
+ * <dict>
+ * <key>key1</key>
+ * <string>value1</string>
+ * <key>key2</key>
+ * <string>value2</string>
+ * <key>key3</key>
+ * <string>value3</string>
+ * </dict>
+ *
+ * <key>nested</key>
+ * <dict>
+ * <key>node1</key>
+ * <dict>
+ * <key>node2</key>
+ * <dict>
+ * <key>node3</key>
+ * <string>value</string>
+ * </dict>
+ * </dict>
+ * </dict>
+ *
+ * </dict>
+ * </plist>
+ * </pre>
+ *
+ * @since 1.2
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: XMLPropertyListConfiguration.java 1368665 2012-08-02 19:48:26Z oheger $
+ */
+public class XMLPropertyListConfiguration extends AbstractHierarchicalFileConfiguration
+{
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = -3162063751042475985L;
+
+ /** Size of the indentation for the generated file. */
+ private static final int INDENT_SIZE = 4;
+
+ /**
+ * Creates an empty XMLPropertyListConfiguration object which can be
+ * used to synthesize a new plist file by adding values and
+ * then saving().
+ */
+ public XMLPropertyListConfiguration()
+ {
+ initRoot();
+ }
+
+ /**
+ * Creates a new instance of {@code XMLPropertyListConfiguration} and
+ * copies the content of the specified configuration into this object.
+ *
+ * @param configuration the configuration to copy
+ * @since 1.4
+ */
+ public XMLPropertyListConfiguration(HierarchicalConfiguration configuration)
+ {
+ super(configuration);
+ }
+
+ /**
+ * Creates and loads the property list from the specified file.
+ *
+ * @param fileName The name of the plist file to load.
+ * @throws org.apache.commons.configuration.ConfigurationException Error
+ * while loading the plist file
+ */
+ public XMLPropertyListConfiguration(String fileName) throws ConfigurationException
+ {
+ super(fileName);
+ }
+
+ /**
+ * Creates and loads the property list from the specified file.
+ *
+ * @param file The plist file to load.
+ * @throws ConfigurationException Error while loading the plist file
+ */
+ public XMLPropertyListConfiguration(File file) throws ConfigurationException
+ {
+ super(file);
+ }
+
+ /**
+ * Creates and loads the property list from the specified URL.
+ *
+ * @param url The location of the plist file to load.
+ * @throws ConfigurationException Error while loading the plist file
+ */
+ public XMLPropertyListConfiguration(URL url) throws ConfigurationException
+ {
+ super(url);
+ }
+
+ @Override
+ public void setProperty(String key, Object value)
+ {
+ // special case for byte arrays, they must be stored as is in the configuration
+ if (value instanceof byte[])
+ {
+ fireEvent(EVENT_SET_PROPERTY, key, value, true);
+ setDetailEvents(false);
+ try
+ {
+ clearProperty(key);
+ addPropertyDirect(key, value);
+ }
+ finally
+ {
+ setDetailEvents(true);
+ }
+ fireEvent(EVENT_SET_PROPERTY, key, value, false);
+ }
+ else
+ {
+ super.setProperty(key, value);
+ }
+ }
+
+ @Override
+ public void addProperty(String key, Object value)
+ {
+ if (value instanceof byte[])
+ {
+ fireEvent(EVENT_ADD_PROPERTY, key, value, true);
+ addPropertyDirect(key, value);
+ fireEvent(EVENT_ADD_PROPERTY, key, value, false);
+ }
+ else
+ {
+ super.addProperty(key, value);
+ }
+ }
+
+ public void load(Reader in) throws ConfigurationException
+ {
+ // We have to make sure that the root node is actually a PListNode.
+ // If this object was not created using the standard constructor, the
+ // root node is a plain Node.
+ if (!(getRootNode() instanceof PListNode))
+ {
+ initRoot();
+ }
+
+ // set up the DTD validation
+ EntityResolver resolver = new EntityResolver()
+ {
+ public InputSource resolveEntity(String publicId, String systemId)
+ {
+ return new InputSource(getClass().getClassLoader().getResourceAsStream("PropertyList-1.0.dtd"));
+ }
+ };
+
+ // parse the file
+ XMLPropertyListHandler handler = new XMLPropertyListHandler(getRoot());
+ try
+ {
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ factory.setValidating(true);
+
+ SAXParser parser = factory.newSAXParser();
+ parser.getXMLReader().setEntityResolver(resolver);
+ parser.getXMLReader().setContentHandler(handler);
+ parser.getXMLReader().parse(new InputSource(in));
+ }
+ catch (Exception e)
+ {
+ throw new ConfigurationException("Unable to parse the configuration file", e);
+ }
+ }
+
+ public void save(Writer out) throws ConfigurationException
+ {
+ PrintWriter writer = new PrintWriter(out);
+
+ if (getEncoding() != null)
+ {
+ writer.println("<?xml version=\"1.0\" encoding=\"" + getEncoding() + "\"?>");
+ }
+ else
+ {
+ writer.println("<?xml version=\"1.0\"?>");
+ }
+
+ writer.println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">");
+ writer.println("<plist version=\"1.0\">");
+
+ printNode(writer, 1, getRoot());
+
+ writer.println("</plist>");
+ writer.flush();
+ }
+
+ /**
+ * Append a node to the writer, indented according to a specific level.
+ */
+ private void printNode(PrintWriter out, int indentLevel, ConfigurationNode node)
+ {
+ String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
+
+ if (node.getName() != null)
+ {
+ out.println(padding + "<key>" + StringEscapeUtils.escapeXml(node.getName()) + "</key>");
+ }
+
+ List<ConfigurationNode> children = node.getChildren();
+ if (!children.isEmpty())
+ {
+ out.println(padding + "<dict>");
+
+ Iterator<ConfigurationNode> it = children.iterator();
+ while (it.hasNext())
+ {
+ ConfigurationNode child = it.next();
+ printNode(out, indentLevel + 1, child);
+
+ if (it.hasNext())
+ {
+ out.println();
+ }
+ }
+
+ out.println(padding + "</dict>");
+ }
+ else if (node.getValue() == null)
+ {
+ out.println(padding + "<dict/>");
+ }
+ else
+ {
+ Object value = node.getValue();
+ printValue(out, indentLevel, value);
+ }
+ }
+
+ /**
+ * Append a value to the writer, indented according to a specific level.
+ */
+ private void printValue(PrintWriter out, int indentLevel, Object value)
+ {
+ String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
+
+ if (value instanceof Date)
+ {
+ synchronized (PListNode.FORMAT)
+ {
+ out.println(padding + "<date>" + PListNode.FORMAT.format((Date) value) + "</date>");
+ }
+ }
+ else if (value instanceof Calendar)
+ {
+ printValue(out, indentLevel, ((Calendar) value).getTime());
+ }
+ else if (value instanceof Number)
+ {
+ if (value instanceof Double || value instanceof Float || value instanceof BigDecimal)
+ {
+ out.println(padding + "<real>" + value.toString() + "</real>");
+ }
+ else
+ {
+ out.println(padding + "<integer>" + value.toString() + "</integer>");
+ }
+ }
+ else if (value instanceof Boolean)
+ {
+ if (((Boolean) value).booleanValue())
+ {
+ out.println(padding + "<true/>");
+ }
+ else
+ {
+ out.println(padding + "<false/>");
+ }
+ }
+ else if (value instanceof List)
+ {
+ out.println(padding + "<array>");
+ Iterator<?> it = ((List<?>) value).iterator();
+ while (it.hasNext())
+ {
+ printValue(out, indentLevel + 1, it.next());
+ }
+ out.println(padding + "</array>");
+ }
+ else if (value instanceof HierarchicalConfiguration)
+ {
+ printNode(out, indentLevel, ((HierarchicalConfiguration) value).getRoot());
+ }
+ else if (value instanceof Configuration)
+ {
+ // display a flat Configuration as a dictionary
+ out.println(padding + "<dict>");
+
+ Configuration config = (Configuration) value;
+ Iterator<String> it = config.getKeys();
+ while (it.hasNext())
+ {
+ // create a node for each property
+ String key = it.next();
+ Node node = new Node(key);
+ node.setValue(config.getProperty(key));
+
+ // print the node
+ printNode(out, indentLevel + 1, node);
+
+ if (it.hasNext())
+ {
+ out.println();
+ }
+ }
+ out.println(padding + "</dict>");
+ }
+ else if (value instanceof Map)
+ {
+ // display a Map as a dictionary
+ Map<String, Object> map = transformMap((Map<?, ?>) value);
+ printValue(out, indentLevel, new MapConfiguration(map));
+ }
+ else if (value instanceof byte[])
+ {
+ String base64 = new String(Base64.encodeBase64((byte[]) value));
+ out.println(padding + "<data>" + StringEscapeUtils.escapeXml(base64) + "</data>");
+ }
+ else if (value != null)
+ {
+ out.println(padding + "<string>" + StringEscapeUtils.escapeXml(String.valueOf(value)) + "</string>");
+ }
+ else
+ {
+ out.println(padding + "<string/>");
+ }
+ }
+
+ /**
+ * Helper method for initializing the configuration's root node.
+ */
+ private void initRoot()
+ {
+ setRootNode(new PListNode());
+ }
+
+ /**
+ * Transform a map of arbitrary types into a map with string keys and object
+ * values. All keys of the source map which are not of type String are
+ * dropped.
+ *
+ * @param src the map to be converted
+ * @return the resulting map
+ */
+ private static Map<String, Object> transformMap(Map<?, ?> src)
+ {
+ Map<String, Object> dest = new HashMap<String, Object>();
+ for (Map.Entry<?, ?> e : src.entrySet())
+ {
+ if (e.getKey() instanceof String)
+ {
+ dest.put((String) e.getKey(), e.getValue());
+ }
+ }
+ return dest;
+ }
+
+ /**
+ * SAX Handler to build the configuration nodes while the document is being parsed.
+ */
+ private class XMLPropertyListHandler extends DefaultHandler
+ {
+ /** The buffer containing the text node being read */
+ private StringBuilder buffer = new StringBuilder();
+
+ /** The stack of configuration nodes */
+ private List<Node> stack = new ArrayList<Node>();
+
+ public XMLPropertyListHandler(Node root)
+ {
+ push(root);
+ }
+
+ /**
+ * Return the node on the top of the stack.
+ */
+ private Node peek()
+ {
+ if (!stack.isEmpty())
+ {
+ return stack.get(stack.size() - 1);
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Remove and return the node on the top of the stack.
+ */
+ private Node pop()
+ {
+ if (!stack.isEmpty())
+ {
+ return stack.remove(stack.size() - 1);
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Put a node on the top of the stack.
+ */
+ private void push(Node node)
+ {
+ stack.add(node);
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
+ {
+ if ("array".equals(qName))
+ {
+ push(new ArrayNode());
+ }
+ else if ("dict".equals(qName))
+ {
+ if (peek() instanceof ArrayNode)
+ {
+ // create the configuration
+ XMLPropertyListConfiguration config = new XMLPropertyListConfiguration();
+
+ // add it to the ArrayNode
+ ArrayNode node = (ArrayNode) peek();
+ node.addValue(config);
+
+ // push the root on the stack
+ push(config.getRoot());
+ }
+ }
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) throws SAXException
+ {
+ if ("key".equals(qName))
+ {
+ // create a new node, link it to its parent and push it on the stack
+ PListNode node = new PListNode();
+ node.setName(buffer.toString());
+ peek().addChild(node);
+ push(node);
+ }
+ else if ("dict".equals(qName))
+ {
+ // remove the root of the XMLPropertyListConfiguration previously pushed on the stack
+ pop();
+ }
+ else
+ {
+ if ("string".equals(qName))
+ {
+ ((PListNode) peek()).addValue(buffer.toString());
+ }
+ else if ("integer".equals(qName))
+ {
+ ((PListNode) peek()).addIntegerValue(buffer.toString());
+ }
+ else if ("real".equals(qName))
+ {
+ ((PListNode) peek()).addRealValue(buffer.toString());
+ }
+ else if ("true".equals(qName))
+ {
+ ((PListNode) peek()).addTrueValue();
+ }
+ else if ("false".equals(qName))
+ {
+ ((PListNode) peek()).addFalseValue();
+ }
+ else if ("data".equals(qName))
+ {
+ ((PListNode) peek()).addDataValue(buffer.toString());
+ }
+ else if ("date".equals(qName))
+ {
+ try
+ {
+ ((PListNode) peek()).addDateValue(buffer.toString());
+ }
+ catch (IllegalArgumentException iex)
+ {
+ getLogger().warn(
+ "Ignoring invalid date property " + buffer);
+ }
+ }
+ else if ("array".equals(qName))
+ {
+ ArrayNode array = (ArrayNode) pop();
+ ((PListNode) peek()).addList(array);
+ }
+
+ // remove the plist node on the stack once the value has been parsed,
+ // array nodes remains on the stack for the next values in the list
+ if (!(peek() instanceof ArrayNode))
+ {
+ pop();
+ }
+ }
+
+ buffer.setLength(0);
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException
+ {
+ buffer.append(ch, start, length);
+ }
+ }
+
+ /**
+ * Node extension with addXXX methods to parse the typed data passed by the SAX handler.
+ * <b>Do not use this class !</b> It is used internally by XMLPropertyConfiguration
+ * to parse the configuration file, it may be removed at any moment in the future.
+ */
+ public static class PListNode extends Node
+ {
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = -7614060264754798317L;
+
+ /**
+ * The MacOS FORMAT of dates in plist files. Note: Because
+ * {@code SimpleDateFormat} is not thread-safe, each access has to be
+ * synchronized.
+ */
+ private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+ static
+ {
+ FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ /**
+ * The GNUstep FORMAT of dates in plist files. Note: Because
+ * {@code SimpleDateFormat} is not thread-safe, each access has to be
+ * synchronized.
+ */
+ private static final DateFormat GNUSTEP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
+
+ /**
+ * Update the value of the node. If the existing value is null, it's
+ * replaced with the new value. If the existing value is a list, the
+ * specified value is appended to the list. If the existing value is
+ * not null, a list with the two values is built.
+ *
+ * @param value the value to be added
+ */
+ public void addValue(Object value)
+ {
+ if (getValue() == null)
+ {
+ setValue(value);
+ }
+ else if (getValue() instanceof Collection)
+ {
+ // This is safe because we create the collections ourselves
+ @SuppressWarnings("unchecked")
+ Collection<Object> collection = (Collection<Object>) getValue();
+ collection.add(value);
+ }
+ else
+ {
+ List<Object> list = new ArrayList<Object>();
+ list.add(getValue());
+ list.add(value);
+ setValue(list);
+ }
+ }
+
+ /**
+ * Parse the specified string as a date and add it to the values of the node.
+ *
+ * @param value the value to be added
+ * @throws IllegalArgumentException if the date string cannot be parsed
+ */
+ public void addDateValue(String value)
+ {
+ try
+ {
+ if (value.indexOf(' ') != -1)
+ {
+ // parse the date using the GNUstep FORMAT
+ synchronized (GNUSTEP_FORMAT)
+ {
+ addValue(GNUSTEP_FORMAT.parse(value));
+ }
+ }
+ else
+ {
+ // parse the date using the MacOS X FORMAT
+ synchronized (FORMAT)
+ {
+ addValue(FORMAT.parse(value));
+ }
+ }
+ }
+ catch (ParseException e)
+ {
+ throw new IllegalArgumentException(String.format(
+ "'%s' cannot be parsed to a date!", value), e);
+ }
+ }
+
+ /**
+ * Parse the specified string as a byte array in base 64 FORMAT
+ * and add it to the values of the node.
+ *
+ * @param value the value to be added
+ */
+ public void addDataValue(String value)
+ {
+ addValue(Base64.decodeBase64(value.getBytes()));
+ }
+
+ /**
+ * Parse the specified string as an Interger and add it to the values of the node.
+ *
+ * @param value the value to be added
+ */
+ public void addIntegerValue(String value)
+ {
+ addValue(new BigInteger(value));
+ }
+
+ /**
+ * Parse the specified string as a Double and add it to the values of the node.
+ *
+ * @param value the value to be added
+ */
+ public void addRealValue(String value)
+ {
+ addValue(new BigDecimal(value));
+ }
+
+ /**
+ * Add a boolean value 'true' to the values of the node.
+ */
+ public void addTrueValue()
+ {
+ addValue(Boolean.TRUE);
+ }
+
+ /**
+ * Add a boolean value 'false' to the values of the node.
+ */
+ public void addFalseValue()
+ {
+ addValue(Boolean.FALSE);
+ }
+
+ /**
+ * Add a sublist to the values of the node.
+ *
+ * @param node the node whose value will be added to the current node value
+ */
+ public void addList(ArrayNode node)
+ {
+ addValue(node.getValue());
+ }
+ }
+
+ /**
+ * Container for array elements. <b>Do not use this class !</b>
+ * It is used internally by XMLPropertyConfiguration to parse the
+ * configuration file, it may be removed at any moment in the future.
+ */
+ public static class ArrayNode extends PListNode
+ {
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = 5586544306664205835L;
+
+ /** The list of values in the array. */
+ private List<Object> list = new ArrayList<Object>();
+
+ /**
+ * Add an object to the array.
+ *
+ * @param value the value to be added
+ */
+ @Override
+ public void addValue(Object value)
+ {
+ list.add(value);
+ }
+
+ /**
+ * Return the list of values in the array.
+ *
+ * @return the {@link List} of values
+ */
+ @Override
+ public Object getValue()
+ {
+ return list;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/plist/package.html b/src/main/java/org/apache/commons/configuration/plist/package.html
new file mode 100644
index 0000000..5a4acb8
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/plist/package.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+<head>
+</head>
+<body>
+
+<p>
+Configuration classes supporting NeXT / OpenStep /GNUStep style configuration.
+</p>
+<p>
+<font size="-2">$Id: package.html 527436 2007-04-11 09:57:29Z ebourg $</font>
+</p>
+
+</body>
+</html>
diff --git a/src/main/java/org/apache/commons/configuration/reloading/FileChangedReloadingStrategy.java b/src/main/java/org/apache/commons/configuration/reloading/FileChangedReloadingStrategy.java
new file mode 100644
index 0000000..bcdda7a
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/reloading/FileChangedReloadingStrategy.java
@@ -0,0 +1,226 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.reloading;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.apache.commons.configuration.ConfigurationUtils;
+import org.apache.commons.configuration.FileConfiguration;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <p>A reloading strategy that will reload the configuration every time its
+ * underlying file is changed.</p>
+ * <p>This reloading strategy does not actively monitor a configuration file,
+ * but is triggered by its associated configuration whenever properties are
+ * accessed. It then checks the configuration file's last modification date
+ * and causes a reload if this has changed.</p>
+ * <p>To avoid permanent disc access on successive property lookups a refresh
+ * delay can be specified. This has the effect that the configuration file's
+ * last modification date is only checked once in this delay period. The default
+ * value for this refresh delay is 5 seconds.</p>
+ * <p>This strategy only works with FileConfiguration instances.</p>
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: FileChangedReloadingStrategy.java 1210646 2011-12-05 21:25:01Z oheger $
+ * @since 1.1
+ */
+public class FileChangedReloadingStrategy implements ReloadingStrategy
+{
+ /** Constant for the jar URL protocol.*/
+ private static final String JAR_PROTOCOL = "jar";
+
+ /** Constant for the default refresh delay.*/
+ private static final int DEFAULT_REFRESH_DELAY = 5000;
+
+ /** Stores a reference to the configuration to be monitored.*/
+ protected FileConfiguration configuration;
+
+ /** The last time the configuration file was modified. */
+ protected long lastModified;
+
+ /** The last time the file was checked for changes. */
+ protected long lastChecked;
+
+ /** The minimum delay in milliseconds between checks. */
+ protected long refreshDelay = DEFAULT_REFRESH_DELAY;
+
+ /** A flag whether a reload is required.*/
+ private boolean reloading;
+
+ /** The Log to use for diagnostic messages */
+ private Log logger = LogFactory.getLog(FileChangedReloadingStrategy.class);
+
+ public void setConfiguration(FileConfiguration configuration)
+ {
+ this.configuration = configuration;
+ }
+
+ public void init()
+ {
+ updateLastModified();
+ }
+
+ public boolean reloadingRequired()
+ {
+ if (!reloading)
+ {
+ long now = System.currentTimeMillis();
+
+ if (now > lastChecked + refreshDelay)
+ {
+ lastChecked = now;
+ if (hasChanged())
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("File change detected: " + getName());
+ }
+ reloading = true;
+ }
+ }
+ }
+
+ return reloading;
+ }
+
+ public void reloadingPerformed()
+ {
+ updateLastModified();
+ }
+
+ /**
+ * Return the minimal time in milliseconds between two reloadings.
+ *
+ * @return the refresh delay (in milliseconds)
+ */
+ public long getRefreshDelay()
+ {
+ return refreshDelay;
+ }
+
+ /**
+ * Set the minimal time between two reloadings.
+ *
+ * @param refreshDelay refresh delay in milliseconds
+ */
+ public void setRefreshDelay(long refreshDelay)
+ {
+ this.refreshDelay = refreshDelay;
+ }
+
+ /**
+ * Update the last modified time.
+ */
+ protected void updateLastModified()
+ {
+ File file = getFile();
+ if (file != null)
+ {
+ lastModified = file.lastModified();
+ }
+ reloading = false;
+ }
+
+ /**
+ * Check if the configuration has changed since the last time it was loaded.
+ *
+ * @return a flag whether the configuration has changed
+ */
+ protected boolean hasChanged()
+ {
+ File file = getFile();
+ if (file == null || !file.exists())
+ {
+ if (logger.isWarnEnabled() && lastModified != 0)
+ {
+ logger.warn("File was deleted: " + getName(file));
+ lastModified = 0;
+ }
+ return false;
+ }
+
+ return file.lastModified() > lastModified;
+ }
+
+ /**
+ * Returns the file that is monitored by this strategy. Note that the return
+ * value can be <b>null </b> under some circumstances.
+ *
+ * @return the monitored file
+ */
+ protected File getFile()
+ {
+ return (configuration.getURL() != null) ? fileFromURL(configuration
+ .getURL()) : configuration.getFile();
+ }
+
+ /**
+ * Helper method for transforming a URL into a file object. This method
+ * handles file: and jar: URLs.
+ *
+ * @param url the URL to be converted
+ * @return the resulting file or <b>null </b>
+ */
+ private File fileFromURL(URL url)
+ {
+ if (JAR_PROTOCOL.equals(url.getProtocol()))
+ {
+ String path = url.getPath();
+ try
+ {
+ return ConfigurationUtils.fileFromURL(new URL(path.substring(0,
+ path.indexOf('!'))));
+ }
+ catch (MalformedURLException mex)
+ {
+ return null;
+ }
+ }
+ else
+ {
+ return ConfigurationUtils.fileFromURL(url);
+ }
+ }
+
+ private String getName()
+ {
+ return getName(getFile());
+ }
+
+ private String getName(File file)
+ {
+ String name = configuration.getURL().toString();
+ if (name == null)
+ {
+ if (file != null)
+ {
+ name = file.getAbsolutePath();
+ }
+ else
+ {
+ name = "base: " + configuration.getBasePath()
+ + "file: " + configuration.getFileName();
+ }
+ }
+ return name;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/reloading/InvariantReloadingStrategy.java b/src/main/java/org/apache/commons/configuration/reloading/InvariantReloadingStrategy.java
new file mode 100644
index 0000000..0553179
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/reloading/InvariantReloadingStrategy.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.reloading;
+
+import org.apache.commons.configuration.FileConfiguration;
+
+/**
+ * A strategy that never triggers a reloading.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: InvariantReloadingStrategy.java 1210646 2011-12-05 21:25:01Z oheger $
+ * @since 1.1
+ */
+public class InvariantReloadingStrategy implements ReloadingStrategy
+{
+ public void setConfiguration(FileConfiguration configuration)
+ {
+ }
+
+ public void init()
+ {
+ }
+
+ public boolean reloadingRequired()
+ {
+ return false;
+ }
+
+ public void reloadingPerformed()
+ {
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/reloading/ManagedReloadingStrategy.java b/src/main/java/org/apache/commons/configuration/reloading/ManagedReloadingStrategy.java
new file mode 100644
index 0000000..2d0e351
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/reloading/ManagedReloadingStrategy.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.reloading;
+
+import org.apache.commons.configuration.FileConfiguration;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * A strategy to reload configuration based on management requests. Designed for
+ * JMX management.
+ *
+ * @author Nicolas De loof
+ * @version $Id: ManagedReloadingStrategy.java 1210646 2011-12-05 21:25:01Z oheger $
+ */
+public class ManagedReloadingStrategy implements ReloadingStrategy,
+ ManagedReloadingStrategyMBean
+{
+ /** The logger. */
+ private Log log = LogFactory.getLog(ManagedReloadingStrategy.class);
+
+ /** Stores a reference to the associated configuration. */
+ private FileConfiguration configuration;
+
+ /** A flag whether a reload is required. */
+ private boolean reloadingRequired;
+
+ /**
+ * @see org.apache.commons.configuration.reloading.ReloadingStrategy#init()
+ */
+ public void init()
+ {
+ }
+
+ /**
+ * @see org.apache.commons.configuration.reloading.ReloadingStrategy#reloadingPerformed()
+ */
+ public void reloadingPerformed()
+ {
+ reloadingRequired = false;
+ }
+
+ /**
+ * Checks whether reloading is required. This implementation checks whether
+ * the {@code refresh()} method has been invoked.
+ *
+ * @return a flag whether reloading is required
+ * @see org.apache.commons.configuration.reloading.ReloadingStrategy#reloadingRequired()
+ */
+ public boolean reloadingRequired()
+ {
+ return reloadingRequired;
+ }
+
+ /**
+ * Sets the associated configuration.
+ *
+ * @param configuration the associated configuration
+ */
+ public void setConfiguration(FileConfiguration configuration)
+ {
+ this.configuration = configuration;
+ }
+
+ /**
+ * Tells this strategy that the monitored configuration file should be
+ * refreshed. This method will typically be called from outside (through an
+ * exposed MBean) on behalf of an administrator.
+ *
+ * @see org.apache.commons.configuration.reloading.ManagedReloadingStrategyMBean#refresh()
+ */
+ public void refresh()
+ {
+ log.info("Reloading configuration.");
+ this.reloadingRequired = true;
+ // force reloading
+ configuration.isEmpty();
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/reloading/ManagedReloadingStrategyMBean.java b/src/main/java/org/apache/commons/configuration/reloading/ManagedReloadingStrategyMBean.java
new file mode 100644
index 0000000..205bbcb
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/reloading/ManagedReloadingStrategyMBean.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.reloading;
+
+/**
+ * MBean definition for managing configuration reload.
+ *
+ * @author Nicolas De Loof
+ * @version $Id: ManagedReloadingStrategyMBean.java 1210646 2011-12-05 21:25:01Z oheger $
+ */
+public interface ManagedReloadingStrategyMBean
+{
+ /**
+ * Management method to force configuration reload.
+ */
+ void refresh();
+}
diff --git a/src/main/java/org/apache/commons/configuration/reloading/Reloadable.java b/src/main/java/org/apache/commons/configuration/reloading/Reloadable.java
new file mode 100644
index 0000000..c3b037c
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/reloading/Reloadable.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.reloading;
+
+/**
+ * Interface that allows other objects to synchronize on a root lock.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @since 1.7
+ * @version $Id: Reloadable.java 1162387 2011-08-27 16:05:20Z oheger $
+ */
+public interface Reloadable
+{
+ Object getReloadLock();
+}
diff --git a/src/main/java/org/apache/commons/configuration/reloading/ReloadingStrategy.java b/src/main/java/org/apache/commons/configuration/reloading/ReloadingStrategy.java
new file mode 100644
index 0000000..32f91ee
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/reloading/ReloadingStrategy.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.reloading;
+
+import org.apache.commons.configuration.FileConfiguration;
+
+/**
+ * A strategy to decide if a configuration should be reloaded.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: ReloadingStrategy.java 1210646 2011-12-05 21:25:01Z oheger $
+ * @since 1.1
+ */
+public interface ReloadingStrategy
+{
+ /**
+ * Set the configuration managed by this strategy.
+ *
+ * @param configuration the configuration to monitor
+ */
+ void setConfiguration(FileConfiguration configuration);
+
+ /**
+ * Initialize the strategy.
+ */
+ void init();
+
+ /**
+ * Tell if the evaluation of the strategy requires to reload the configuration.
+ *
+ * @return a flag whether a reload should be performed
+ */
+ boolean reloadingRequired();
+
+ /**
+ * Notify the strategy that the file has been reloaded.
+ */
+ void reloadingPerformed();
+
+}
diff --git a/src/main/java/org/apache/commons/configuration/reloading/VFSFileChangedReloadingStrategy.java b/src/main/java/org/apache/commons/configuration/reloading/VFSFileChangedReloadingStrategy.java
new file mode 100644
index 0000000..f7bc24f
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/reloading/VFSFileChangedReloadingStrategy.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.reloading;
+
+import org.apache.commons.configuration.ConfigurationRuntimeException;
+import org.apache.commons.configuration.FileConfiguration;
+import org.apache.commons.configuration.FileSystem;
+import org.apache.commons.configuration.FileSystemBased;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileSystemManager;
+import org.apache.commons.vfs2.VFS;
+
+/**
+ * <p>
+ * A file-based reloading strategy that uses <a
+ * href="http://commons.apache.org/vfs/">Commons VFS</a> to determine when a
+ * file was changed.
+ * </p>
+ * <p>
+ * This reloading strategy is very similar to
+ * {@link FileChangedReloadingStrategy}, except for the fact that it uses VFS
+ * and thus can deal with a variety of different configuration sources.
+ * </p>
+ * <p>
+ * This strategy only works with FileConfiguration instances.
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: VFSFileChangedReloadingStrategy.java 1162383 2011-08-27 15:57:11Z oheger $
+ * @since 1.7
+ */
+public class VFSFileChangedReloadingStrategy implements ReloadingStrategy
+{
+ /** Constant for the default refresh delay.*/
+ private static final int DEFAULT_REFRESH_DELAY = 5000;
+
+ /** Stores a reference to the configuration to be monitored.*/
+ protected FileConfiguration configuration;
+
+ /** The last time the configuration file was modified. */
+ protected long lastModified;
+
+ /** The last time the file was checked for changes. */
+ protected long lastChecked;
+
+ /** The minimum delay in milliseconds between checks. */
+ protected long refreshDelay = DEFAULT_REFRESH_DELAY;
+
+ /** A flag whether a reload is required.*/
+ private boolean reloading;
+
+ /** Stores the logger.*/
+ private Log log = LogFactory.getLog(getClass());
+
+ public void setConfiguration(FileConfiguration configuration)
+ {
+ this.configuration = configuration;
+ }
+
+ public void init()
+ {
+ if (configuration.getURL() == null && configuration.getFileName() == null)
+ {
+ return;
+ }
+ if (this.configuration == null)
+ {
+ throw new IllegalStateException("No configuration has been set for this strategy");
+ }
+ updateLastModified();
+ }
+
+ public boolean reloadingRequired()
+ {
+ if (!reloading)
+ {
+ long now = System.currentTimeMillis();
+
+ if (now > lastChecked + refreshDelay)
+ {
+ lastChecked = now;
+ if (hasChanged())
+ {
+ reloading = true;
+ }
+ }
+ }
+
+ return reloading;
+ }
+
+ public void reloadingPerformed()
+ {
+ updateLastModified();
+ }
+
+ /**
+ * Return the minimal time in milliseconds between two reloadings.
+ *
+ * @return the refresh delay (in milliseconds)
+ */
+ public long getRefreshDelay()
+ {
+ return refreshDelay;
+ }
+
+ /**
+ * Set the minimal time between two reloadings.
+ *
+ * @param refreshDelay refresh delay in milliseconds
+ */
+ public void setRefreshDelay(long refreshDelay)
+ {
+ this.refreshDelay = refreshDelay;
+ }
+
+ /**
+ * Update the last modified time.
+ */
+ protected void updateLastModified()
+ {
+ FileObject file = getFile();
+ if (file != null)
+ {
+ try
+ {
+ lastModified = file.getContent().getLastModifiedTime();
+ }
+ catch (FileSystemException fse)
+ {
+ log.error("Unable to get last modified time for" + file.getName().getURI());
+ }
+ }
+ reloading = false;
+ }
+
+ /**
+ * Check if the configuration has changed since the last time it was loaded.
+ *
+ * @return a flag whether the configuration has changed
+ */
+ protected boolean hasChanged()
+ {
+ FileObject file = getFile();
+ try
+ {
+ if (file == null || !file.exists())
+ {
+ return false;
+ }
+
+ return file.getContent().getLastModifiedTime() > lastModified;
+ }
+ catch (FileSystemException ex)
+ {
+ log.error("Unable to get last modified time for" + file.getName().getURI());
+ return false;
+ }
+ }
+
+ /**
+ * Returns the file that is monitored by this strategy. Note that the return
+ * value can be <b>null </b> under some circumstances.
+ *
+ * @return the monitored file
+ */
+ protected FileObject getFile()
+ {
+ try
+ {
+ FileSystemManager fsManager = VFS.getManager();
+ FileSystem fs = ((FileSystemBased) configuration).getFileSystem();
+ String uri = fs.getPath(null, configuration.getURL(), configuration.getBasePath(),
+ configuration.getFileName());
+ if (uri == null)
+ {
+ throw new ConfigurationRuntimeException("Unable to determine file to monitor");
+ }
+ return fsManager.resolveFile(uri);
+ }
+ catch (FileSystemException fse)
+ {
+ String msg = "Unable to monitor " + configuration.getURL().toString();
+ log.error(msg);
+ throw new ConfigurationRuntimeException(msg, fse);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/reloading/package.html b/src/main/java/org/apache/commons/configuration/reloading/package.html
new file mode 100644
index 0000000..b9f1ba8
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/reloading/package.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+<head>
+</head>
+<body>
+
+<p>
+The <code>reloading</code> package contains the definition of the
+<code>ReloadingStrategy</code> interface, which provides automatic reloading
+facilities for file based configurations. There are also some concrete
+implementations of this interface that can be used out of the box.
+</p>
+<p>
+<font size="-2">$Id: package.html 439648 2006-09-02 20:42:10Z oheger $</font>
+</p>
+
+</body>
+</html>
diff --git a/src/main/java/org/apache/commons/configuration/resolver/CatalogResolver.java b/src/main/java/org/apache/commons/configuration/resolver/CatalogResolver.java
new file mode 100644
index 0000000..4e4cf7a
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/resolver/CatalogResolver.java
@@ -0,0 +1,521 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.resolver;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.FileNameMap;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Vector;
+
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.ConfigurationUtils;
+import org.apache.commons.configuration.FileSystem;
+import org.apache.commons.lang.text.StrSubstitutor;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.xml.resolver.CatalogException;
+import org.apache.xml.resolver.readers.CatalogReader;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+/**
+ * Thin wrapper around xml commons CatalogResolver to allow list of catalogs
+ * to be provided.
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @since 1.7
+ * @version $Id: CatalogResolver.java 1301991 2012-03-17 20:18:02Z sebb $
+ */
+public class CatalogResolver implements EntityResolver
+{
+ /**
+ * Debug everything.
+ */
+ private static final int DEBUG_ALL = 9;
+
+ /**
+ * Normal debug setting.
+ */
+ private static final int DEBUG_NORMAL = 4;
+
+ /**
+ * Debug nothing.
+ */
+ private static final int DEBUG_NONE = 0;
+
+ /**
+ * The CatalogManager
+ */
+ protected CatalogManager manager = new CatalogManager();
+
+ /**
+ * The FileSystem in use.
+ */
+ protected FileSystem fs = FileSystem.getDefaultFileSystem();
+
+ /**
+ * The CatalogResolver
+ */
+ private org.apache.xml.resolver.tools.CatalogResolver resolver;
+
+ /**
+ * Stores the logger.
+ */
+ private Log log;
+
+ /**
+ * Constructs the CatalogResolver
+ */
+ public CatalogResolver()
+ {
+ manager.setIgnoreMissingProperties(true);
+ manager.setUseStaticCatalog(false);
+ manager.setFileSystem(fs);
+ setLogger(null);
+ }
+
+ /**
+ * Set the list of catalog file names
+ *
+ * @param catalogs The delimited list of catalog files.
+ */
+ public void setCatalogFiles(String catalogs)
+ {
+ manager.setCatalogFiles(catalogs);
+ }
+
+ /**
+ * Set the FileSystem.
+ * @param fileSystem The FileSystem.
+ */
+ public void setFileSystem(FileSystem fileSystem)
+ {
+ this.fs = fileSystem;
+ manager.setFileSystem(fileSystem);
+ }
+
+ /**
+ * Set the base path.
+ * @param baseDir The base path String.
+ */
+ public void setBaseDir(String baseDir)
+ {
+ manager.setBaseDir(baseDir);
+ }
+
+ /**
+ * Set the StrSubstitutor.
+ * @param substitutor The StrSubstitutor.
+ */
+ public void setSubstitutor(StrSubstitutor substitutor)
+ {
+ manager.setSubstitutor(substitutor);
+ }
+
+ /**
+ * Enables debug logging of xml-commons Catalog processing.
+ * @param debug True if debugging should be enabled, false otherwise.
+ */
+ public void setDebug(boolean debug)
+ {
+ if (debug)
+ {
+ manager.setVerbosity(DEBUG_ALL);
+ }
+ else
+ {
+ manager.setVerbosity(DEBUG_NONE);
+ }
+ }
+
+ /**
+ * Implements the {@code resolveEntity} method
+ * for the SAX interface.
+ * <p/>
+ * <p>Presented with an optional public identifier and a system
+ * identifier, this function attempts to locate a mapping in the
+ * catalogs.</p>
+ * <p/>
+ * <p>If such a mapping is found, the resolver attempts to open
+ * the mapped value as an InputSource and return it. Exceptions are
+ * ignored and null is returned if the mapped value cannot be opened
+ * as an input source.</p>
+ * <p/>
+ * <p>If no mapping is found (or an error occurs attempting to open
+ * the mapped value as an input source), null is returned and the system
+ * will use the specified system identifier as if no entityResolver
+ * was specified.</p>
+ *
+ * @param publicId The public identifier for the entity in question.
+ * This may be null.
+ * @param systemId The system identifier for the entity in question.
+ * XML requires a system identifier on all external entities, so this
+ * value is always specified.
+ * @return An InputSource for the mapped identifier, or null.
+ * @throws SAXException if an error occurs.
+ */
+ public InputSource resolveEntity(String publicId, String systemId)
+ throws SAXException
+ {
+ String resolved = getResolver().getResolvedEntity(publicId, systemId);
+
+ if (resolved != null)
+ {
+ String badFilePrefix = "file://";
+ String correctFilePrefix = "file:///";
+
+ // Java 5 has a bug when constructing file URLS
+ if (resolved.startsWith(badFilePrefix) && !resolved.startsWith(correctFilePrefix))
+ {
+ resolved = correctFilePrefix + resolved.substring(badFilePrefix.length());
+ }
+
+ try
+ {
+ InputStream is = fs.getInputStream(null, resolved);
+ InputSource iSource = new InputSource(resolved);
+ iSource.setPublicId(publicId);
+ iSource.setByteStream(is);
+ return iSource;
+ }
+ catch (Exception e)
+ {
+ log.warn("Failed to create InputSource for " + resolved + " ("
+ + e.toString() + ")");
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the logger used by this configuration object.
+ *
+ * @return the logger
+ */
+ public Log getLogger()
+ {
+ return log;
+ }
+
+ /**
+ * Allows to set the logger to be used by this configuration object. This
+ * method makes it possible for clients to exactly control logging behavior.
+ * Per default a logger is set that will ignore all log messages. Derived
+ * classes that want to enable logging should call this method during their
+ * initialization with the logger to be used.
+ *
+ * @param log the new logger
+ */
+ public void setLogger(Log log)
+ {
+ this.log = (log != null) ? log : LogFactory.getLog(CatalogResolver.class);
+ }
+
+ private synchronized org.apache.xml.resolver.tools.CatalogResolver getResolver()
+ {
+ if (resolver == null)
+ {
+ resolver = new org.apache.xml.resolver.tools.CatalogResolver(manager);
+ }
+ return resolver;
+ }
+
+ /**
+ * Extend the CatalogManager to make the FileSystem and base directory accessible.
+ */
+ public static class CatalogManager extends org.apache.xml.resolver.CatalogManager
+ {
+ /** The static catalog used by this manager. */
+ private static org.apache.xml.resolver.Catalog staticCatalog;
+
+ /** The FileSystem */
+ private FileSystem fs;
+
+ /** The base directory */
+ private String baseDir = System.getProperty("user.dir");
+
+ /** The String Substitutor */
+ private StrSubstitutor substitutor;
+
+ /**
+ * Set the FileSystem
+ * @param fileSystem The FileSystem in use.
+ */
+ public void setFileSystem(FileSystem fileSystem)
+ {
+ this.fs = fileSystem;
+ }
+
+ /**
+ * Retrieve the FileSystem.
+ * @return The FileSystem.
+ */
+ public FileSystem getFileSystem()
+ {
+ return this.fs;
+ }
+
+ /**
+ * Set the base directory.
+ * @param baseDir The base directory.
+ */
+ public void setBaseDir(String baseDir)
+ {
+ if (baseDir != null)
+ {
+ this.baseDir = baseDir;
+ }
+ }
+
+ /**
+ * Return the base directory.
+ * @return The base directory.
+ */
+ public String getBaseDir()
+ {
+ return this.baseDir;
+ }
+
+ public void setSubstitutor(StrSubstitutor substitutor)
+ {
+ this.substitutor = substitutor;
+ }
+
+ public StrSubstitutor getStrSubstitutor()
+ {
+ return this.substitutor;
+ }
+
+
+ /**
+ * Get a new catalog instance. This method is only overridden because xml-resolver
+ * might be in a parent ClassLoader and will be incapable of loading our Catalog
+ * implementation.
+ *
+ * This method always returns a new instance of the underlying catalog class.
+ * @return the Catalog.
+ */
+ @Override
+ public org.apache.xml.resolver.Catalog getPrivateCatalog()
+ {
+ org.apache.xml.resolver.Catalog catalog = staticCatalog;
+
+ if (catalog == null || !getUseStaticCatalog())
+ {
+ try
+ {
+ catalog = new Catalog();
+ catalog.setCatalogManager(this);
+ catalog.setupReaders();
+ catalog.loadSystemCatalogs();
+ }
+ catch (Exception ex)
+ {
+ ex.printStackTrace();
+ }
+
+ if (getUseStaticCatalog())
+ {
+ staticCatalog = catalog;
+ }
+ }
+
+ return catalog;
+ }
+
+ /**
+ * Get a catalog instance.
+ *
+ * If this manager uses static catalogs, the same static catalog will
+ * always be returned. Otherwise a new catalog will be returned.
+ * @return The Catalog.
+ */
+ @Override
+ public org.apache.xml.resolver.Catalog getCatalog()
+ {
+ return getPrivateCatalog();
+ }
+ }
+
+ /**
+ * Overrides the Catalog implementation to use the underlying FileSystem.
+ */
+ public static class Catalog extends org.apache.xml.resolver.Catalog
+ {
+ /** The FileSystem */
+ private FileSystem fs;
+
+ /** FileNameMap to determine the mime type */
+ private FileNameMap fileNameMap = URLConnection.getFileNameMap();
+
+ /**
+ * Load the catalogs.
+ * @throws IOException if an error occurs.
+ */
+ @Override
+ public void loadSystemCatalogs() throws IOException
+ {
+ fs = ((CatalogManager) catalogManager).getFileSystem();
+ String base = ((CatalogManager) catalogManager).getBaseDir();
+
+ // This is safe because the catalog manager returns a vector of strings.
+ @SuppressWarnings("unchecked")
+ Vector<String> catalogs = catalogManager.getCatalogFiles();
+ if (catalogs != null)
+ {
+ for (int count = 0; count < catalogs.size(); count++)
+ {
+ String fileName = catalogs.elementAt(count);
+
+ URL url = null;
+ InputStream is = null;
+
+ try
+ {
+ url = ConfigurationUtils.locate(fs, base, fileName);
+ if (url != null)
+ {
+ is = fs.getInputStream(url);
+ }
+ }
+ catch (ConfigurationException ce)
+ {
+ String name = (url == null) ? fileName : url.toString();
+ // Ignore the exception.
+ catalogManager.debug.message(DEBUG_ALL,
+ "Unable to get input stream for " + name + ". " + ce.getMessage());
+ }
+ if (is != null)
+ {
+ String mimeType = fileNameMap.getContentTypeFor(fileName);
+ try
+ {
+ if (mimeType != null)
+ {
+ parseCatalog(mimeType, is);
+ continue;
+ }
+ }
+ catch (Exception ex)
+ {
+ // Ignore the exception.
+ catalogManager.debug.message(DEBUG_ALL,
+ "Exception caught parsing input stream for " + fileName + ". "
+ + ex.getMessage());
+ }
+ finally
+ {
+ is.close();
+ }
+ }
+ parseCatalog(base, fileName);
+ }
+ }
+
+ }
+
+ /**
+ * Parse the specified catalog file.
+ * @param baseDir The base directory, if not included in the file name.
+ * @param fileName The catalog file. May be a full URI String.
+ * @throws IOException If an error occurs.
+ */
+ public void parseCatalog(String baseDir, String fileName) throws IOException
+ {
+ base = ConfigurationUtils.locate(fs, baseDir, fileName);
+ catalogCwd = base;
+ default_override = catalogManager.getPreferPublic();
+ catalogManager.debug.message(DEBUG_NORMAL, "Parse catalog: " + fileName);
+
+ boolean parsed = false;
+
+ for (int count = 0; !parsed && count < readerArr.size(); count++)
+ {
+ CatalogReader reader = (CatalogReader) readerArr.get(count);
+ InputStream inStream;
+
+ try
+ {
+ inStream = fs.getInputStream(base);
+ }
+ catch (Exception ex)
+ {
+ catalogManager.debug.message(DEBUG_NORMAL, "Unable to access " + base
+ + ex.getMessage());
+ break;
+ }
+
+ try
+ {
+ reader.readCatalog(this, inStream);
+ parsed = true;
+ }
+ catch (CatalogException ce)
+ {
+ catalogManager.debug.message(DEBUG_NORMAL, "Parse failed for " + fileName
+ + ce.getMessage());
+ if (ce.getExceptionType() == CatalogException.PARSE_FAILED)
+ {
+ break;
+ }
+ else
+ {
+ // try again!
+ continue;
+ }
+ }
+ finally
+ {
+ try
+ {
+ inStream.close();
+ }
+ catch (IOException ioe)
+ {
+ // Ignore the exception.
+ inStream = null;
+ }
+ }
+ }
+
+ if (parsed)
+ {
+ parsePendingCatalogs();
+ }
+ }
+
+ /**
+ * Perform character normalization on a URI reference.
+ *
+ * @param uriref The URI reference
+ * @return The normalized URI reference.
+ */
+ @Override
+ protected String normalizeURI(String uriref)
+ {
+ StrSubstitutor substitutor = ((CatalogManager) catalogManager).getStrSubstitutor();
+ String resolved = substitutor != null ? substitutor.replace(uriref) : uriref;
+ return super.normalizeURI(resolved);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/resolver/DefaultEntityResolver.java b/src/main/java/org/apache/commons/configuration/resolver/DefaultEntityResolver.java
new file mode 100644
index 0000000..46a192e
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/resolver/DefaultEntityResolver.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.resolver;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+/**
+ * The DefaultEntityResolver used by XML Configurations.
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @since 1.7
+ * @version $Id: DefaultEntityResolver.java 1301991 2012-03-17 20:18:02Z sebb $
+ */
+public class DefaultEntityResolver implements EntityResolver, EntityRegistry
+{
+ /** Stores a map with the registered public IDs.*/
+ private Map<String, URL> registeredEntities = new HashMap<String, URL>();
+
+ /**
+ * <p>
+ * Registers the specified URL for the specified public identifier.
+ * </p>
+ * <p>
+ * This implementation maps {@code PUBLICID}'s to URLs (from which
+ * the resource will be loaded). A common use case for this method is to
+ * register local URLs (possibly computed at runtime by a class loader) for
+ * DTDs and Schemas. This allows the performance advantage of using a local
+ * version without having to ensure every {@code SYSTEM} URI on every
+ * processed XML document is local. This implementation provides only basic
+ * functionality. If more sophisticated features are required, either calling
+ * {@code XMLConfiguration.setDocumentBuilder(DocumentBuilder)} to set a custom
+ * {@code DocumentBuilder} (which also can be initialized with a
+ * custom {@code EntityResolver}) or creating a custom entity resolver
+ * and registering it with the XMLConfiguration is recommended.
+ * </p>
+ *
+ * @param publicId Public identifier of the Entity to be resolved
+ * @param entityURL The URL to use for reading this Entity
+ * @throws IllegalArgumentException if the public ID is undefined
+ */
+ public void registerEntityId(String publicId, URL entityURL)
+ {
+ if (publicId == null)
+ {
+ throw new IllegalArgumentException("Public ID must not be null!");
+ }
+ getRegisteredEntities().put(publicId, entityURL);
+ }
+
+ /**
+ * Resolves the requested external entity. This is the default
+ * implementation of the {@code EntityResolver} interface. It checks
+ * the passed in public ID against the registered entity IDs and uses a
+ * local URL if possible.
+ *
+ * @param publicId the public identifier of the entity being referenced
+ * @param systemId the system identifier of the entity being referenced
+ * @return an input source for the specified entity
+ * @throws org.xml.sax.SAXException if a parsing exception occurs
+ */
+ public InputSource resolveEntity(String publicId, String systemId)
+ throws SAXException
+ {
+ // Has this system identifier been registered?
+ URL entityURL = null;
+ if (publicId != null)
+ {
+ entityURL = getRegisteredEntities().get(publicId);
+ }
+
+ if (entityURL != null)
+ {
+ // Obtain an InputSource for this URL. This code is based on the
+ // createInputSourceFromURL() method of Commons Digester.
+ try
+ {
+ URLConnection connection = entityURL.openConnection();
+ connection.setUseCaches(false);
+ InputStream stream = connection.getInputStream();
+ InputSource source = new InputSource(stream);
+ source.setSystemId(entityURL.toExternalForm());
+ return source;
+ }
+ catch (IOException e)
+ {
+ throw new SAXException(e);
+ }
+ }
+ else
+ {
+ // default processing behavior
+ return null;
+ }
+ }
+
+ /**
+ * Returns a map with the entity IDs that have been registered using the
+ * {@code registerEntityId()} method.
+ *
+ * @return a map with the registered entity IDs
+ */
+ public Map<String, URL> getRegisteredEntities()
+ {
+ return registeredEntities;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/resolver/EntityRegistry.java b/src/main/java/org/apache/commons/configuration/resolver/EntityRegistry.java
new file mode 100644
index 0000000..8dc752f
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/resolver/EntityRegistry.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.resolver;
+
+import java.net.URL;
+import java.util.Map;
+
+/**
+ * Interface used for registering and retrieving PUBLICID to URL mappings.
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @since 1.7
+ * @version $Id: EntityRegistry.java 1206577 2011-11-26 20:25:52Z oheger $
+ */
+public interface EntityRegistry
+{
+ /**
+ * <p>
+ * Registers the specified URL for the specified public identifier.
+ * </p>
+ * <p>
+ * This implementation maps {@code PUBLICID}'s to URLs (from which
+ * the resource will be loaded). A common use case for this method is to
+ * register local URLs (possibly computed at runtime by a class loader) for
+ * DTDs and Schemas. This allows the performance advantage of using a local
+ * version without having to ensure every {@code SYSTEM} URI on every
+ * processed XML document is local. This implementation provides only basic
+ * functionality. If more sophisticated features are required, either calling
+ * {@code XMLConfiguration.setDocumentBuilder(DocumentBuilder)} to set a custom
+ * {@code DocumentBuilder} (which also can be initialized with a
+ * custom {@code EntityResolver}) or creating a custom entity resolver
+ * and registering it with the XMLConfiguration is recommended.
+ * </p>
+ *
+ * @param publicId Public identifier of the Entity to be resolved
+ * @param entityURL The URL to use for reading this Entity
+ * @throws IllegalArgumentException if the public ID is undefined
+ */
+ void registerEntityId(String publicId, URL entityURL);
+
+ /**
+ * Returns a map with the entity IDs that have been registered using the
+ * {@code registerEntityId()} method.
+ *
+ * @return a map with the registered entity IDs
+ */
+ Map<String, URL> getRegisteredEntities();
+}
diff --git a/src/main/java/org/apache/commons/configuration/resolver/EntityResolverSupport.java b/src/main/java/org/apache/commons/configuration/resolver/EntityResolverSupport.java
new file mode 100644
index 0000000..ff0318a
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/resolver/EntityResolverSupport.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.resolver;
+
+import org.xml.sax.EntityResolver;
+
+/**
+ * Interface that identifies the class as using an EntityResolver
+ * @author <a href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @since 1.7
+ * @version $Id: EntityResolverSupport.java 1206761 2011-11-27 16:35:34Z oheger $
+ */
+public interface EntityResolverSupport
+{
+ /**
+ * Return the EntityResolver associated with the class.
+ * @return The EntityResolver.
+ */
+ EntityResolver getEntityResolver();
+
+ /**
+ * Set the EntityResolver to associate with this class.
+ * @param resolver The EntityResolver
+ */
+ void setEntityResolver(EntityResolver resolver);
+}
diff --git a/src/main/java/org/apache/commons/configuration/resolver/package.html b/src/main/java/org/apache/commons/configuration/resolver/package.html
new file mode 100644
index 0000000..833f3fc
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/resolver/package.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+<head>
+</head>
+<body>
+
+<p>
+A package containing EntityResolvers.
+</p>
+<p>
+<font size="-2">$Id: package.html 1162380 2011-08-27 15:49:26Z oheger $</font>
+</p>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/src/main/java/org/apache/commons/configuration/tree/ConfigurationNode.java b/src/main/java/org/apache/commons/configuration/tree/ConfigurationNode.java
new file mode 100644
index 0000000..c8a3d88
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/ConfigurationNode.java
@@ -0,0 +1,269 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import java.util.List;
+
+/**
+ * <p>
+ * Definition of an interface for the nodes of a hierarchical configuration.
+ * </p>
+ * <p>
+ * This interface defines a tree like structure for configuration data. A node
+ * has a value and can have an arbitrary number of children and attributes.
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationNode.java 1234988 2012-01-23 21:12:15Z oheger $
+ */
+public interface ConfigurationNode
+{
+ /**
+ * Returns the name of this node.
+ *
+ * @return the node name
+ */
+ String getName();
+
+ /**
+ * Sets the name of this node.
+ *
+ * @param name the node name
+ */
+ void setName(String name);
+
+ /**
+ * Returns the value of this node.
+ *
+ * @return the node's value
+ */
+ Object getValue();
+
+ /**
+ * Sets the value of this node.
+ *
+ * @param val the node's value
+ */
+ void setValue(Object val);
+
+ /**
+ * Returns this node's reference.
+ *
+ * @return the reference
+ */
+ Object getReference();
+
+ /**
+ * Sets this node's reference. This reference can be used by concrete
+ * Configuration implementations to store data associated with each node. A
+ * XML based configuration for instance could here store a reference to the
+ * corresponding DOM element.
+ *
+ * @param ref the reference
+ */
+ void setReference(Object ref);
+
+ /**
+ * Returns this node's parent. Can be <b>null</b>, then this node is the
+ * top level node.
+ *
+ * @return the parent of this node
+ */
+ ConfigurationNode getParentNode();
+
+ /**
+ * Sets the parent of this node.
+ *
+ * @param parent the parent of this node
+ */
+ void setParentNode(ConfigurationNode parent);
+
+ /**
+ * Adds a child to this node.
+ *
+ * @param node the new child
+ */
+ void addChild(ConfigurationNode node);
+
+ /**
+ * Returns a list with the child nodes of this node. The nodes in this list
+ * should be in the order they were inserted into this node.
+ *
+ * @return a list with the children of this node (never <b>null</b>)
+ */
+ List<ConfigurationNode> getChildren();
+
+ /**
+ * Returns the number of this node's children.
+ *
+ * @return the number of the children of this node
+ */
+ int getChildrenCount();
+
+ /**
+ * Returns a list with all children of this node with the given name.
+ *
+ * @param name the name of the searched children
+ * @return a list with all child nodes with this name (never <b>null</b>)
+ */
+ List<ConfigurationNode> getChildren(String name);
+
+ /**
+ * Returns the number of children with the given name.
+ *
+ * @param name the name
+ * @return the number of children with this name
+ */
+ int getChildrenCount(String name);
+
+ /**
+ * Returns the child node with the given index. If the index does not
+ * exist, an exception will be thrown.
+ * @param index the index of the child node (0-based)
+ * @return the child node with this index
+ */
+ ConfigurationNode getChild(int index);
+
+ /**
+ * Removes the given node from this node's children.
+ *
+ * @param child the child node to be removed
+ * @return a flag if the node could be removed
+ */
+ boolean removeChild(ConfigurationNode child);
+
+ /**
+ * Removes all child nodes of this node with the given name.
+ *
+ * @param childName the name of the children to be removed
+ * @return a flag if at least one child was removed
+ */
+ boolean removeChild(String childName);
+
+ /**
+ * Removes all children from this node.
+ */
+ void removeChildren();
+
+ /**
+ * Returns a flag whether this node is an attribute.
+ *
+ * @return a flag whether this node is an attribute
+ */
+ boolean isAttribute();
+
+ /**
+ * Sets a flag whether this node is an attribute.
+ *
+ * @param f the attribute flag
+ */
+ void setAttribute(boolean f);
+
+ /**
+ * Returns a list with this node's attributes. Attributes are also modeled
+ * as {@code ConfigurationNode} objects.
+ *
+ * @return a list with the attributes
+ */
+ List<ConfigurationNode> getAttributes();
+
+ /**
+ * Returns the number of attributes of this node.
+ * @return the number of attributes
+ */
+ int getAttributeCount();
+
+ /**
+ * Returns a list with the attribute nodes with the given name. Attributes
+ * with same names can be added multiple times, so the return value of this
+ * method is a list.
+ *
+ * @param name the name of the attribute
+ * @return the attribute nodes with this name (never <b>null</b>)
+ */
+ List<ConfigurationNode> getAttributes(String name);
+
+ /**
+ * Returns the number of attributes with the given name.
+ *
+ * @param name the name of the attribute
+ * @return the number of attributes with this name
+ */
+ int getAttributeCount(String name);
+
+ /**
+ * Returns the attribute node with the given index. If no such index exists,
+ * an exception will be thrown.
+ * @param index the index
+ * @return the attribute node with this index
+ */
+ ConfigurationNode getAttribute(int index);
+
+ /**
+ * Removes the specified attribute from this node.
+ *
+ * @param node the attribute to remove
+ * @return a flag if the node could be removed
+ */
+ boolean removeAttribute(ConfigurationNode node);
+
+ /**
+ * Removes all attributes with the given name.
+ *
+ * @param name the name of the attributes to be removed
+ * @return a flag if at least one attribute was removed
+ */
+ boolean removeAttribute(String name);
+
+ /**
+ * Removes all attributes of this node.
+ */
+ void removeAttributes();
+
+ /**
+ * Adds the specified attribute to this node
+ *
+ * @param attr the attribute node
+ */
+ void addAttribute(ConfigurationNode attr);
+
+ /**
+ * Returns a flag if this node is defined. This means that the node contains
+ * some data.
+ *
+ * @return a flag whether this node is defined
+ */
+ boolean isDefined();
+
+ /**
+ * Visits this node and all its sub nodes. This method provides a simple
+ * means for going through a hierarchical structure of configuration nodes.
+ *
+ * @see ConfigurationNodeVisitor
+ * @param visitor the visitor
+ */
+ void visit(ConfigurationNodeVisitor visitor);
+
+ /**
+ * Returns a copy of this node.
+ * @return the copy
+ */
+ Object clone();
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/ConfigurationNodeVisitor.java b/src/main/java/org/apache/commons/configuration/tree/ConfigurationNodeVisitor.java
new file mode 100644
index 0000000..554f422
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/ConfigurationNodeVisitor.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+/**
+ * <p>
+ * Definition of a <em>Visitor</em> interface for a configuration node
+ * structure.
+ * </p>
+ * <p>
+ * The {@code ConfigurationNode} interface defines a {@code visit()},
+ * which simplifies traversal of a complex node hierarchy. A configuration node
+ * implementation must provide a way of visiting all nodes in the current
+ * hierarchy. This is a typical application of the GoF <em>Visitor</em>
+ * pattern.
+ * </p>
+ *
+ * @since 1.3
+ * @see ConfigurationNode
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationNodeVisitor.java 1206463 2011-11-26 15:47:08Z oheger $
+ */
+public interface ConfigurationNodeVisitor
+{
+ /**
+ * Visits the specified node. This method is called before eventually
+ * existing children of this node are processed.
+ *
+ * @param node the node to be visited
+ */
+ void visitBeforeChildren(ConfigurationNode node);
+
+ /**
+ * Visits the specified node. This method is called after eventually
+ * existing children of this node have been processed.
+ *
+ * @param node the node to be visited
+ */
+ void visitAfterChildren(ConfigurationNode node);
+
+ /**
+ * Returns a flag whether the actual visit process should be aborted. This
+ * method allows a visitor implementation to state that it does not need any
+ * further data. It may be used e.g. by visitors that search for a certain
+ * node in the hierarchy. After that node was found, there is no need to
+ * process the remaining nodes, too.
+ *
+ * @return a flag if the visit process should be stopped
+ */
+ boolean terminate();
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/ConfigurationNodeVisitorAdapter.java b/src/main/java/org/apache/commons/configuration/tree/ConfigurationNodeVisitorAdapter.java
new file mode 100644
index 0000000..6e2d1f1
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/ConfigurationNodeVisitorAdapter.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+/**
+ * <p>
+ * A simple adapter class that simplifies writing custom node visitor
+ * implementations.
+ * </p>
+ * <p>
+ * This class provides dummy implementations for the methods defined in the
+ * {@code ConfigurationNodeVisitor} interface. Derived classes only need
+ * to override the methods they really need.
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationNodeVisitorAdapter.java 1206464 2011-11-26 15:49:10Z oheger $
+ */
+public class ConfigurationNodeVisitorAdapter implements
+ ConfigurationNodeVisitor
+{
+ /**
+ * Empty dummy implementation of this interface method.
+ *
+ * @param node the node
+ */
+ public void visitBeforeChildren(ConfigurationNode node)
+ {
+ }
+
+ /**
+ * Empty dummy implementation of this interface method.
+ *
+ * @param node the node
+ */
+ public void visitAfterChildren(ConfigurationNode node)
+ {
+ }
+
+ /**
+ * Dummy implementation of this interface method. Returns always <b>false</b>.
+ *
+ * @return the terminate flag
+ */
+ public boolean terminate()
+ {
+ return false;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/DefaultConfigurationKey.java b/src/main/java/org/apache/commons/configuration/tree/DefaultConfigurationKey.java
new file mode 100644
index 0000000..23575ed
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/DefaultConfigurationKey.java
@@ -0,0 +1,876 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * <p>
+ * A simple class that supports creation of and iteration on configuration keys
+ * supported by a {@link DefaultExpressionEngine} object.
+ * </p>
+ * <p>
+ * For key creation the class works similar to a StringBuffer: There are several
+ * {@code appendXXXX()} methods with which single parts of a key can be
+ * constructed. All these methods return a reference to the actual object so
+ * they can be written in a chain. When using this methods the exact syntax for
+ * keys need not be known.
+ * </p>
+ * <p>
+ * This class also defines a specialized iterator for configuration keys. With
+ * such an iterator a key can be tokenized into its single parts. For each part
+ * it can be checked whether it has an associated index.
+ * </p>
+ * <p>
+ * Instances of this class are always associated with an instance of
+ * {@link DefaultExpressionEngine}, from which the current
+ * delimiters are obtained. So key creation and parsing is specific to this
+ * associated expression engine.
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: DefaultConfigurationKey.java 1231724 2012-01-15 18:40:31Z oheger $
+ */
+public class DefaultConfigurationKey
+{
+ /** Constant for the initial StringBuffer size. */
+ private static final int INITIAL_SIZE = 32;
+
+ /** Stores a reference to the associated expression engine. */
+ private DefaultExpressionEngine expressionEngine;
+
+ /** Holds a buffer with the so far created key. */
+ private StringBuilder keyBuffer;
+
+ /**
+ * Creates a new instance of {@code DefaultConfigurationKey} and sets
+ * the associated expression engine.
+ *
+ * @param engine the expression engine
+ */
+ public DefaultConfigurationKey(DefaultExpressionEngine engine)
+ {
+ keyBuffer = new StringBuilder(INITIAL_SIZE);
+ setExpressionEngine(engine);
+ }
+
+ /**
+ * Creates a new instance of {@code DefaultConfigurationKey} and sets
+ * the associated expression engine and an initial key.
+ *
+ * @param engine the expression engine
+ * @param key the key to be wrapped
+ */
+ public DefaultConfigurationKey(DefaultExpressionEngine engine, String key)
+ {
+ setExpressionEngine(engine);
+ keyBuffer = new StringBuilder(trim(key));
+ }
+
+ /**
+ * Returns the associated default expression engine.
+ *
+ * @return the associated expression engine
+ */
+ public DefaultExpressionEngine getExpressionEngine()
+ {
+ return expressionEngine;
+ }
+
+ /**
+ * Sets the associated expression engine.
+ *
+ * @param expressionEngine the expression engine (must not be <b>null</b>)
+ */
+ public void setExpressionEngine(DefaultExpressionEngine expressionEngine)
+ {
+ if (expressionEngine == null)
+ {
+ throw new IllegalArgumentException(
+ "Expression engine must not be null!");
+ }
+ this.expressionEngine = expressionEngine;
+ }
+
+ /**
+ * Appends the name of a property to this key. If necessary, a property
+ * delimiter will be added. If the boolean argument is set to <b>true</b>,
+ * property delimiters contained in the property name will be escaped.
+ *
+ * @param property the name of the property to be added
+ * @param escape a flag if property delimiters in the passed in property name
+ * should be escaped
+ * @return a reference to this object
+ */
+ public DefaultConfigurationKey append(String property, boolean escape)
+ {
+ String key;
+ if (escape && property != null)
+ {
+ key = escapeDelimiters(property);
+ }
+ else
+ {
+ key = property;
+ }
+ key = trim(key);
+
+ if (keyBuffer.length() > 0 && !isAttributeKey(property)
+ && key.length() > 0)
+ {
+ keyBuffer.append(getExpressionEngine().getPropertyDelimiter());
+ }
+
+ keyBuffer.append(key);
+ return this;
+ }
+
+ /**
+ * Appends the name of a property to this key. If necessary, a property
+ * delimiter will be added. Property delimiters in the given string will not
+ * be escaped.
+ *
+ * @param property the name of the property to be added
+ * @return a reference to this object
+ */
+ public DefaultConfigurationKey append(String property)
+ {
+ return append(property, false);
+ }
+
+ /**
+ * Appends an index to this configuration key.
+ *
+ * @param index the index to be appended
+ * @return a reference to this object
+ */
+ public DefaultConfigurationKey appendIndex(int index)
+ {
+ keyBuffer.append(getExpressionEngine().getIndexStart());
+ keyBuffer.append(index);
+ keyBuffer.append(getExpressionEngine().getIndexEnd());
+ return this;
+ }
+
+ /**
+ * Appends an attribute to this configuration key.
+ *
+ * @param attr the name of the attribute to be appended
+ * @return a reference to this object
+ */
+ public DefaultConfigurationKey appendAttribute(String attr)
+ {
+ keyBuffer.append(constructAttributeKey(attr));
+ return this;
+ }
+
+ /**
+ * Returns the actual length of this configuration key.
+ *
+ * @return the length of this key
+ */
+ public int length()
+ {
+ return keyBuffer.length();
+ }
+
+ /**
+ * Sets the new length of this configuration key. With this method it is
+ * possible to truncate the key, e.g. to return to a state prior calling
+ * some {@code append()} methods. The semantic is the same as the
+ * {@code setLength()} method of {@code StringBuilder}.
+ *
+ * @param len the new length of the key
+ */
+ public void setLength(int len)
+ {
+ keyBuffer.setLength(len);
+ }
+
+ /**
+ * Checks if two {@code ConfigurationKey} objects are equal. The
+ * method can be called with strings or other objects, too.
+ *
+ * @param c the object to compare
+ * @return a flag if both objects are equal
+ */
+ @Override
+ public boolean equals(Object c)
+ {
+ if (c == null)
+ {
+ return false;
+ }
+
+ return keyBuffer.toString().equals(c.toString());
+ }
+
+ /**
+ * Returns the hash code for this object.
+ *
+ * @return the hash code
+ */
+ @Override
+ public int hashCode()
+ {
+ return String.valueOf(keyBuffer).hashCode();
+ }
+
+ /**
+ * Returns a string representation of this object. This is the configuration
+ * key as a plain string.
+ *
+ * @return a string for this object
+ */
+ @Override
+ public String toString()
+ {
+ return keyBuffer.toString();
+ }
+
+ /**
+ * Tests if the specified key represents an attribute according to the
+ * current expression engine.
+ *
+ * @param key the key to be checked
+ * @return <b>true</b> if this is an attribute key, <b>false</b> otherwise
+ */
+ public boolean isAttributeKey(String key)
+ {
+ if (key == null)
+ {
+ return false;
+ }
+
+ return key.startsWith(getExpressionEngine().getAttributeStart())
+ && (getExpressionEngine().getAttributeEnd() == null || key
+ .endsWith(getExpressionEngine().getAttributeEnd()));
+ }
+
+ /**
+ * Decorates the given key so that it represents an attribute. Adds special
+ * start and end markers. The passed in string will be modified only if does
+ * not already represent an attribute.
+ *
+ * @param key the key to be decorated
+ * @return the decorated attribute key
+ */
+ public String constructAttributeKey(String key)
+ {
+ if (key == null)
+ {
+ return StringUtils.EMPTY;
+ }
+ if (isAttributeKey(key))
+ {
+ return key;
+ }
+ else
+ {
+ StringBuilder buf = new StringBuilder();
+ buf.append(getExpressionEngine().getAttributeStart()).append(key);
+ if (getExpressionEngine().getAttributeEnd() != null)
+ {
+ buf.append(getExpressionEngine().getAttributeEnd());
+ }
+ return buf.toString();
+ }
+ }
+
+ /**
+ * Extracts the name of the attribute from the given attribute key. This
+ * method removes the attribute markers - if any - from the specified key.
+ *
+ * @param key the attribute key
+ * @return the name of the corresponding attribute
+ */
+ public String attributeName(String key)
+ {
+ return isAttributeKey(key) ? removeAttributeMarkers(key) : key;
+ }
+
+ /**
+ * Removes leading property delimiters from the specified key.
+ *
+ * @param key the key
+ * @return the key with removed leading property delimiters
+ */
+ public String trimLeft(String key)
+ {
+ if (key == null)
+ {
+ return StringUtils.EMPTY;
+ }
+ else
+ {
+ String result = key;
+ while (hasLeadingDelimiter(result))
+ {
+ result = result.substring(getExpressionEngine()
+ .getPropertyDelimiter().length());
+ }
+ return result;
+ }
+ }
+
+ /**
+ * Removes trailing property delimiters from the specified key.
+ *
+ * @param key the key
+ * @return the key with removed trailing property delimiters
+ */
+ public String trimRight(String key)
+ {
+ if (key == null)
+ {
+ return StringUtils.EMPTY;
+ }
+ else
+ {
+ String result = key;
+ while (hasTrailingDelimiter(result))
+ {
+ result = result
+ .substring(0, result.length()
+ - getExpressionEngine().getPropertyDelimiter()
+ .length());
+ }
+ return result;
+ }
+ }
+
+ /**
+ * Removes delimiters at the beginning and the end of the specified key.
+ *
+ * @param key the key
+ * @return the key with removed property delimiters
+ */
+ public String trim(String key)
+ {
+ return trimRight(trimLeft(key));
+ }
+
+ /**
+ * Returns an iterator for iterating over the single components of this
+ * configuration key.
+ *
+ * @return an iterator for this key
+ */
+ public KeyIterator iterator()
+ {
+ return new KeyIterator();
+ }
+
+ /**
+ * Helper method that checks if the specified key ends with a property
+ * delimiter.
+ *
+ * @param key the key to check
+ * @return a flag if there is a trailing delimiter
+ */
+ private boolean hasTrailingDelimiter(String key)
+ {
+ return key.endsWith(getExpressionEngine().getPropertyDelimiter())
+ && (getExpressionEngine().getEscapedDelimiter() == null || !key
+ .endsWith(getExpressionEngine().getEscapedDelimiter()));
+ }
+
+ /**
+ * Helper method that checks if the specified key starts with a property
+ * delimiter.
+ *
+ * @param key the key to check
+ * @return a flag if there is a leading delimiter
+ */
+ private boolean hasLeadingDelimiter(String key)
+ {
+ return key.startsWith(getExpressionEngine().getPropertyDelimiter())
+ && (getExpressionEngine().getEscapedDelimiter() == null || !key
+ .startsWith(getExpressionEngine().getEscapedDelimiter()));
+ }
+
+ /**
+ * Helper method for removing attribute markers from a key.
+ *
+ * @param key the key
+ * @return the key with removed attribute markers
+ */
+ private String removeAttributeMarkers(String key)
+ {
+ return key
+ .substring(
+ getExpressionEngine().getAttributeStart().length(),
+ key.length()
+ - ((getExpressionEngine().getAttributeEnd() != null) ? getExpressionEngine()
+ .getAttributeEnd().length()
+ : 0));
+ }
+
+ /**
+ * Unescapes the delimiters in the specified string.
+ *
+ * @param key the key to be unescaped
+ * @return the unescaped key
+ */
+ private String unescapeDelimiters(String key)
+ {
+ return (getExpressionEngine().getEscapedDelimiter() == null) ? key
+ : StringUtils.replace(key, getExpressionEngine()
+ .getEscapedDelimiter(), getExpressionEngine()
+ .getPropertyDelimiter());
+ }
+
+ /**
+ * Escapes the delimiters in the specified string.
+ *
+ * @param key the key to be escaped
+ * @return the escaped key
+ */
+ private String escapeDelimiters(String key)
+ {
+ return (getExpressionEngine().getEscapedDelimiter() == null || key
+ .indexOf(getExpressionEngine().getPropertyDelimiter()) < 0) ? key
+ : StringUtils.replace(key, getExpressionEngine()
+ .getPropertyDelimiter(), getExpressionEngine()
+ .getEscapedDelimiter());
+ }
+
+ /**
+ * A specialized iterator class for tokenizing a configuration key. This
+ * class implements the normal iterator interface. In addition it provides
+ * some specific methods for configuration keys.
+ */
+ public class KeyIterator implements Iterator<Object>, Cloneable
+ {
+ /** Stores the current key name. */
+ private String current;
+
+ /** Stores the start index of the actual token. */
+ private int startIndex;
+
+ /** Stores the end index of the actual token. */
+ private int endIndex;
+
+ /** Stores the index of the actual property if there is one. */
+ private int indexValue;
+
+ /** Stores a flag if the actual property has an index. */
+ private boolean hasIndex;
+
+ /** Stores a flag if the actual property is an attribute. */
+ private boolean attribute;
+
+ /**
+ * Returns the next key part of this configuration key. This is a short
+ * form of {@code nextKey(false)}.
+ *
+ * @return the next key part
+ */
+ public String nextKey()
+ {
+ return nextKey(false);
+ }
+
+ /**
+ * Returns the next key part of this configuration key. The boolean
+ * parameter indicates wheter a decorated key should be returned. This
+ * affects only attribute keys: if the parameter is <b>false</b>, the
+ * attribute markers are stripped from the key; if it is <b>true</b>,
+ * they remain.
+ *
+ * @param decorated a flag if the decorated key is to be returned
+ * @return the next key part
+ */
+ public String nextKey(boolean decorated)
+ {
+ if (!hasNext())
+ {
+ throw new NoSuchElementException("No more key parts!");
+ }
+
+ hasIndex = false;
+ indexValue = -1;
+ String key = findNextIndices();
+
+ current = key;
+ hasIndex = checkIndex(key);
+ attribute = checkAttribute(current);
+
+ return currentKey(decorated);
+ }
+
+ /**
+ * Checks if there is a next element.
+ *
+ * @return a flag if there is a next element
+ */
+ public boolean hasNext()
+ {
+ return endIndex < keyBuffer.length();
+ }
+
+ /**
+ * Returns the next object in the iteration.
+ *
+ * @return the next object
+ */
+ public Object next()
+ {
+ return nextKey();
+ }
+
+ /**
+ * Removes the current object in the iteration. This method is not
+ * supported by this iterator type, so an exception is thrown.
+ */
+ public void remove()
+ {
+ throw new UnsupportedOperationException("Remove not supported!");
+ }
+
+ /**
+ * Returns the current key of the iteration (without skipping to the
+ * next element). This is the same key the previous {@code next()}
+ * call had returned. (Short form of {@code currentKey(false)}.
+ *
+ * @return the current key
+ */
+ public String currentKey()
+ {
+ return currentKey(false);
+ }
+
+ /**
+ * Returns the current key of the iteration (without skipping to the
+ * next element). The boolean parameter indicates wheter a decorated key
+ * should be returned. This affects only attribute keys: if the
+ * parameter is <b>false</b>, the attribute markers are stripped from
+ * the key; if it is <b>true</b>, they remain.
+ *
+ * @param decorated a flag if the decorated key is to be returned
+ * @return the current key
+ */
+ public String currentKey(boolean decorated)
+ {
+ return (decorated && !isPropertyKey()) ? constructAttributeKey(current)
+ : current;
+ }
+
+ /**
+ * Returns a flag if the current key is an attribute. This method can be
+ * called after {@code next()}.
+ *
+ * @return a flag if the current key is an attribute
+ */
+ public boolean isAttribute()
+ {
+ // if attribute emulation mode is active, the last part of a key is
+ // always an attribute key, too
+ return attribute || (isAttributeEmulatingMode() && !hasNext());
+ }
+
+ /**
+ * Returns a flag whether the current key refers to a property (i.e. is
+ * no special attribute key). Usually this method will return the
+ * opposite of {@code isAttribute()}, but if the delimiters for
+ * normal properties and attributes are set to the same string, it is
+ * possible that both methods return <b>true</b>.
+ *
+ * @return a flag if the current key is a property key
+ * @see #isAttribute()
+ */
+ public boolean isPropertyKey()
+ {
+ return !attribute;
+ }
+
+ /**
+ * Returns the index value of the current key. If the current key does
+ * not have an index, return value is -1. This method can be called
+ * after {@code next()}.
+ *
+ * @return the index value of the current key
+ */
+ public int getIndex()
+ {
+ return indexValue;
+ }
+
+ /**
+ * Returns a flag if the current key has an associated index. This
+ * method can be called after {@code next()}.
+ *
+ * @return a flag if the current key has an index
+ */
+ public boolean hasIndex()
+ {
+ return hasIndex;
+ }
+
+ /**
+ * Creates a clone of this object.
+ *
+ * @return a clone of this object
+ */
+ @Override
+ public Object clone()
+ {
+ try
+ {
+ return super.clone();
+ }
+ catch (CloneNotSupportedException cex)
+ {
+ // should not happen
+ return null;
+ }
+ }
+
+ /**
+ * Helper method for determining the next indices.
+ *
+ * @return the next key part
+ */
+ private String findNextIndices()
+ {
+ startIndex = endIndex;
+ // skip empty names
+ while (startIndex < length()
+ && hasLeadingDelimiter(keyBuffer.substring(startIndex)))
+ {
+ startIndex += getExpressionEngine().getPropertyDelimiter()
+ .length();
+ }
+
+ // Key ends with a delimiter?
+ if (startIndex >= length())
+ {
+ endIndex = length();
+ startIndex = endIndex - 1;
+ return keyBuffer.substring(startIndex, endIndex);
+ }
+ else
+ {
+ return nextKeyPart();
+ }
+ }
+
+ /**
+ * Helper method for extracting the next key part. Takes escaping of
+ * delimiter characters into account.
+ *
+ * @return the next key part
+ */
+ private String nextKeyPart()
+ {
+ int attrIdx = keyBuffer.toString().indexOf(
+ getExpressionEngine().getAttributeStart(), startIndex);
+ if (attrIdx < 0 || attrIdx == startIndex)
+ {
+ attrIdx = length();
+ }
+
+ int delIdx = nextDelimiterPos(keyBuffer.toString(), startIndex,
+ attrIdx);
+ if (delIdx < 0)
+ {
+ delIdx = attrIdx;
+ }
+
+ endIndex = Math.min(attrIdx, delIdx);
+ return unescapeDelimiters(keyBuffer.substring(startIndex, endIndex));
+ }
+
+ /**
+ * Searches the next unescaped delimiter from the given position.
+ *
+ * @param key the key
+ * @param pos the start position
+ * @param endPos the end position
+ * @return the position of the next delimiter or -1 if there is none
+ */
+ private int nextDelimiterPos(String key, int pos, int endPos)
+ {
+ int delimiterPos = pos;
+ boolean found = false;
+
+ do
+ {
+ delimiterPos = key.indexOf(getExpressionEngine()
+ .getPropertyDelimiter(), delimiterPos);
+ if (delimiterPos < 0 || delimiterPos >= endPos)
+ {
+ return -1;
+ }
+ int escapePos = escapedPosition(key, delimiterPos);
+ if (escapePos < 0)
+ {
+ found = true;
+ }
+ else
+ {
+ delimiterPos = escapePos;
+ }
+ }
+ while (!found);
+
+ return delimiterPos;
+ }
+
+ /**
+ * Checks if a delimiter at the specified position is escaped. If this
+ * is the case, the next valid search position will be returned.
+ * Otherwise the return value is -1.
+ *
+ * @param key the key to check
+ * @param pos the position where a delimiter was found
+ * @return information about escaped delimiters
+ */
+ private int escapedPosition(String key, int pos)
+ {
+ if (getExpressionEngine().getEscapedDelimiter() == null)
+ {
+ // nothing to escape
+ return -1;
+ }
+ int escapeOffset = escapeOffset();
+ if (escapeOffset < 0 || escapeOffset > pos)
+ {
+ // No escaping possible at this position
+ return -1;
+ }
+
+ int escapePos = key.indexOf(getExpressionEngine()
+ .getEscapedDelimiter(), pos - escapeOffset);
+ if (escapePos <= pos && escapePos >= 0)
+ {
+ // The found delimiter is escaped. Next valid search position
+ // is behind the escaped delimiter.
+ return escapePos
+ + getExpressionEngine().getEscapedDelimiter().length();
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ /**
+ * Determines the relative offset of an escaped delimiter in relation to
+ * a delimiter. Depending on the used delimiter and escaped delimiter
+ * tokens the position where to search for an escaped delimiter is
+ * different. If, for instance, the dot character (".") is
+ * used as delimiter, and a doubled dot ("..") as escaped
+ * delimiter, the escaped delimiter starts at the same position as the
+ * delimiter. If the token "\." was used, it would start one
+ * character before the delimiter because the delimiter character
+ * "." is the second character in the escaped delimiter
+ * string. This relation will be determined by this method. For this to
+ * work the delimiter string must be contained in the escaped delimiter
+ * string.
+ *
+ * @return the relative offset of the escaped delimiter in relation to a
+ * delimiter
+ */
+ private int escapeOffset()
+ {
+ return getExpressionEngine().getEscapedDelimiter().indexOf(
+ getExpressionEngine().getPropertyDelimiter());
+ }
+
+ /**
+ * Helper method for checking if the passed key is an attribute. If this
+ * is the case, the internal fields will be set.
+ *
+ * @param key the key to be checked
+ * @return a flag if the key is an attribute
+ */
+ private boolean checkAttribute(String key)
+ {
+ if (isAttributeKey(key))
+ {
+ current = removeAttributeMarkers(key);
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ /**
+ * Helper method for checking if the passed key contains an index. If
+ * this is the case, internal fields will be set.
+ *
+ * @param key the key to be checked
+ * @return a flag if an index is defined
+ */
+ private boolean checkIndex(String key)
+ {
+ boolean result = false;
+
+ try
+ {
+ int idx = key.lastIndexOf(getExpressionEngine().getIndexStart());
+ if (idx > 0)
+ {
+ int endidx = key.indexOf(getExpressionEngine().getIndexEnd(),
+ idx);
+
+ if (endidx > idx + 1)
+ {
+ indexValue = Integer.parseInt(key.substring(idx + 1, endidx));
+ current = key.substring(0, idx);
+ result = true;
+ }
+ }
+ }
+ catch (NumberFormatException nfe)
+ {
+ result = false;
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns a flag whether attributes are marked the same way as normal
+ * property keys. We call this the "attribute emulating mode".
+ * When navigating through node hierarchies it might be convenient to
+ * treat attributes the same way than other child nodes, so an
+ * expression engine supports to set the attribute markers to the same
+ * value than the property delimiter. If this is the case, some special
+ * checks have to be performed.
+ *
+ * @return a flag if attributes and normal property keys are treated the
+ * same way
+ */
+ private boolean isAttributeEmulatingMode()
+ {
+ return getExpressionEngine().getAttributeEnd() == null
+ && StringUtils.equals(getExpressionEngine()
+ .getPropertyDelimiter(), getExpressionEngine()
+ .getAttributeStart());
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/DefaultConfigurationNode.java b/src/main/java/org/apache/commons/configuration/tree/DefaultConfigurationNode.java
new file mode 100644
index 0000000..cbd0b24
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/DefaultConfigurationNode.java
@@ -0,0 +1,717 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.configuration.ConfigurationRuntimeException;
+
+/**
+ * <p>
+ * A default implementation of the {@code ConfigurationNode} interface.
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: DefaultConfigurationNode.java 1301991 2012-03-17 20:18:02Z sebb $
+ */
+public class DefaultConfigurationNode implements ConfigurationNode, Cloneable
+{
+ /** Stores the children of this node. */
+ private SubNodes children;
+
+ /** Stores the attributes of this node. */
+ private SubNodes attributes;
+
+ /** Stores a reference to this node's parent. */
+ private ConfigurationNode parent;
+
+ /** Stores the value of this node. */
+ private Object value;
+
+ /** Stores the reference. */
+ private Object reference;
+
+ /** Stores the name of this node. */
+ private String name;
+
+ /** Stores a flag if this is an attribute. */
+ private boolean attribute;
+
+ /**
+ * Creates a new uninitialized instance of {@code DefaultConfigurationNode}.
+ */
+ public DefaultConfigurationNode()
+ {
+ this(null);
+ }
+
+ /**
+ * Creates a new instance of {@code DefaultConfigurationNode} and
+ * initializes it with the node name.
+ *
+ * @param name the name of this node
+ */
+ public DefaultConfigurationNode(String name)
+ {
+ this(name, null);
+ }
+
+ /**
+ * Creates a new instance of {@code DefaultConfigurationNode} and
+ * initializes it with the name and a value.
+ *
+ * @param name the node's name
+ * @param value the node's value
+ */
+ public DefaultConfigurationNode(String name, Object value)
+ {
+ setName(name);
+ setValue(value);
+ initSubNodes();
+ }
+
+ /**
+ * Returns the name of this node.
+ *
+ * @return the name of this node
+ */
+ public String getName()
+ {
+ return name;
+ }
+
+ /**
+ * Sets the name of this node.
+ *
+ * @param name the new name
+ */
+ public void setName(String name)
+ {
+ checkState();
+ this.name = name;
+ }
+
+ /**
+ * Returns the value of this node.
+ *
+ * @return the value of this node
+ */
+ public Object getValue()
+ {
+ return value;
+ }
+
+ /**
+ * Sets the value of this node.
+ *
+ * @param val the value of this node
+ */
+ public void setValue(Object val)
+ {
+ value = val;
+ }
+
+ /**
+ * Returns the reference.
+ *
+ * @return the reference
+ */
+ public Object getReference()
+ {
+ return reference;
+ }
+
+ /**
+ * Sets the reference.
+ *
+ * @param reference the reference object
+ */
+ public void setReference(Object reference)
+ {
+ this.reference = reference;
+ }
+
+ /**
+ * Returns a reference to this node's parent.
+ *
+ * @return the parent node or <b>null </b> if this is the root
+ */
+ public ConfigurationNode getParentNode()
+ {
+ return parent;
+ }
+
+ /**
+ * Sets the parent of this node.
+ *
+ * @param parent the parent of this node
+ */
+ public void setParentNode(ConfigurationNode parent)
+ {
+ this.parent = parent;
+ }
+
+ /**
+ * Adds a new child to this node.
+ *
+ * @param child the new child
+ */
+ public void addChild(ConfigurationNode child)
+ {
+ children.addNode(child);
+ child.setAttribute(false);
+ child.setParentNode(this);
+ }
+
+ /**
+ * Returns a list with all children of this node.
+ *
+ * @return a list with all child nodes
+ */
+ public List<ConfigurationNode> getChildren()
+ {
+ return children.getSubNodes();
+ }
+
+ /**
+ * Returns the number of all children of this node.
+ *
+ * @return the number of all children
+ */
+ public int getChildrenCount()
+ {
+ return children.getSubNodes().size();
+ }
+
+ /**
+ * Returns a list of all children with the given name.
+ *
+ * @param name the name; can be <b>null </b>, then all children are returned
+ * @return a list of all children with the given name
+ */
+ public List<ConfigurationNode> getChildren(String name)
+ {
+ return children.getSubNodes(name);
+ }
+
+ /**
+ * Returns the number of children with the given name.
+ *
+ * @param name the name; can be <b>null </b>, then the number of all
+ * children is returned
+ * @return the number of child nodes with this name
+ */
+ public int getChildrenCount(String name)
+ {
+ return children.getSubNodes(name).size();
+ }
+
+ /**
+ * Returns the child node with the given index.
+ *
+ * @param index the index (0-based)
+ * @return the child with this index
+ */
+ public ConfigurationNode getChild(int index)
+ {
+ return children.getNode(index);
+ }
+
+ /**
+ * Removes the specified child node from this node.
+ *
+ * @param child the node to be removed
+ * @return a flag if a node was removed
+ */
+ public boolean removeChild(ConfigurationNode child)
+ {
+ return children.removeNode(child);
+ }
+
+ /**
+ * Removes all children with the given name.
+ *
+ * @param childName the name of the children to be removed
+ * @return a flag if at least one child node was removed
+ */
+ public boolean removeChild(String childName)
+ {
+ return children.removeNodes(childName);
+ }
+
+ /**
+ * Removes all child nodes of this node.
+ */
+ public void removeChildren()
+ {
+ children.clear();
+ }
+
+ /**
+ * Checks if this node is an attribute node.
+ *
+ * @return a flag if this is an attribute node
+ */
+ public boolean isAttribute()
+ {
+ return attribute;
+ }
+
+ /**
+ * Sets the attribute flag. Note: this method can only be called if the node
+ * is not already part of a node hierarchy.
+ *
+ * @param f the attribute flag
+ */
+ public void setAttribute(boolean f)
+ {
+ checkState();
+ attribute = f;
+ }
+
+ /**
+ * Adds the specified attribute to this node.
+ *
+ * @param attr the attribute to be added
+ */
+ public void addAttribute(ConfigurationNode attr)
+ {
+ attributes.addNode(attr);
+ attr.setAttribute(true);
+ attr.setParentNode(this);
+ }
+
+ /**
+ * Returns a list with the attributes of this node. This list contains
+ * {@code DefaultConfigurationNode} objects, too.
+ *
+ * @return the attribute list, never <b>null </b>
+ */
+ public List<ConfigurationNode> getAttributes()
+ {
+ return attributes.getSubNodes();
+ }
+
+ /**
+ * Returns the number of attributes contained in this node.
+ *
+ * @return the number of attributes
+ */
+ public int getAttributeCount()
+ {
+ return attributes.getSubNodes().size();
+ }
+
+ /**
+ * Returns a list with all attributes of this node with the given name.
+ *
+ * @param name the attribute's name
+ * @return all attributes with this name
+ */
+ public List<ConfigurationNode> getAttributes(String name)
+ {
+ return attributes.getSubNodes(name);
+ }
+
+ /**
+ * Returns the number of attributes of this node with the given name.
+ *
+ * @param name the name
+ * @return the number of attributes with this name
+ */
+ public int getAttributeCount(String name)
+ {
+ return getAttributes(name).size();
+ }
+
+ /**
+ * Removes the specified attribute.
+ *
+ * @param node the attribute node to be removed
+ * @return a flag if the attribute could be removed
+ */
+ public boolean removeAttribute(ConfigurationNode node)
+ {
+ return attributes.removeNode(node);
+ }
+
+ /**
+ * Removes all attributes with the specified name.
+ *
+ * @param name the name
+ * @return a flag if at least one attribute was removed
+ */
+ public boolean removeAttribute(String name)
+ {
+ return attributes.removeNodes(name);
+ }
+
+ /**
+ * Returns the attribute with the given index.
+ *
+ * @param index the index (0-based)
+ * @return the attribute with this index
+ */
+ public ConfigurationNode getAttribute(int index)
+ {
+ return attributes.getNode(index);
+ }
+
+ /**
+ * Removes all attributes of this node.
+ */
+ public void removeAttributes()
+ {
+ attributes.clear();
+ }
+
+ /**
+ * Returns a flag if this node is defined. This means that the node contains
+ * some data.
+ *
+ * @return a flag whether this node is defined
+ */
+ public boolean isDefined()
+ {
+ return getValue() != null || getChildrenCount() > 0
+ || getAttributeCount() > 0;
+ }
+
+ /**
+ * Visits this node and all its sub nodes.
+ *
+ * @param visitor the visitor
+ */
+ public void visit(ConfigurationNodeVisitor visitor)
+ {
+ if (visitor == null)
+ {
+ throw new IllegalArgumentException("Visitor must not be null!");
+ }
+
+ if (!visitor.terminate())
+ {
+ visitor.visitBeforeChildren(this);
+ children.visit(visitor);
+ attributes.visit(visitor);
+ visitor.visitAfterChildren(this);
+ }
+ }
+
+ /**
+ * Creates a copy of this object. This is not a deep copy, the children are
+ * not cloned.
+ *
+ * @return a copy of this object
+ */
+ @Override
+ public Object clone()
+ {
+ try
+ {
+ DefaultConfigurationNode copy = (DefaultConfigurationNode) super
+ .clone();
+ copy.initSubNodes();
+ return copy;
+ }
+ catch (CloneNotSupportedException cex)
+ {
+ // should not happen
+ throw new ConfigurationRuntimeException("Cannot clone " + getClass());
+ }
+ }
+
+ /**
+ * Checks if a modification of this node is allowed. Some properties of a
+ * node must not be changed when the node has a parent. This method checks
+ * this and throws a runtime exception if necessary.
+ */
+ protected void checkState()
+ {
+ if (getParentNode() != null)
+ {
+ throw new IllegalStateException(
+ "Node cannot be modified when added to a parent!");
+ }
+ }
+
+ /**
+ * Creates a {@code SubNodes} instance that is used for storing
+ * either this node's children or attributes.
+ *
+ * @param attributes <b>true</b> if the returned instance is used for
+ * storing attributes, <b>false</b> for storing child nodes
+ * @return the {@code SubNodes} object to use
+ */
+ protected SubNodes createSubNodes(boolean attributes)
+ {
+ return new SubNodes();
+ }
+
+ /**
+ * Deals with the reference when a node is removed. This method is called
+ * for each removed child node or attribute. It can be overloaded in sub
+ * classes, for which the reference has a concrete meaning and remove
+ * operations need some update actions. This default implementation is
+ * empty.
+ */
+ protected void removeReference()
+ {
+ }
+
+ /**
+ * Helper method for initializing the sub nodes objects.
+ */
+ private void initSubNodes()
+ {
+ children = createSubNodes(false);
+ attributes = createSubNodes(true);
+ }
+
+ /**
+ * An internally used helper class for managing a collection of sub nodes.
+ */
+ protected static class SubNodes
+ {
+ /** Stores a list for the sub nodes. */
+ private List<ConfigurationNode> nodes;
+
+ /** Stores a map for accessing subnodes by name. */
+ private Map<String, List<ConfigurationNode>> namedNodes;
+
+ /**
+ * Adds a new sub node.
+ *
+ * @param node the node to add
+ */
+ public void addNode(ConfigurationNode node)
+ {
+ if (node == null || node.getName() == null)
+ {
+ throw new IllegalArgumentException(
+ "Node to add must have a defined name!");
+ }
+ node.setParentNode(null); // reset, will later be set
+
+ if (nodes == null)
+ {
+ nodes = new ArrayList<ConfigurationNode>();
+ namedNodes = new HashMap<String, List<ConfigurationNode>>();
+ }
+
+ nodes.add(node);
+ List<ConfigurationNode> lst = namedNodes.get(node.getName());
+ if (lst == null)
+ {
+ lst = new LinkedList<ConfigurationNode>();
+ namedNodes.put(node.getName(), lst);
+ }
+ lst.add(node);
+ }
+
+ /**
+ * Removes a sub node.
+ *
+ * @param node the node to remove
+ * @return a flag if the node could be removed
+ */
+ public boolean removeNode(ConfigurationNode node)
+ {
+ if (nodes != null && node != null && nodes.contains(node))
+ {
+ detachNode(node);
+ nodes.remove(node);
+
+ List<ConfigurationNode> lst = namedNodes.get(node.getName());
+ if (lst != null)
+ {
+ lst.remove(node);
+ if (lst.isEmpty())
+ {
+ namedNodes.remove(node.getName());
+ }
+ }
+ return true;
+ }
+
+ else
+ {
+ return false;
+ }
+ }
+
+ /**
+ * Removes all sub nodes with the given name.
+ *
+ * @param name the name
+ * @return a flag if at least on sub node was removed
+ */
+ public boolean removeNodes(String name)
+ {
+ if (nodes != null && name != null)
+ {
+ List<ConfigurationNode> lst = namedNodes.remove(name);
+ if (lst != null)
+ {
+ detachNodes(lst);
+ nodes.removeAll(lst);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Removes all sub nodes.
+ */
+ public void clear()
+ {
+ if (nodes != null)
+ {
+ detachNodes(nodes);
+ nodes = null;
+ namedNodes = null;
+ }
+ }
+
+ /**
+ * Returns the node with the given index. If this index cannot be found,
+ * an {@code IndexOutOfBoundException} exception will be thrown.
+ *
+ * @param index the index (0-based)
+ * @return the sub node at the specified index
+ */
+ public ConfigurationNode getNode(int index)
+ {
+ if (nodes == null)
+ {
+ throw new IndexOutOfBoundsException("No sub nodes available!");
+ }
+ return nodes.get(index);
+ }
+
+ /**
+ * Returns a list with all stored sub nodes. The return value is never
+ * <b>null</b>.
+ *
+ * @return a list with the sub nodes
+ */
+ public List<ConfigurationNode> getSubNodes()
+ {
+ if (nodes == null)
+ {
+ return Collections.emptyList();
+ }
+ else
+ {
+ return Collections.unmodifiableList(nodes);
+ }
+ }
+
+ /**
+ * Returns a list of the sub nodes with the given name. The return value
+ * is never <b>null</b>.
+ *
+ * @param name the name; if <b>null</b> is passed, all sub nodes will
+ * be returned
+ * @return all sub nodes with this name
+ */
+ public List<ConfigurationNode> getSubNodes(String name)
+ {
+ if (name == null)
+ {
+ return getSubNodes();
+ }
+
+ List<ConfigurationNode> result;
+ if (nodes == null)
+ {
+ result = null;
+ }
+ else
+ {
+ result = namedNodes.get(name);
+ }
+
+ if (result == null)
+ {
+ return Collections.emptyList();
+ }
+ else
+ {
+ return Collections.unmodifiableList(result);
+ }
+ }
+
+ /**
+ * Let the passed in visitor visit all sub nodes.
+ *
+ * @param visitor the visitor
+ */
+ public void visit(ConfigurationNodeVisitor visitor)
+ {
+ if (nodes != null)
+ {
+ for (Iterator<ConfigurationNode> it = nodes.iterator(); it.hasNext()
+ && !visitor.terminate();)
+ {
+ it.next().visit(visitor);
+ }
+ }
+ }
+
+ /**
+ * This method is called whenever a sub node is removed from this
+ * object. It ensures that the removed node's parent is reset and its
+ * {@code removeReference()} method gets called.
+ *
+ * @param subNode the node to be removed
+ */
+ protected void detachNode(ConfigurationNode subNode)
+ {
+ subNode.setParentNode(null);
+ if (subNode instanceof DefaultConfigurationNode)
+ {
+ ((DefaultConfigurationNode) subNode).removeReference();
+ }
+ }
+
+ /**
+ * Detaches a list of sub nodes. This method calls
+ * {@code detachNode()} for each node contained in the list.
+ *
+ * @param subNodes the list with nodes to be detached
+ */
+ protected void detachNodes(Collection<? extends ConfigurationNode> subNodes)
+ {
+ for (ConfigurationNode nd : subNodes)
+ {
+ detachNode(nd);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/DefaultExpressionEngine.java b/src/main/java/org/apache/commons/configuration/tree/DefaultExpressionEngine.java
new file mode 100644
index 0000000..e4a8a8c
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/DefaultExpressionEngine.java
@@ -0,0 +1,545 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * <p>
+ * A default implementation of the {@code ExpressionEngine} interface
+ * providing the "native"e; expression language for hierarchical
+ * configurations.
+ * </p>
+ * <p>
+ * This class implements a rather simple expression language for navigating
+ * through a hierarchy of configuration nodes. It supports the following
+ * operations:
+ * </p>
+ * <p>
+ * <ul>
+ * <li>Navigating from a node to one of its children using the child node
+ * delimiter, which is by the default a dot (".").</li>
+ * <li>Navigating from a node to one of its attributes using the attribute node
+ * delimiter, which by default follows the XPATH like syntax
+ * <code>[@<attributeName>]</code>.</li>
+ * <li>If there are multiple child or attribute nodes with the same name, a
+ * specific node can be selected using a numerical index. By default indices are
+ * written in parenthesis.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * As an example consider the following XML document:
+ * </p>
+ *
+ * <pre>
+ * <database>
+ * <tables>
+ * <table type="system">
+ * <name>users</name>
+ * <fields>
+ * <field>
+ * <name>lid</name>
+ * <type>long</name>
+ * </field>
+ * <field>
+ * <name>usrName</name>
+ * <type>java.lang.String</type>
+ * </field>
+ * ...
+ * </fields>
+ * </table>
+ * <table>
+ * <name>documents</name>
+ * <fields>
+ * <field>
+ * <name>docid</name>
+ * <type>long</type>
+ * </field>
+ * ...
+ * </fields>
+ * </table>
+ * ...
+ * </tables>
+ * </database>
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * If this document is parsed and stored in a hierarchical configuration object,
+ * for instance the key {@code tables.table(0).name} can be used to find
+ * out the name of the first table. In opposite {@code tables.table.name}
+ * would return a collection with the names of all available tables. Similarly
+ * the key {@code tables.table(1).fields.field.name} returns a collection
+ * with the names of all fields of the second table. If another index is added
+ * after the {@code field} element, a single field can be accessed:
+ * {@code tables.table(1).fields.field(0).name}. The key
+ * {@code tables.table(0)[@type]} would select the type attribute of the
+ * first table.
+ * </p>
+ * <p>
+ * This example works with the default values for delimiters and index markers.
+ * It is also possible to set custom values for these properties so that you can
+ * adapt a {@code DefaultExpressionEngine} to your personal needs.
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: DefaultExpressionEngine.java 1301991 2012-03-17 20:18:02Z sebb $
+ */
+public class DefaultExpressionEngine implements ExpressionEngine
+{
+ /** Constant for the default property delimiter. */
+ public static final String DEFAULT_PROPERTY_DELIMITER = ".";
+
+ /** Constant for the default escaped property delimiter. */
+ public static final String DEFAULT_ESCAPED_DELIMITER = DEFAULT_PROPERTY_DELIMITER
+ + DEFAULT_PROPERTY_DELIMITER;
+
+ /** Constant for the default attribute start marker. */
+ public static final String DEFAULT_ATTRIBUTE_START = "[@";
+
+ /** Constant for the default attribute end marker. */
+ public static final String DEFAULT_ATTRIBUTE_END = "]";
+
+ /** Constant for the default index start marker. */
+ public static final String DEFAULT_INDEX_START = "(";
+
+ /** Constant for the default index end marker. */
+ public static final String DEFAULT_INDEX_END = ")";
+
+ /** Stores the property delimiter. */
+ private String propertyDelimiter = DEFAULT_PROPERTY_DELIMITER;
+
+ /** Stores the escaped property delimiter. */
+ private String escapedDelimiter = DEFAULT_ESCAPED_DELIMITER;
+
+ /** Stores the attribute start marker. */
+ private String attributeStart = DEFAULT_ATTRIBUTE_START;
+
+ /** Stores the attribute end marker. */
+ private String attributeEnd = DEFAULT_ATTRIBUTE_END;
+
+ /** Stores the index start marker. */
+ private String indexStart = DEFAULT_INDEX_START;
+
+ /** stores the index end marker. */
+ private String indexEnd = DEFAULT_INDEX_END;
+
+ /**
+ * Sets the attribute end marker.
+ *
+ * @return the attribute end marker
+ */
+ public String getAttributeEnd()
+ {
+ return attributeEnd;
+ }
+
+ /**
+ * Sets the attribute end marker.
+ *
+ * @param attributeEnd the attribute end marker; can be <b>null</b> if no
+ * end marker is needed
+ */
+ public void setAttributeEnd(String attributeEnd)
+ {
+ this.attributeEnd = attributeEnd;
+ }
+
+ /**
+ * Returns the attribute start marker.
+ *
+ * @return the attribute start marker
+ */
+ public String getAttributeStart()
+ {
+ return attributeStart;
+ }
+
+ /**
+ * Sets the attribute start marker. Attribute start and end marker are used
+ * together to detect attributes in a property key.
+ *
+ * @param attributeStart the attribute start marker
+ */
+ public void setAttributeStart(String attributeStart)
+ {
+ this.attributeStart = attributeStart;
+ }
+
+ /**
+ * Returns the escaped property delimiter string.
+ *
+ * @return the escaped property delimiter
+ */
+ public String getEscapedDelimiter()
+ {
+ return escapedDelimiter;
+ }
+
+ /**
+ * Sets the escaped property delimiter string. With this string a delimiter
+ * that belongs to the key of a property can be escaped. If for instance
+ * "." is used as property delimiter, you can set the escaped
+ * delimiter to "\." and can then escape the delimiter with a back
+ * slash.
+ *
+ * @param escapedDelimiter the escaped delimiter string
+ */
+ public void setEscapedDelimiter(String escapedDelimiter)
+ {
+ this.escapedDelimiter = escapedDelimiter;
+ }
+
+ /**
+ * Returns the index end marker.
+ *
+ * @return the index end marker
+ */
+ public String getIndexEnd()
+ {
+ return indexEnd;
+ }
+
+ /**
+ * Sets the index end marker.
+ *
+ * @param indexEnd the index end marker
+ */
+ public void setIndexEnd(String indexEnd)
+ {
+ this.indexEnd = indexEnd;
+ }
+
+ /**
+ * Returns the index start marker.
+ *
+ * @return the index start marker
+ */
+ public String getIndexStart()
+ {
+ return indexStart;
+ }
+
+ /**
+ * Sets the index start marker. Index start and end marker are used together
+ * to detect indices in a property key.
+ *
+ * @param indexStart the index start marker
+ */
+ public void setIndexStart(String indexStart)
+ {
+ this.indexStart = indexStart;
+ }
+
+ /**
+ * Returns the property delimiter.
+ *
+ * @return the property delimiter
+ */
+ public String getPropertyDelimiter()
+ {
+ return propertyDelimiter;
+ }
+
+ /**
+ * Sets the property delimiter. This string is used to split the parts of a
+ * property key.
+ *
+ * @param propertyDelimiter the property delimiter
+ */
+ public void setPropertyDelimiter(String propertyDelimiter)
+ {
+ this.propertyDelimiter = propertyDelimiter;
+ }
+
+ /**
+ * Evaluates the given key and returns all matching nodes. This method
+ * supports the syntax as described in the class comment.
+ *
+ * @param root the root node
+ * @param key the key
+ * @return a list with the matching nodes
+ */
+ public List<ConfigurationNode> query(ConfigurationNode root, String key)
+ {
+ List<ConfigurationNode> nodes = new LinkedList<ConfigurationNode>();
+ findNodesForKey(new DefaultConfigurationKey(this, key).iterator(),
+ root, nodes);
+ return nodes;
+ }
+
+ /**
+ * Determines the key of the passed in node. This implementation takes the
+ * given parent key, adds a property delimiter, and then adds the node's
+ * name. (For attribute nodes the attribute delimiters are used instead.)
+ * The name of the root node is a blanc string. Note that no indices will be
+ * returned.
+ *
+ * @param node the node whose key is to be determined
+ * @param parentKey the key of this node's parent
+ * @return the key for the given node
+ */
+ public String nodeKey(ConfigurationNode node, String parentKey)
+ {
+ if (parentKey == null)
+ {
+ // this is the root node
+ return StringUtils.EMPTY;
+ }
+
+ else
+ {
+ DefaultConfigurationKey key = new DefaultConfigurationKey(this,
+ parentKey);
+ if (node.isAttribute())
+ {
+ key.appendAttribute(node.getName());
+ }
+ else
+ {
+ key.append(node.getName(), true);
+ }
+ return key.toString();
+ }
+ }
+
+ /**
+ * <p>
+ * Prepares Adding the property with the specified key.
+ * </p>
+ * <p>
+ * To be able to deal with the structure supported by hierarchical
+ * configuration implementations the passed in key is of importance,
+ * especially the indices it might contain. The following example should
+ * clarify this: Suppose the actual node structure looks like the
+ * following:
+ * </p>
+ * <p>
+ * <pre>
+ * tables
+ * +-- table
+ * +-- name = user
+ * +-- fields
+ * +-- field
+ * +-- name = uid
+ * +-- field
+ * +-- name = firstName
+ * ...
+ * +-- table
+ * +-- name = documents
+ * +-- fields
+ * ...
+ * </pre>
+ * </p>
+ * <p>
+ * In this example a database structure is defined, e.g. all fields of the
+ * first table could be accessed using the key
+ * {@code tables.table(0).fields.field.name}. If now properties are
+ * to be added, it must be exactly specified at which position in the
+ * hierarchy the new property is to be inserted. So to add a new field name
+ * to a table it is not enough to say just
+ * </p>
+ * <p>
+ * <pre>
+ * config.addProperty("tables.table.fields.field.name", "newField");
+ * </pre>
+ * </p>
+ * <p>
+ * The statement given above contains some ambiguity. For instance it is not
+ * clear, to which table the new field should be added. If this method finds
+ * such an ambiguity, it is resolved by following the last valid path. Here
+ * this would be the last table. The same is true for the {@code field};
+ * because there are multiple fields and no explicit index is provided, a
+ * new {@code name} property would be added to the last field - which
+ * is probably not what was desired.
+ * </p>
+ * <p>
+ * To make things clear explicit indices should be provided whenever
+ * possible. In the example above the exact table could be specified by
+ * providing an index for the {@code table} element as in
+ * {@code tables.table(1).fields}. By specifying an index it can
+ * also be expressed that at a given position in the configuration tree a
+ * new branch should be added. In the example above we did not want to add
+ * an additional {@code name} element to the last field of the table,
+ * but we want a complete new {@code field} element. This can be
+ * achieved by specifying an invalid index (like -1) after the element where
+ * a new branch should be created. Given this our example would run:
+ * </p>
+ * <p>
+ * <pre>
+ * config.addProperty("tables.table(1).fields.field(-1).name", "newField");
+ * </pre>
+ * </p>
+ * <p>
+ * With this notation it is possible to add new branches everywhere. We
+ * could for instance create a new {@code table} element by
+ * specifying
+ * </p>
+ * <p>
+ * <pre>
+ * config.addProperty("tables.table(-1).fields.field.name", "newField2");
+ * </pre>
+ * </p>
+ * <p>
+ * (Note that because after the {@code table} element a new branch is
+ * created indices in following elements are not relevant; the branch is new
+ * so there cannot be any ambiguities.)
+ * </p>
+ *
+ * @param root the root node of the nodes hierarchy
+ * @param key the key of the new property
+ * @return a data object with information needed for the add operation
+ */
+ public NodeAddData prepareAdd(ConfigurationNode root, String key)
+ {
+ DefaultConfigurationKey.KeyIterator it = new DefaultConfigurationKey(
+ this, key).iterator();
+ if (!it.hasNext())
+ {
+ throw new IllegalArgumentException(
+ "Key for add operation must be defined!");
+ }
+
+ NodeAddData result = new NodeAddData();
+ result.setParent(findLastPathNode(it, root));
+
+ while (it.hasNext())
+ {
+ if (!it.isPropertyKey())
+ {
+ throw new IllegalArgumentException(
+ "Invalid key for add operation: " + key
+ + " (Attribute key in the middle.)");
+ }
+ result.addPathNode(it.currentKey());
+ it.next();
+ }
+
+ result.setNewNodeName(it.currentKey());
+ result.setAttribute(!it.isPropertyKey());
+ return result;
+ }
+
+ /**
+ * Recursive helper method for evaluating a key. This method processes all
+ * facets of a configuration key, traverses the tree of properties and
+ * fetches the the nodes of all matching properties.
+ *
+ * @param keyPart the configuration key iterator
+ * @param node the actual node
+ * @param nodes here the found nodes are stored
+ */
+ protected void findNodesForKey(DefaultConfigurationKey.KeyIterator keyPart,
+ ConfigurationNode node, Collection<ConfigurationNode> nodes)
+ {
+ if (!keyPart.hasNext())
+ {
+ nodes.add(node);
+ }
+
+ else
+ {
+ String key = keyPart.nextKey(false);
+ if (keyPart.isPropertyKey())
+ {
+ processSubNodes(keyPart, node.getChildren(key), nodes);
+ }
+ if (keyPart.isAttribute())
+ {
+ processSubNodes(keyPart, node.getAttributes(key), nodes);
+ }
+ }
+ }
+
+ /**
+ * Finds the last existing node for an add operation. This method traverses
+ * the configuration node tree along the specified key. The last existing
+ * node on this path is returned.
+ *
+ * @param keyIt the key iterator
+ * @param node the actual node
+ * @return the last existing node on the given path
+ */
+ protected ConfigurationNode findLastPathNode(
+ DefaultConfigurationKey.KeyIterator keyIt, ConfigurationNode node)
+ {
+ String keyPart = keyIt.nextKey(false);
+
+ if (keyIt.hasNext())
+ {
+ if (!keyIt.isPropertyKey())
+ {
+ // Attribute keys can only appear as last elements of the path
+ throw new IllegalArgumentException(
+ "Invalid path for add operation: "
+ + "Attribute key in the middle!");
+ }
+ int idx = keyIt.hasIndex() ? keyIt.getIndex() : node
+ .getChildrenCount(keyPart) - 1;
+ if (idx < 0 || idx >= node.getChildrenCount(keyPart))
+ {
+ return node;
+ }
+ else
+ {
+ return findLastPathNode(keyIt, node.getChildren(keyPart).get(idx));
+ }
+ }
+
+ else
+ {
+ return node;
+ }
+ }
+
+ /**
+ * Called by {@code findNodesForKey()} to process the sub nodes of
+ * the current node depending on the type of the current key part (children,
+ * attributes, or both).
+ *
+ * @param keyPart the key part
+ * @param subNodes a list with the sub nodes to process
+ * @param nodes the target collection
+ */
+ private void processSubNodes(DefaultConfigurationKey.KeyIterator keyPart,
+ List<ConfigurationNode> subNodes, Collection<ConfigurationNode> nodes)
+ {
+ if (keyPart.hasIndex())
+ {
+ if (keyPart.getIndex() >= 0 && keyPart.getIndex() < subNodes.size())
+ {
+ findNodesForKey((DefaultConfigurationKey.KeyIterator) keyPart
+ .clone(), subNodes.get(keyPart.getIndex()), nodes);
+ }
+ }
+ else
+ {
+ for (ConfigurationNode node : subNodes)
+ {
+ findNodesForKey((DefaultConfigurationKey.KeyIterator) keyPart
+ .clone(), node, nodes);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/ExpressionEngine.java b/src/main/java/org/apache/commons/configuration/tree/ExpressionEngine.java
new file mode 100644
index 0000000..7ab266e
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/ExpressionEngine.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import java.util.List;
+
+/**
+ * <p>
+ * Definition of an interface for evaluating keys for hierarchical
+ * configurations.
+ * </p>
+ * <p>
+ * An <em>expression engine</em> knows how to map a key for a configuration's
+ * property to a single or a set of configuration nodes. Thus it defines the way
+ * how properties are addressed in this configuration. Methods of a
+ * configuration that have to handle property key (e.g.
+ * {@code getProperty()} or {@code addProperty()} do not interpret
+ * the passed in keys on their own, but delegate this task to an associated
+ * expression engine. This expression engine will then find out, which
+ * configuration nodes are addressed by the key.
+ * </p>
+ * <p>
+ * Separating the task of evaluating property keys from the configuration object
+ * has the advantage that many different expression languages (i.e. ways for
+ * querying or setting properties) can be supported. Just set a suitable
+ * implementation of this interface as the configuration's expression engine,
+ * and you can use the syntax provided by this implementation.
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ExpressionEngine.java 1206474 2011-11-26 16:14:09Z oheger $
+ */
+public interface ExpressionEngine
+{
+ /**
+ * Finds the node(s) that is (are) matched by the specified key. This is the
+ * main method for interpreting property keys. An implementation must
+ * traverse the given root node and its children to find all nodes that are
+ * matched by the given key. If the key is not correct in the syntax
+ * provided by that implementation, it is free to throw a (runtime)
+ * exception indicating this error condition.
+ *
+ * @param root the root node of a hierarchy of configuration nodes
+ * @param key the key to be evaluated
+ * @return a list with the nodes that are matched by the key (should never
+ * be <b>null</b>)
+ */
+ List<ConfigurationNode> query(ConfigurationNode root, String key);
+
+ /**
+ * Returns the key for the specified node in the expression language
+ * supported by an implementation. This method is called whenever a property
+ * key for a node has to be constructed, e.g. by the
+ * {@link org.apache.commons.configuration.Configuration#getKeys() getKeys()}
+ * method.
+ *
+ * @param node the node, for which the key must be constructed
+ * @param parentKey the key of this node's parent (can be <b>null</b> for
+ * the root node)
+ * @return this node's key
+ */
+ String nodeKey(ConfigurationNode node, String parentKey);
+
+ /**
+ * Returns information needed for an add operation. This method gets called
+ * when new properties are to be added to a configuration. An implementation
+ * has to interpret the specified key, find the parent node for the new
+ * elements, and provide all information about new nodes to be added.
+ *
+ * @param root the root node
+ * @param key the key for the new property
+ * @return an object with all information needed for the add operation
+ */
+ NodeAddData prepareAdd(ConfigurationNode root, String key);
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/MergeCombiner.java b/src/main/java/org/apache/commons/configuration/tree/MergeCombiner.java
new file mode 100644
index 0000000..72a77b0
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/MergeCombiner.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * <p>
+ * A specialized implementation of the {@code NodeCombiner} interface
+ * that performs a merge from two passed in node hierarchies.
+ * </p>
+ * <p>
+ * This combiner performs the merge using a few rules:
+ * <ol>
+ * <li>Nodes can be merged when attributes that appear in both have the same value.</li>
+ * <li>Only a single node in the second file is considered a match to the node in the first file.</li>
+ * <li>Attributes in nodes that match are merged.
+ * <li>Nodes in both files that do not match are added to the result.</li>
+ * </ol>
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: MergeCombiner.java 1301991 2012-03-17 20:18:02Z sebb $
+ * @since 1.7
+ */
+public class MergeCombiner extends NodeCombiner
+{
+ /**
+ * Combines the given nodes to a new union node.
+ *
+ * @param node1 the first source node
+ * @param node2 the second source node
+ * @return the union node
+ */
+
+ @Override
+ public ConfigurationNode combine(ConfigurationNode node1, ConfigurationNode node2)
+ {
+ ViewNode result = createViewNode();
+ result.setName(node1.getName());
+ result.setValue(node1.getValue());
+ addAttributes(result, node1, node2);
+
+ // Check if nodes can be combined
+ List<ConfigurationNode> children2 = new LinkedList<ConfigurationNode>(node2.getChildren());
+ for (ConfigurationNode child1 : node1.getChildren())
+ {
+ ConfigurationNode child2 = canCombine(node1, node2, child1, children2);
+ if (child2 != null)
+ {
+ result.addChild(combine(child1, child2));
+ children2.remove(child2);
+ }
+ else
+ {
+ result.addChild(child1);
+ }
+ }
+
+ // Add remaining children of node 2
+ for (ConfigurationNode c : children2)
+ {
+ result.addChild(c);
+ }
+ return result;
+ }
+
+ /**
+ * Handles the attributes during a combination process. First all attributes
+ * of the first node will be added to the result. Then all attributes of the
+ * second node, which are not contained in the first node, will also be
+ * added.
+ *
+ * @param result the resulting node
+ * @param node1 the first node
+ * @param node2 the second node
+ */
+ protected void addAttributes(ViewNode result, ConfigurationNode node1,
+ ConfigurationNode node2)
+ {
+ result.appendAttributes(node1);
+ for (ConfigurationNode attr : node2.getAttributes())
+ {
+ if (node1.getAttributeCount(attr.getName()) == 0)
+ {
+ result.addAttribute(attr);
+ }
+ }
+ }
+
+ /**
+ * Tests if the first node can be combined with the second node. A node can
+ * only be combined if its attributes are all present in the second node and
+ * they all have the same value.
+ *
+ * @param node1 the first node
+ * @param node2 the second node
+ * @param child the child node (of the first node)
+ * @return a child of the second node, with which a combination is possible
+ */
+ protected ConfigurationNode canCombine(ConfigurationNode node1,
+ ConfigurationNode node2, ConfigurationNode child, List<ConfigurationNode> children2)
+ {
+ List<ConfigurationNode> attrs1 = child.getAttributes();
+ List<ConfigurationNode> nodes = new ArrayList<ConfigurationNode>();
+
+ List<ConfigurationNode> children = node2.getChildren(child.getName());
+ Iterator<ConfigurationNode> it = children.iterator();
+ while (it.hasNext())
+ {
+ ConfigurationNode node = it.next();
+ Iterator<ConfigurationNode> iter = attrs1.iterator();
+ while (iter.hasNext())
+ {
+ ConfigurationNode attr1 = iter.next();
+ List<ConfigurationNode> list2 = node.getAttributes(attr1.getName());
+ if (list2.size() == 1
+ && !attr1.getValue().equals(list2.get(0).getValue()))
+ {
+ node = null;
+ break;
+ }
+ }
+ if (node != null)
+ {
+ nodes.add(node);
+ }
+ }
+
+ if (nodes.size() == 1)
+ {
+ return nodes.get(0);
+ }
+ if (nodes.size() > 1 && !isListNode(child))
+ {
+ Iterator<ConfigurationNode> iter = nodes.iterator();
+ while (iter.hasNext())
+ {
+ children2.remove(iter.next());
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/NodeAddData.java b/src/main/java/org/apache/commons/configuration/tree/NodeAddData.java
new file mode 100644
index 0000000..f65ea8b
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/NodeAddData.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * <p>
+ * A simple data class used by {@link ExpressionEngine} to store
+ * the results of the {@code prepareAdd()} operation.
+ * </p>
+ * <p>
+ * If a new property is to be added to a configuration, the affected
+ * {@code Configuration} object must know, where in its hierarchy of
+ * configuration nodes new elements have to be added. This information is
+ * obtained by an {@code ExpressionEngine} object that interprets the key
+ * of the new property. This expression engine will pack all information
+ * necessary for the configuration to perform the add operation in an instance
+ * of this class.
+ * </p>
+ * <p>
+ * Information managed by this class contains:
+ * <ul>
+ * <li>the configuration node, to which new elements must be added</li>
+ * <li>the name of the new node</li>
+ * <li>whether the new node is a child node or an attribute node</li>
+ * <li>if a whole branch is to be added at once, the names of all nodes between
+ * the parent node (the target of the add operation) and the new node</li>
+ * </ul>
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: NodeAddData.java 1234988 2012-01-23 21:12:15Z oheger $
+ */
+public class NodeAddData
+{
+ /** Stores the parent node of the add operation. */
+ private ConfigurationNode parent;
+
+ /**
+ * Stores a list with nodes that are on the path between the parent node and
+ * the new node.
+ */
+ private List<String> pathNodes;
+
+ /** Stores the name of the new node. */
+ private String newNodeName;
+
+ /** Stores the attribute flag. */
+ private boolean attribute;
+
+ /**
+ * Creates a new, uninitialized instance of {@code NodeAddData}.
+ */
+ public NodeAddData()
+ {
+ this(null, null);
+ }
+
+ /**
+ * Creates a new instance of {@code NodeAddData} and sets the most
+ * important data fields.
+ *
+ * @param parent the parent node
+ * @param nodeName the name of the new node
+ */
+ public NodeAddData(ConfigurationNode parent, String nodeName)
+ {
+ setParent(parent);
+ setNewNodeName(nodeName);
+ }
+
+ /**
+ * Returns a flag if the new node to be added is an attribute.
+ *
+ * @return <b>true</b> for an attribute node, <b>false</b> for a child
+ * node
+ */
+ public boolean isAttribute()
+ {
+ return attribute;
+ }
+
+ /**
+ * Sets the attribute flag. This flag determines whether an attribute or a
+ * child node will be added.
+ *
+ * @param attribute the attribute flag
+ */
+ public void setAttribute(boolean attribute)
+ {
+ this.attribute = attribute;
+ }
+
+ /**
+ * Returns the name of the new node.
+ *
+ * @return the new node's name
+ */
+ public String getNewNodeName()
+ {
+ return newNodeName;
+ }
+
+ /**
+ * Sets the name of the new node. A node with this name will be added to the
+ * configuration's node hierarchy.
+ *
+ * @param newNodeName the name of the new node
+ */
+ public void setNewNodeName(String newNodeName)
+ {
+ this.newNodeName = newNodeName;
+ }
+
+ /**
+ * Returns the parent node.
+ *
+ * @return the parent node
+ */
+ public ConfigurationNode getParent()
+ {
+ return parent;
+ }
+
+ /**
+ * Sets the parent node. New nodes will be added to this node.
+ *
+ * @param parent the parent node
+ */
+ public void setParent(ConfigurationNode parent)
+ {
+ this.parent = parent;
+ }
+
+ /**
+ * Returns a list with further nodes that must be added. This is needed if a
+ * complete branch is to be added at once. For instance imagine that there
+ * exists only a node {@code database}. Now the key
+ * {@code database.connection.settings.username} (assuming the syntax
+ * of the default expression engine) is to be added. Then
+ * {@code username} is the name of the new node, but the nodes
+ * {@code connection} and {@code settings} must be added to
+ * the parent node first. In this example these names would be returned by
+ * this method.
+ *
+ * @return a list with the names of nodes that must be added as parents of
+ * the new node (never <b>null</b>)
+ */
+ public List<String> getPathNodes()
+ {
+ if (pathNodes != null)
+ {
+ return Collections.unmodifiableList(pathNodes);
+ }
+ else
+ {
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Adds the name of a path node. With this method an additional node to be
+ * added can be defined.
+ *
+ * @param nodeName the name of the node
+ * @see #getPathNodes()
+ */
+ public void addPathNode(String nodeName)
+ {
+ if (pathNodes == null)
+ {
+ pathNodes = new LinkedList<String>();
+ }
+ pathNodes.add(nodeName);
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/NodeCombiner.java b/src/main/java/org/apache/commons/configuration/tree/NodeCombiner.java
new file mode 100644
index 0000000..f4526f6
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/NodeCombiner.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * <p>
+ * A base class for node combiner implementations.
+ * </p>
+ * <p>
+ * A <em>node combiner</em> is an object that knows how two hierarchical node
+ * structures can be combined into a single one. Of course, there are many
+ * possible ways of implementing such a combination, e.g. constructing a union,
+ * an intersection, or an "override" structure (were nodes in the first
+ * hierarchy take precedence over nodes in the second hierarchy). This abstract
+ * base class only provides some helper methods and defines the common interface
+ * for node combiners. Concrete sub classes will implement the diverse
+ * combination algorithms.
+ * </p>
+ * <p>
+ * For some concrete combiner implementations it is important to distinguish
+ * whether a node is a single node or whether it belongs to a list structure.
+ * Alone from the input structures, the combiner will not always be able to make
+ * this decision. So sometimes it may be necessary for the developer to
+ * configure the combiner and tell it, which nodes should be treated as list
+ * nodes. For this purpose the {@code addListNode()} method exists. It
+ * can be passed the name of a node, which should be considered a list node.
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: NodeCombiner.java 1206476 2011-11-26 16:19:53Z oheger $
+ * @since 1.3
+ */
+public abstract class NodeCombiner
+{
+ /** Stores a list with node names that are known to be list nodes. */
+ protected Set<String> listNodes;
+
+ /**
+ * Creates a new instance of {@code NodeCombiner}.
+ */
+ public NodeCombiner()
+ {
+ listNodes = new HashSet<String>();
+ }
+
+ /**
+ * Adds the name of a node to the list of known list nodes. This means that
+ * nodes with this name will never be combined.
+ *
+ * @param nodeName the name to be added
+ */
+ public void addListNode(String nodeName)
+ {
+ listNodes.add(nodeName);
+ }
+
+ /**
+ * Returns a set with the names of nodes that are known to be list nodes.
+ *
+ * @return a set with the names of list nodes
+ */
+ public Set<String> getListNodes()
+ {
+ return Collections.unmodifiableSet(listNodes);
+ }
+
+ /**
+ * Checks if a node is a list node. This implementation tests if the given
+ * node name is contained in the set of known list nodes. Derived classes
+ * which use different criteria may overload this method.
+ *
+ * @param node the node to be tested
+ * @return a flag whether this is a list node
+ */
+ public boolean isListNode(ConfigurationNode node)
+ {
+ return listNodes.contains(node.getName());
+ }
+
+ /**
+ * Combines the hierarchies represented by the given root nodes. This method
+ * must be defined in concrete sub classes with the implementation of a
+ * specific combination algorithm.
+ *
+ * @param node1 the first root node
+ * @param node2 the second root node
+ * @return the resulting combined node structure
+ */
+ public abstract ConfigurationNode combine(ConfigurationNode node1,
+ ConfigurationNode node2);
+
+ /**
+ * Creates a new view node. This method will be called whenever a new view
+ * node is to be created. It can be overridden to create special view nodes.
+ * This base implementation returns a new instance of
+ * {@link ViewNode}.
+ *
+ * @return the new view node
+ */
+ protected ViewNode createViewNode()
+ {
+ return new ViewNode();
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/OverrideCombiner.java b/src/main/java/org/apache/commons/configuration/tree/OverrideCombiner.java
new file mode 100644
index 0000000..64d5aec
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/OverrideCombiner.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+
+/**
+ * <p>
+ * A concrete combiner implementation that is able to construct an override
+ * combination.
+ * </p>
+ * <p>
+ * An <em>override combination</em> means that nodes in the first node
+ * structure take precedence over nodes in the second, or - in other words -
+ * nodes of the second structure are only added to the resulting structure if
+ * they do not occur in the first one. This is especially suitable for dealing
+ * with the properties of configurations that are defined in an
+ * {@code override} section of a configuration definition file (hence the
+ * name).
+ * </p>
+ * <p>
+ * This combiner will iterate over the second node hierarchy and find all nodes
+ * that are not contained in the first hierarchy; these are added to the result.
+ * If a node can be found in both structures, it is checked whether a
+ * combination (in a recursive way) can be constructed for the two, which will
+ * then be added. Per default, nodes are combined, which occur only once in both
+ * structures. This test is implemented in the {@code canCombine()}
+ * method.
+ * </p>
+ * <p>
+ * As is true for the {@link UnionCombiner}, for this combiner
+ * list nodes are important. The {@code addListNode()} can be called to
+ * declare certain nodes as list nodes. This has the effect that these nodes
+ * will never be combined.
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: OverrideCombiner.java 1301991 2012-03-17 20:18:02Z sebb $
+ * @since 1.3
+ */
+public class OverrideCombiner extends NodeCombiner
+{
+ /**
+ * Constructs an override combination for the passed in node structures.
+ *
+ * @param node1 the first node
+ * @param node2 the second node
+ * @return the resulting combined node structure
+ */
+ @Override
+ public ConfigurationNode combine(ConfigurationNode node1,
+ ConfigurationNode node2)
+ {
+ ViewNode result = createViewNode();
+ result.setName(node1.getName());
+
+ // Process nodes from the first structure, which override the second
+ for (ConfigurationNode child : node1.getChildren())
+ {
+ ConfigurationNode child2 = canCombine(node1, node2, child);
+ if (child2 != null)
+ {
+ result.addChild(combine(child, child2));
+ }
+ else
+ {
+ result.addChild(child);
+ }
+ }
+
+ // Process nodes from the second structure, which are not contained
+ // in the first structure
+ for (ConfigurationNode child : node2.getChildren())
+ {
+ if (node1.getChildrenCount(child.getName()) < 1)
+ {
+ result.addChild(child);
+ }
+ }
+
+ // Handle attributes and value
+ addAttributes(result, node1, node2);
+ result.setValue((node1.getValue() != null) ? node1.getValue() : node2
+ .getValue());
+
+ return result;
+ }
+
+ /**
+ * Handles the attributes during a combination process. First all attributes
+ * of the first node will be added to the result. Then all attributes of the
+ * second node, which are not contained in the first node, will also be
+ * added.
+ *
+ * @param result the resulting node
+ * @param node1 the first node
+ * @param node2 the second node
+ */
+ protected void addAttributes(ViewNode result, ConfigurationNode node1,
+ ConfigurationNode node2)
+ {
+ result.appendAttributes(node1);
+ for (ConfigurationNode attr : node2.getAttributes())
+ {
+ if (node1.getAttributeCount(attr.getName()) == 0)
+ {
+ result.addAttribute(attr);
+ }
+ }
+ }
+
+ /**
+ * Tests if a child node of the second node can be combined with the given
+ * child node of the first node. If this is the case, the corresponding node
+ * will be returned, otherwise <b>null</b>. This implementation checks
+ * whether the child node occurs only once in both hierarchies and is no
+ * known list node.
+ *
+ * @param node1 the first node
+ * @param node2 the second node
+ * @param child the child node (of the first node)
+ * @return a child of the second node, with which a combination is possible
+ */
+ protected ConfigurationNode canCombine(ConfigurationNode node1,
+ ConfigurationNode node2, ConfigurationNode child)
+ {
+ if (node2.getChildrenCount(child.getName()) == 1
+ && node1.getChildrenCount(child.getName()) == 1
+ && !isListNode(child))
+ {
+ return node2.getChildren(child.getName()).get(0);
+ }
+ else
+ {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/TreeUtils.java b/src/main/java/org/apache/commons/configuration/tree/TreeUtils.java
new file mode 100644
index 0000000..7817870
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/TreeUtils.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import java.io.PrintStream;
+import java.util.Iterator;
+
+/**
+ * Utility methods.
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TreeUtils.java 1301991 2012-03-17 20:18:02Z sebb $
+ * @since 1.7
+ */
+public final class TreeUtils
+{
+ /** Prevent creating this class. */
+ private TreeUtils()
+ {
+ }
+
+ /**
+ * Print out the data in the configuration.
+ * @param stream The OutputStream.
+ * @param result The root node of the tree.
+ */
+ public static void printTree(PrintStream stream, ConfigurationNode result)
+ {
+ if (stream != null)
+ {
+ printTree(stream, "", result);
+ }
+ }
+
+ private static void printTree(PrintStream stream, String indent, ConfigurationNode result)
+ {
+ StringBuffer buffer = new StringBuffer(indent).append("<").append(result.getName());
+ Iterator<ConfigurationNode> iter = result.getAttributes().iterator();
+ while (iter.hasNext())
+ {
+ ConfigurationNode node = iter.next();
+ buffer.append(" ").append(node.getName()).append("='").append(node.getValue()).append("'");
+ }
+ buffer.append(">");
+ stream.print(buffer.toString());
+ if (result.getValue() != null)
+ {
+ stream.print(result.getValue());
+ }
+ boolean newline = false;
+ if (result.getChildrenCount() > 0)
+ {
+ stream.print("\n");
+ iter = result.getChildren().iterator();
+ while (iter.hasNext())
+ {
+ printTree(stream, indent + " ", iter.next());
+ }
+ newline = true;
+ }
+ if (newline)
+ {
+ stream.print(indent);
+ }
+ stream.println("</" + result.getName() + ">");
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/UnionCombiner.java b/src/main/java/org/apache/commons/configuration/tree/UnionCombiner.java
new file mode 100644
index 0000000..39fee6c
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/UnionCombiner.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * <p>
+ * A specialized implementation of the {@code NodeCombiner} interface
+ * that constructs a union from two passed in node hierarchies.
+ * </p>
+ * <p>
+ * The given source hierarchies are traversed and their nodes are added to the
+ * resulting structure. Under some circumstances two nodes can be combined
+ * rather than adding both. This is the case if both nodes are single children
+ * (no lists) of their parents and do not have values. The corresponding check
+ * is implemented in the {@code findCombineNode()} method.
+ * </p>
+ * <p>
+ * Sometimes it is not possible for this combiner to detect whether two nodes
+ * can be combined or not. Consider the following two node hierarchies:
+ * </p>
+ * <p>
+ *
+ * <pre>
+ * Hierarchy 1:
+ *
+ * Database
+ * +--Tables
+ * +--Table
+ * +--name [users]
+ * +--fields
+ * +--field
+ * | +--name [uid]
+ * +--field
+ * | +--name [usrname]
+ * ...
+ * </pre>
+ *
+ * </p>
+ * <p>
+ *
+ * <pre>
+ * Hierarchy 2:
+ *
+ * Database
+ * +--Tables
+ * +--Table
+ * +--name [documents]
+ * +--fields
+ * +--field
+ * | +--name [docid]
+ * +--field
+ * | +--name [docname]
+ * ...
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * Both hierarchies contain data about database tables. Each describes a single
+ * table. If these hierarchies are to be combined, the result should probably
+ * look like the following:
+ * <p>
+ *
+ * <pre>
+ * Database
+ * +--Tables
+ * +--Table
+ * | +--name [users]
+ * | +--fields
+ * | +--field
+ * | | +--name [uid]
+ * | ...
+ * +--Table
+ * +--name [documents]
+ * +--fields
+ * +--field
+ * | +--name [docid]
+ * ...
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * i.e. the {@code Tables} nodes should be combined, while the
+ * {@code Table} nodes should both be added to the resulting tree. From
+ * the combiner's point of view there is no difference between the
+ * {@code Tables} and the {@code Table} nodes in the source trees,
+ * so the developer has to help out and give a hint that the {@code Table}
+ * nodes belong to a list structure. This can be done using the
+ * {@code addListNode()} method; this method expects the name of a node,
+ * which should be treated as a list node. So if
+ * {@code addListNode("Table");} was called, the combiner knows that it
+ * must not combine the {@code Table} nodes, but add it both to the
+ * resulting tree.
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: UnionCombiner.java 1206486 2011-11-26 16:41:12Z oheger $
+ * @since 1.3
+ */
+public class UnionCombiner extends NodeCombiner
+{
+ /**
+ * Combines the given nodes to a new union node.
+ *
+ * @param node1 the first source node
+ * @param node2 the second source node
+ * @return the union node
+ */
+ @Override
+ public ConfigurationNode combine(ConfigurationNode node1,
+ ConfigurationNode node2)
+ {
+ ViewNode result = createViewNode();
+ result.setName(node1.getName());
+ result.appendAttributes(node1);
+ result.appendAttributes(node2);
+
+ // Check if nodes can be combined
+ List<ConfigurationNode> children2 = new LinkedList<ConfigurationNode>(node2.getChildren());
+ for (ConfigurationNode child1 : node1.getChildren())
+ {
+ ConfigurationNode child2 = findCombineNode(node1, node2, child1,
+ children2);
+ if (child2 != null)
+ {
+ result.addChild(combine(child1, child2));
+ children2.remove(child2);
+ }
+ else
+ {
+ result.addChild(child1);
+ }
+ }
+
+ // Add remaining children of node 2
+ for (ConfigurationNode c : children2)
+ {
+ result.addChild(c);
+ }
+
+ return result;
+ }
+
+ /**
+ * <p>
+ * Tries to find a child node of the second source node, with which a child
+ * of the first source node can be combined. During combining of the source
+ * nodes an iteration over the first source node's children is performed.
+ * For each child node it is checked whether a corresponding child node in
+ * the second source node exists. If this is the case, these corresponding
+ * child nodes are recursively combined and the result is added to the
+ * combined node. This method implements the checks whether such a recursive
+ * combination is possible. The actual implementation tests the following
+ * conditions:
+ * </p>
+ * <p>
+ * <ul>
+ * <li>In both the first and the second source node there is only one child
+ * node with the given name (no list structures).</li>
+ * <li>The given name is not in the list of known list nodes, i.e. it was
+ * not passed to the {@code addListNode()} method.</li>
+ * <li>None of these matching child nodes has a value.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * If all of these tests are successful, the matching child node of the
+ * second source node is returned. Otherwise the result is <b>null</b>.
+ * </p>
+ *
+ * @param node1 the first source node
+ * @param node2 the second source node
+ * @param child the child node of the first source node to be checked
+ * @param children a list with all children of the second source node
+ * @return the matching child node of the second source node or <b>null</b>
+ * if there is none
+ */
+ protected ConfigurationNode findCombineNode(ConfigurationNode node1,
+ ConfigurationNode node2, ConfigurationNode child, List<ConfigurationNode> children)
+ {
+ if (child.getValue() == null && !isListNode(child)
+ && node1.getChildrenCount(child.getName()) == 1
+ && node2.getChildrenCount(child.getName()) == 1)
+ {
+ ConfigurationNode child2 = node2.getChildren(
+ child.getName()).iterator().next();
+ if (child2.getValue() == null)
+ {
+ return child2;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/ViewNode.java b/src/main/java/org/apache/commons/configuration/tree/ViewNode.java
new file mode 100644
index 0000000..720f6e2
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/ViewNode.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+
+/**
+ * <p>
+ * A specialized node implementation to be used in view configurations.
+ * </p>
+ * <p>
+ * Some configurations provide a logical view on the nodes of other
+ * configurations. These configurations construct their own hierarchy of nodes
+ * based on the node trees of their source configurations. This special node
+ * class can be used for this purpose. It allows child nodes and attributes to
+ * be added without changing their parent node. So a node can belong to a
+ * hierarchy of nodes of a source configuration, but be also contained in a view
+ * configuration.
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ViewNode.java 1206488 2011-11-26 16:42:41Z oheger $
+ * @since 1.3
+ */
+public class ViewNode extends DefaultConfigurationNode
+{
+ /**
+ * Adds an attribute to this view node. The new attribute's parent node will
+ * be saved.
+ *
+ * @param attr the attribute node to be added
+ */
+ @Override
+ public void addAttribute(ConfigurationNode attr)
+ {
+ ConfigurationNode parent = null;
+
+ if (attr != null)
+ {
+ parent = attr.getParentNode();
+ super.addAttribute(attr);
+ attr.setParentNode(parent);
+ }
+ else
+ {
+ throw new IllegalArgumentException("Attribute node must not be null!");
+ }
+ }
+
+ /**
+ * Adds a child node to this view node. The new child's parent node will be
+ * saved.
+ *
+ * @param child the child node to be added
+ */
+ @Override
+ public void addChild(ConfigurationNode child)
+ {
+ ConfigurationNode parent = null;
+
+ if (child != null)
+ {
+ parent = child.getParentNode();
+ super.addChild(child);
+ child.setParentNode(parent);
+ }
+ else
+ {
+ throw new IllegalArgumentException("Child node must not be null!");
+ }
+ }
+
+ /**
+ * Adds all attribute nodes of the given source node to this view node.
+ *
+ * @param source the source node
+ */
+ public void appendAttributes(ConfigurationNode source)
+ {
+ if (source != null)
+ {
+ for (ConfigurationNode attr : source.getAttributes())
+ {
+ addAttribute(attr);
+ }
+ }
+ }
+
+ /**
+ * Adds all child nodes of the given source node to this view node.
+ *
+ * @param source the source node
+ */
+ public void appendChildren(ConfigurationNode source)
+ {
+ if (source != null)
+ {
+ for (ConfigurationNode child : source.getChildren())
+ {
+ addChild(child);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/package.html b/src/main/java/org/apache/commons/configuration/tree/package.html
new file mode 100644
index 0000000..9008f09
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/package.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+<head>
+</head>
+<body>
+
+<p>
+A package with helper and utility classes used by hierarchical configurations.
+</p>
+<p>
+<font size="-2">$Id: package.html 439648 2006-09-02 20:42:10Z oheger $</font>
+</p>
+
+</body>
+</html>
diff --git a/src/main/java/org/apache/commons/configuration/tree/xpath/ConfigurationNodeIteratorAttribute.java b/src/main/java/org/apache/commons/configuration/tree/xpath/ConfigurationNodeIteratorAttribute.java
new file mode 100644
index 0000000..c22efdf
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/xpath/ConfigurationNodeIteratorAttribute.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree.xpath;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.jxpath.ri.QName;
+import org.apache.commons.jxpath.ri.model.NodePointer;
+
+/**
+ * A specialized node iterator implementation that deals with attribute nodes.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationNodeIteratorAttribute.java 1206492 2011-11-26 16:52:16Z oheger $
+ */
+class ConfigurationNodeIteratorAttribute extends
+ ConfigurationNodeIteratorBase
+{
+ /** Constant for the wildcard node name.*/
+ private static final String WILDCARD = "*";
+
+ /**
+ * Creates a new instance of {@code ConfigurationNodeIteratorAttribute}.
+ * @param parent the parent node pointer
+ * @param name the name of the selected attribute
+ */
+ public ConfigurationNodeIteratorAttribute(NodePointer parent, QName name)
+ {
+ super(parent, false);
+ initSubNodeList(createSubNodeList((ConfigurationNode) parent.getNode(),
+ name));
+ }
+
+ /**
+ * Determines which attributes are selected based on the passed in node
+ * name.
+ * @param node the current node
+ * @param name the name of the selected attribute
+ * @return a list with the selected attributes
+ */
+ protected List<ConfigurationNode> createSubNodeList(ConfigurationNode node, QName name)
+ {
+ if (name.getPrefix() != null)
+ {
+ // namespace prefixes are not supported
+ return Collections.emptyList();
+ }
+
+ List<ConfigurationNode> result = new ArrayList<ConfigurationNode>();
+ if (!WILDCARD.equals(name.getName()))
+ {
+ result.addAll(node.getAttributes(name.getName()));
+ }
+ else
+ {
+ result.addAll(node.getAttributes());
+ }
+
+ return result;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/xpath/ConfigurationNodeIteratorBase.java b/src/main/java/org/apache/commons/configuration/tree/xpath/ConfigurationNodeIteratorBase.java
new file mode 100644
index 0000000..6b5e88b
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/xpath/ConfigurationNodeIteratorBase.java
@@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree.xpath;
+
+import java.util.List;
+
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.jxpath.ri.model.NodeIterator;
+import org.apache.commons.jxpath.ri.model.NodePointer;
+
+/**
+ * <p>
+ * A base class for implementing iterators over configuration nodes.
+ * </p>
+ * <p>
+ * This class already provides common functionality for implementing the
+ * iteration process. Derived classes will implement specific behavior based on
+ * the concrete node type (child node or attribute node).
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationNodeIteratorBase.java 1206491 2011-11-26 16:51:50Z oheger $
+ */
+abstract class ConfigurationNodeIteratorBase implements NodeIterator
+{
+ /** Stores the parent node pointer. */
+ private NodePointer parent;
+
+ /** Stores the list with the sub nodes. */
+ private List<ConfigurationNode> subNodes;
+
+ /** Stores the current position. */
+ private int position;
+
+ /** Stores the start offset of the iterator. */
+ private int startOffset;
+
+ /** Stores the reverse flag. */
+ private boolean reverse;
+
+ /**
+ * Creates a new instance of {@code ConfigurationNodeIteratorBase}
+ * and initializes it.
+ *
+ * @param parent the parent pointer
+ * @param reverse the reverse flag
+ */
+ protected ConfigurationNodeIteratorBase(NodePointer parent, boolean reverse)
+ {
+ this.parent = parent;
+ this.reverse = reverse;
+ }
+
+ /**
+ * Returns the position of the iteration.
+ *
+ * @return the position
+ */
+ public int getPosition()
+ {
+ return position;
+ }
+
+ /**
+ * Sets the position of the iteration.
+ *
+ * @param pos the new position
+ * @return a flag if this is a valid position
+ */
+ public boolean setPosition(int pos)
+ {
+ position = pos;
+ return pos >= 1 && pos <= getMaxPosition();
+ }
+
+ /**
+ * Returns the current node pointer.
+ *
+ * @return the current pointer in this iteration
+ */
+ public NodePointer getNodePointer()
+ {
+ if (getPosition() < 1 && !setPosition(1))
+ {
+ return null;
+ }
+
+ return createNodePointer(subNodes.get(positionToIndex(getPosition())));
+ }
+
+ /**
+ * Returns the parent node pointer.
+ *
+ * @return the parent node pointer
+ */
+ protected NodePointer getParent()
+ {
+ return parent;
+ }
+
+ /**
+ * Returns the start offset of the iteration.
+ *
+ * @return the start offset
+ */
+ protected int getStartOffset()
+ {
+ return startOffset;
+ }
+
+ /**
+ * Sets the start offset of the iteration. This is used when a start element
+ * was set.
+ *
+ * @param startOffset the start offset
+ */
+ protected void setStartOffset(int startOffset)
+ {
+ this.startOffset = startOffset;
+ if (reverse)
+ {
+ this.startOffset--;
+ }
+ else
+ {
+ this.startOffset++;
+ }
+ }
+
+ /**
+ * Initializes the list of sub nodes for the iteration. This method must be
+ * called during initialization phase.
+ *
+ * @param nodes the list with the sub nodes
+ */
+ protected void initSubNodeList(List<ConfigurationNode> nodes)
+ {
+ subNodes = nodes;
+ if (reverse)
+ {
+ setStartOffset(subNodes.size());
+ }
+ }
+
+ /**
+ * Returns the maximum position for this iterator.
+ *
+ * @return the maximum allowed position
+ */
+ protected int getMaxPosition()
+ {
+ return reverse ? getStartOffset() + 1 : subNodes.size()
+ - getStartOffset();
+ }
+
+ /**
+ * Creates the configuration node pointer for the current position. This
+ * method is called by {@code getNodePointer()}. Derived classes
+ * must create the correct pointer object.
+ *
+ * @param node the current configuration node
+ * @return the node pointer
+ */
+ protected NodePointer createNodePointer(ConfigurationNode node)
+ {
+ return new ConfigurationNodePointer(getParent(), node);
+ }
+
+ /**
+ * Returns the index in the data list for the given position. This method
+ * also checks the reverse flag.
+ *
+ * @param pos the position (1-based)
+ * @return the corresponding list index
+ */
+ protected int positionToIndex(int pos)
+ {
+ return (reverse ? 1 - pos : pos - 1) + getStartOffset();
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/xpath/ConfigurationNodeIteratorChildren.java b/src/main/java/org/apache/commons/configuration/tree/xpath/ConfigurationNodeIteratorChildren.java
new file mode 100644
index 0000000..9a39a39
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/xpath/ConfigurationNodeIteratorChildren.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree.xpath;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.jxpath.ri.Compiler;
+import org.apache.commons.jxpath.ri.QName;
+import org.apache.commons.jxpath.ri.compiler.NodeNameTest;
+import org.apache.commons.jxpath.ri.compiler.NodeTest;
+import org.apache.commons.jxpath.ri.compiler.NodeTypeTest;
+import org.apache.commons.jxpath.ri.model.NodePointer;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * A specialized iterator implementation for the child nodes of a configuration
+ * node.
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationNodeIteratorChildren.java 1206493 2011-11-26 16:56:42Z oheger $
+ */
+class ConfigurationNodeIteratorChildren extends ConfigurationNodeIteratorBase
+{
+ /**
+ * Creates a new instance of {@code ConfigurationNodeIteratorChildren}
+ * and initializes it.
+ *
+ * @param parent the parent pointer
+ * @param nodeTest the test selecting the sub nodes
+ * @param reverse the reverse flag
+ * @param startsWith the first element of the iteration
+ */
+ public ConfigurationNodeIteratorChildren(NodePointer parent,
+ NodeTest nodeTest, boolean reverse, NodePointer startsWith)
+ {
+ super(parent, reverse);
+ ConfigurationNode root = (ConfigurationNode) parent.getNode();
+ List<ConfigurationNode> childNodes = createSubNodeList(root, nodeTest);
+ initSubNodeList(childNodes);
+ if (startsWith != null)
+ {
+ setStartOffset(findStartIndex(root,
+ (ConfigurationNode) startsWith.getNode()));
+ }
+ }
+
+ /**
+ * Creates the list with sub nodes. This method gets called during
+ * initialization phase. It finds out, based on the given test, which nodes
+ * must be iterated over.
+ *
+ * @param node the current node
+ * @param test the test object
+ * @return a list with the matching nodes
+ */
+ protected List<ConfigurationNode> createSubNodeList(ConfigurationNode node, NodeTest test)
+ {
+ List<ConfigurationNode> children = node.getChildren();
+
+ if (test == null)
+ {
+ return children;
+ }
+ else
+ {
+ if (test instanceof NodeNameTest)
+ {
+ NodeNameTest nameTest = (NodeNameTest) test;
+ QName name = nameTest.getNodeName();
+ if (name.getPrefix() == null)
+ {
+ if (nameTest.isWildcard())
+ {
+ return children;
+ }
+
+ List<ConfigurationNode> result = new ArrayList<ConfigurationNode>();
+ for (ConfigurationNode child : children)
+ {
+ if (StringUtils.equals(name.getName(), child.getName()))
+ {
+ result.add(child);
+ }
+ }
+ return result;
+ }
+ }
+
+ else if (test instanceof NodeTypeTest)
+ {
+ NodeTypeTest typeTest = (NodeTypeTest) test;
+ if (typeTest.getNodeType() == Compiler.NODE_TYPE_NODE
+ || typeTest.getNodeType() == Compiler.NODE_TYPE_TEXT)
+ {
+ return children;
+ }
+ }
+ }
+
+ return Collections.emptyList();
+ }
+
+ /**
+ * Determines the start position of the iteration. Finds the index of the
+ * given start node in the children of the root node.
+ *
+ * @param node the root node
+ * @param startNode the start node
+ * @return the start node's index
+ */
+ protected int findStartIndex(ConfigurationNode node,
+ ConfigurationNode startNode)
+ {
+ for (int index = 0; index < node.getChildrenCount(); index++)
+ {
+ if (node.getChild(index) == startNode)
+ {
+ return index;
+ }
+ }
+
+ return -1;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/xpath/ConfigurationNodePointer.java b/src/main/java/org/apache/commons/configuration/tree/xpath/ConfigurationNodePointer.java
new file mode 100644
index 0000000..8a08f66
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/xpath/ConfigurationNodePointer.java
@@ -0,0 +1,270 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree.xpath;
+
+import java.util.List;
+import java.util.Locale;
+
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.jxpath.ri.Compiler;
+import org.apache.commons.jxpath.ri.QName;
+import org.apache.commons.jxpath.ri.compiler.NodeTest;
+import org.apache.commons.jxpath.ri.compiler.NodeTypeTest;
+import org.apache.commons.jxpath.ri.model.NodeIterator;
+import org.apache.commons.jxpath.ri.model.NodePointer;
+
+/**
+ * <p>
+ * A specific {@code NodePointer} implementation for configuration nodes.
+ * </p>
+ * <p>
+ * This is needed for queries using JXPath.
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationNodePointer.java 1206496 2011-11-26 17:01:14Z oheger $
+ */
+class ConfigurationNodePointer extends NodePointer
+{
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = -1087475639680007713L;
+
+ /** Stores the associated configuration node. */
+ private ConfigurationNode node;
+
+ /**
+ * Creates a new instance of {@code ConfigurationNodePointer}.
+ *
+ * @param node the node
+ * @param locale the locale
+ */
+ public ConfigurationNodePointer(ConfigurationNode node, Locale locale)
+ {
+ super(null, locale);
+ this.node = node;
+ }
+
+ /**
+ * Creates a new instance of {@code ConfigurationNodePointer} and
+ * initializes it with its parent pointer.
+ *
+ * @param parent the parent pointer
+ * @param node the associated node
+ */
+ public ConfigurationNodePointer(NodePointer parent, ConfigurationNode node)
+ {
+ super(parent);
+ this.node = node;
+ }
+
+ /**
+ * Returns a flag whether this node is a leaf. This is the case if there are
+ * no child nodes.
+ *
+ * @return a flag if this node is a leaf
+ */
+ @Override
+ public boolean isLeaf()
+ {
+ return node.getChildrenCount() < 1;
+ }
+
+ /**
+ * Returns a flag if this node is a collection. This is not the case.
+ *
+ * @return the collection flag
+ */
+ @Override
+ public boolean isCollection()
+ {
+ return false;
+ }
+
+ /**
+ * Returns this node's length. This is always 1.
+ *
+ * @return the node's length
+ */
+ @Override
+ public int getLength()
+ {
+ return 1;
+ }
+
+ /**
+ * Checks whether this node pointer refers to an attribute node. This method
+ * checks the attribute flag of the associated configuration node.
+ *
+ * @return the attribute flag
+ */
+ @Override
+ public boolean isAttribute()
+ {
+ return node.isAttribute();
+ }
+
+ /**
+ * Returns this node's name.
+ *
+ * @return the name
+ */
+ @Override
+ public QName getName()
+ {
+ return new QName(null, node.getName());
+ }
+
+ /**
+ * Returns this node's base value. This is the associated configuration
+ * node.
+ *
+ * @return the base value
+ */
+ @Override
+ public Object getBaseValue()
+ {
+ return node;
+ }
+
+ /**
+ * Returns the immediate node. This is the associated configuration node.
+ *
+ * @return the immediate node
+ */
+ @Override
+ public Object getImmediateNode()
+ {
+ return node;
+ }
+
+ /**
+ * Returns the value of this node.
+ *
+ * @return the represented node's value
+ */
+ @Override
+ public Object getValue()
+ {
+ return node.getValue();
+ }
+
+ /**
+ * Sets the value of this node.
+ *
+ * @param value the new value
+ */
+ @Override
+ public void setValue(Object value)
+ {
+ node.setValue(value);
+ }
+
+ /**
+ * Compares two child node pointers.
+ *
+ * @param pointer1 one pointer
+ * @param pointer2 another pointer
+ * @return a flag, which pointer should be sorted first
+ */
+ @Override
+ public int compareChildNodePointers(NodePointer pointer1,
+ NodePointer pointer2)
+ {
+ ConfigurationNode node1 = (ConfigurationNode) pointer1.getBaseValue();
+ ConfigurationNode node2 = (ConfigurationNode) pointer2.getBaseValue();
+
+ // attributes will be sorted before child nodes
+ if (node1.isAttribute() && !node2.isAttribute())
+ {
+ return -1;
+ }
+ else if (node2.isAttribute() && !node1.isAttribute())
+ {
+ return 1;
+ }
+
+ else
+ {
+ // sort based on the occurrence in the sub node list
+ List<ConfigurationNode> subNodes = node1.isAttribute() ? node.getAttributes() : node
+ .getChildren();
+ for (ConfigurationNode child : subNodes)
+ {
+ if (child == node1)
+ {
+ return -1;
+ }
+ else if (child == node2)
+ {
+ return 1;
+ }
+ }
+ return 0; // should not happen
+ }
+ }
+
+ /**
+ * Returns an iterator for the attributes that match the given name.
+ *
+ * @param name the attribute name
+ * @return the iterator for the attributes
+ */
+ @Override
+ public NodeIterator attributeIterator(QName name)
+ {
+ return new ConfigurationNodeIteratorAttribute(this, name);
+ }
+
+ /**
+ * Returns an iterator for the children of this pointer that match the given
+ * test object.
+ *
+ * @param test the test object
+ * @param reverse the reverse flag
+ * @param startWith the start value of the iteration
+ */
+ @Override
+ public NodeIterator childIterator(NodeTest test, boolean reverse,
+ NodePointer startWith)
+ {
+ return new ConfigurationNodeIteratorChildren(this, test, reverse,
+ startWith);
+ }
+
+ /**
+ * Tests if this node matches the given test. Configuration nodes are text
+ * nodes, too because they can contain a value.
+ *
+ * @param test the test object
+ * @return a flag if this node corresponds to the test
+ */
+ @Override
+ public boolean testNode(NodeTest test)
+ {
+ if (test instanceof NodeTypeTest
+ && ((NodeTypeTest) test).getNodeType() == Compiler.NODE_TYPE_TEXT)
+ {
+ return true;
+ }
+ return super.testNode(test);
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/xpath/ConfigurationNodePointerFactory.java b/src/main/java/org/apache/commons/configuration/tree/xpath/ConfigurationNodePointerFactory.java
new file mode 100644
index 0000000..b7d7da0
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/xpath/ConfigurationNodePointerFactory.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree.xpath;
+
+import java.util.Locale;
+
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.jxpath.ri.QName;
+import org.apache.commons.jxpath.ri.model.NodePointer;
+import org.apache.commons.jxpath.ri.model.NodePointerFactory;
+
+/**
+ * Implementation of the {@code NodePointerFactory} interface for
+ * configuration nodes.
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationNodePointerFactory.java 1206498 2011-11-26 17:01:58Z oheger $
+ */
+public class ConfigurationNodePointerFactory implements NodePointerFactory
+{
+ /** Constant for the order of this factory. */
+ public static final int CONFIGURATION_NODE_POINTER_FACTORY_ORDER = 200;
+
+ /**
+ * Returns the order of this factory between other factories.
+ *
+ * @return this order's factory
+ */
+ public int getOrder()
+ {
+ return CONFIGURATION_NODE_POINTER_FACTORY_ORDER;
+ }
+
+ /**
+ * Creates a node pointer for the specified bean. If the bean is a
+ * configuration node, a corresponding pointer is returned.
+ *
+ * @param name the name of the node
+ * @param bean the bean
+ * @param locale the locale
+ * @return a pointer for a configuration node if the bean is such a node
+ */
+ public NodePointer createNodePointer(QName name, Object bean, Locale locale)
+ {
+ if (bean instanceof ConfigurationNode)
+ {
+ return new ConfigurationNodePointer((ConfigurationNode) bean,
+ locale);
+ }
+ return null;
+ }
+
+ /**
+ * Creates a node pointer for the specified bean. If the bean is a
+ * configuration node, a corresponding pointer is returned.
+ *
+ * @param parent the parent node
+ * @param name the name
+ * @param bean the bean
+ * @return a pointer for a configuration node if the bean is such a node
+ */
+ public NodePointer createNodePointer(NodePointer parent, QName name,
+ Object bean)
+ {
+ if (bean instanceof ConfigurationNode)
+ {
+ return new ConfigurationNodePointer(parent,
+ (ConfigurationNode) bean);
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/xpath/XPathExpressionEngine.java b/src/main/java/org/apache/commons/configuration/tree/xpath/XPathExpressionEngine.java
new file mode 100644
index 0000000..a169a36
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/xpath/XPathExpressionEngine.java
@@ -0,0 +1,426 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree.xpath;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.ExpressionEngine;
+import org.apache.commons.configuration.tree.NodeAddData;
+import org.apache.commons.jxpath.JXPathContext;
+import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * <p>
+ * A specialized implementation of the {@code ExpressionEngine} interface
+ * that is able to evaluate XPATH expressions.
+ * </p>
+ * <p>
+ * This class makes use of <a href="http://commons.apache.org/jxpath/"> Commons
+ * JXPath</a> for handling XPath expressions and mapping them to the nodes of a
+ * hierarchical configuration. This makes the rich and powerful XPATH syntax
+ * available for accessing properties from a configuration object.
+ * </p>
+ * <p>
+ * For selecting properties arbitrary XPATH expressions can be used, which
+ * select single or multiple configuration nodes. The associated
+ * {@code Configuration} instance will directly pass the specified property
+ * keys into this engine. If a key is not syntactically correct, an exception
+ * will be thrown.
+ * </p>
+ * <p>
+ * For adding new properties, this expression engine uses a specific syntax: the
+ * "key" of a new property must consist of two parts that are
+ * separated by whitespace:
+ * <ol>
+ * <li>An XPATH expression selecting a single node, to which the new element(s)
+ * are to be added. This can be an arbitrary complex expression, but it must
+ * select exactly one node, otherwise an exception will be thrown.</li>
+ * <li>The name of the new element(s) to be added below this parent node. Here
+ * either a single node name or a complete path of nodes (separated by the
+ * "/" character or "@" for an attribute) can be specified.</li>
+ * </ol>
+ * Some examples for valid keys that can be passed into the configuration's
+ * {@code addProperty()} method follow:
+ * </p>
+ * <p>
+ *
+ * <pre>
+ * "/tables/table[1] type"
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * This will add a new {@code type} node as a child of the first
+ * {@code table} element.
+ * </p>
+ * <p>
+ *
+ * <pre>
+ * "/tables/table[1] @type"
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * Similar to the example above, but this time a new attribute named
+ * {@code type} will be added to the first {@code table} element.
+ * </p>
+ * <p>
+ *
+ * <pre>
+ * "/tables table/fields/field/name"
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * This example shows how a complex path can be added. Parent node is the
+ * {@code tables} element. Here a new branch consisting of the nodes
+ * {@code table}, {@code fields}, {@code field}, and
+ * {@code name} will be added.
+ * </p>
+ * <p>
+ *
+ * <pre>
+ * "/tables table/fields/field at type"
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * This is similar to the last example, but in this case a complex path ending
+ * with an attribute is defined.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> This extended syntax for adding properties only works
+ * with the {@code addProperty()} method. {@code setProperty()} does
+ * not support creating new nodes this way.
+ * </p>
+ * <p>
+ * From version 1.7 on, it is possible to use regular keys in calls to
+ * {@code addProperty()} (i.e. keys that do not have to contain a
+ * whitespace as delimiter). In this case the key is evaluated, and the biggest
+ * part pointing to an existing node is determined. The remaining part is then
+ * added as new path. As an example consider the key
+ *
+ * <pre>
+ * "tables/table[last()]/fields/field/name"
+ * </pre>
+ *
+ * If the key does not point to an existing node, the engine will check the
+ * paths {@code "tables/table[last()]/fields/field"},
+ * {@code "tables/table[last()]/fields"},
+ * {@code "tables/table[last()]"}, and so on, until a key is
+ * found which points to a node. Let's assume that the last key listed above can
+ * be resolved in this way. Then from this key the following key is derived:
+ * {@code "tables/table[last()] fields/field/name"} by appending
+ * the remaining part after a whitespace. This key can now be processed using
+ * the original algorithm. Keys of this form can also be used with the
+ * {@code setProperty()} method. However, it is still recommended to use
+ * the old format because it makes explicit at which position new nodes should
+ * be added. For keys without a whitespace delimiter there may be ambiguities.
+ * </p>
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: XPathExpressionEngine.java 1206563 2011-11-26 19:47:26Z oheger $
+ */
+public class XPathExpressionEngine implements ExpressionEngine
+{
+ /** Constant for the path delimiter. */
+ static final String PATH_DELIMITER = "/";
+
+ /** Constant for the attribute delimiter. */
+ static final String ATTR_DELIMITER = "@";
+
+ /** Constant for the delimiters for splitting node paths. */
+ private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER
+ + ATTR_DELIMITER;
+
+ /**
+ * Constant for a space which is used as delimiter in keys for adding
+ * properties.
+ */
+ private static final String SPACE = " ";
+
+ /**
+ * Executes a query. The passed in property key is directly passed to a
+ * JXPath context.
+ *
+ * @param root the configuration root node
+ * @param key the query to be executed
+ * @return a list with the nodes that are selected by the query
+ */
+ public List<ConfigurationNode> query(ConfigurationNode root, String key)
+ {
+ if (StringUtils.isEmpty(key))
+ {
+ return Collections.singletonList(root);
+ }
+ else
+ {
+ JXPathContext context = createContext(root, key);
+ // This is safe because our node pointer implementations will return
+ // a list of configuration nodes.
+ @SuppressWarnings("unchecked")
+ List<ConfigurationNode> result = context.selectNodes(key);
+ if (result == null)
+ {
+ result = Collections.emptyList();
+ }
+ return result;
+ }
+ }
+
+ /**
+ * Returns a (canonical) key for the given node based on the parent's key.
+ * This implementation will create an XPATH expression that selects the
+ * given node (under the assumption that the passed in parent key is valid).
+ * As the {@code nodeKey()} implementation of
+ * {@link org.apache.commons.configuration.tree.DefaultExpressionEngine DefaultExpressionEngine}
+ * this method will not return indices for nodes. So all child nodes of a
+ * given parent with the same name will have the same key.
+ *
+ * @param node the node for which a key is to be constructed
+ * @param parentKey the key of the parent node
+ * @return the key for the given node
+ */
+ public String nodeKey(ConfigurationNode node, String parentKey)
+ {
+ if (parentKey == null)
+ {
+ // name of the root node
+ return StringUtils.EMPTY;
+ }
+ else if (node.getName() == null)
+ {
+ // paranoia check for undefined node names
+ return parentKey;
+ }
+
+ else
+ {
+ StringBuilder buf = new StringBuilder(parentKey.length()
+ + node.getName().length() + PATH_DELIMITER.length());
+ if (parentKey.length() > 0)
+ {
+ buf.append(parentKey);
+ buf.append(PATH_DELIMITER);
+ }
+ if (node.isAttribute())
+ {
+ buf.append(ATTR_DELIMITER);
+ }
+ buf.append(node.getName());
+ return buf.toString();
+ }
+ }
+
+ /**
+ * Prepares an add operation for a configuration property. The expected
+ * format of the passed in key is explained in the class comment.
+ *
+ * @param root the configuration's root node
+ * @param key the key describing the target of the add operation and the
+ * path of the new node
+ * @return a data object to be evaluated by the calling configuration object
+ */
+ public NodeAddData prepareAdd(ConfigurationNode root, String key)
+ {
+ if (key == null)
+ {
+ throw new IllegalArgumentException(
+ "prepareAdd: key must not be null!");
+ }
+
+ String addKey = key;
+ int index = findKeySeparator(addKey);
+ if (index < 0)
+ {
+ addKey = generateKeyForAdd(root, addKey);
+ index = findKeySeparator(addKey);
+ }
+
+ List<ConfigurationNode> nodes = query(root, addKey.substring(0, index).trim());
+ if (nodes.size() != 1)
+ {
+ throw new IllegalArgumentException(
+ "prepareAdd: key must select exactly one target node!");
+ }
+
+ NodeAddData data = new NodeAddData();
+ data.setParent(nodes.get(0));
+ initNodeAddData(data, addKey.substring(index).trim());
+ return data;
+ }
+
+ /**
+ * Creates the {@code JXPathContext} used for executing a query. This
+ * method will create a new context and ensure that it is correctly
+ * initialized.
+ *
+ * @param root the configuration root node
+ * @param key the key to be queried
+ * @return the new context
+ */
+ protected JXPathContext createContext(ConfigurationNode root, String key)
+ {
+ JXPathContext context = JXPathContext.newContext(root);
+ context.setLenient(true);
+ return context;
+ }
+
+ /**
+ * Initializes most properties of a {@code NodeAddData} object. This
+ * method is called by {@code prepareAdd()} after the parent node has
+ * been found. Its task is to interpret the passed in path of the new node.
+ *
+ * @param data the data object to initialize
+ * @param path the path of the new node
+ */
+ protected void initNodeAddData(NodeAddData data, String path)
+ {
+ String lastComponent = null;
+ boolean attr = false;
+ boolean first = true;
+
+ StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
+ true);
+ while (tok.hasMoreTokens())
+ {
+ String token = tok.nextToken();
+ if (PATH_DELIMITER.equals(token))
+ {
+ if (attr)
+ {
+ invalidPath(path, " contains an attribute"
+ + " delimiter at an unallowed position.");
+ }
+ if (lastComponent == null)
+ {
+ invalidPath(path,
+ " contains a '/' at an unallowed position.");
+ }
+ data.addPathNode(lastComponent);
+ lastComponent = null;
+ }
+
+ else if (ATTR_DELIMITER.equals(token))
+ {
+ if (attr)
+ {
+ invalidPath(path,
+ " contains multiple attribute delimiters.");
+ }
+ if (lastComponent == null && !first)
+ {
+ invalidPath(path,
+ " contains an attribute delimiter at an unallowed position.");
+ }
+ if (lastComponent != null)
+ {
+ data.addPathNode(lastComponent);
+ }
+ attr = true;
+ lastComponent = null;
+ }
+
+ else
+ {
+ lastComponent = token;
+ }
+ first = false;
+ }
+
+ if (lastComponent == null)
+ {
+ invalidPath(path, "contains no components.");
+ }
+ data.setNewNodeName(lastComponent);
+ data.setAttribute(attr);
+ }
+
+ /**
+ * Tries to generate a key for adding a property. This method is called if a
+ * key was used for adding properties which does not contain a space
+ * character. It splits the key at its single components and searches for
+ * the last existing component. Then a key compatible for adding properties
+ * is generated.
+ *
+ * @param root the root node of the configuration
+ * @param key the key in question
+ * @return the key to be used for adding the property
+ */
+ private String generateKeyForAdd(ConfigurationNode root, String key)
+ {
+ int pos = key.lastIndexOf(PATH_DELIMITER, key.length());
+
+ while (pos >= 0)
+ {
+ String keyExisting = key.substring(0, pos);
+ if (!query(root, keyExisting).isEmpty())
+ {
+ StringBuilder buf = new StringBuilder(key.length() + 1);
+ buf.append(keyExisting).append(SPACE);
+ buf.append(key.substring(pos + 1));
+ return buf.toString();
+ }
+ pos = key.lastIndexOf(PATH_DELIMITER, pos - 1);
+ }
+
+ return SPACE + key;
+ }
+
+ /**
+ * Helper method for throwing an exception about an invalid path.
+ *
+ * @param path the invalid path
+ * @param msg the exception message
+ */
+ private void invalidPath(String path, String msg)
+ {
+ throw new IllegalArgumentException("Invalid node path: \"" + path
+ + "\" " + msg);
+ }
+
+ /**
+ * Determines the position of the separator in a key for adding new
+ * properties. If no delimiter is found, result is -1.
+ *
+ * @param key the key
+ * @return the position of the delimiter
+ */
+ private static int findKeySeparator(String key)
+ {
+ int index = key.length() - 1;
+ while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
+ {
+ index--;
+ }
+ return index;
+ }
+
+ // static initializer: registers the configuration node pointer factory
+ static
+ {
+ JXPathContextReferenceImpl
+ .addNodePointerFactory(new ConfigurationNodePointerFactory());
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/tree/xpath/package.html b/src/main/java/org/apache/commons/configuration/tree/xpath/package.html
new file mode 100644
index 0000000..c9b63eb
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/tree/xpath/package.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+<head>
+</head>
+<body>
+
+<p>
+This package contains the implementation of the XPathExpressionEngine class, which
+enables XPATH support for querying configuration properties.
+</p>
+<p>
+<font size="-2">$Id: package.html 439648 2006-09-02 20:42:10Z oheger $</font>
+</p>
+
+</body>
+</html>
diff --git a/src/main/java/org/apache/commons/configuration/web/AppletConfiguration.java b/src/main/java/org/apache/commons/configuration/web/AppletConfiguration.java
new file mode 100644
index 0000000..637cb07
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/web/AppletConfiguration.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.web;
+
+import java.applet.Applet;
+import java.util.Arrays;
+import java.util.Iterator;
+
+/**
+ * A configuration wrapper to read applet parameters. This configuration is
+ * read only, adding or removing a property will throw an
+ * UnsupportedOperationException.
+ *
+ * @author <a href="mailto:ebourg at apache.org">Emmanuel Bourg</a>
+ * @version $Id: AppletConfiguration.java 1211124 2011-12-06 20:56:16Z oheger $
+ * @since 1.1
+ */
+public class AppletConfiguration extends BaseWebConfiguration
+{
+ /** Stores the wrapped applet.*/
+ protected Applet applet;
+
+ /**
+ * Create an AppletConfiguration using the initialization parameters of
+ * the specified Applet.
+ *
+ * @param applet the applet
+ */
+ public AppletConfiguration(Applet applet)
+ {
+ this.applet = applet;
+ }
+
+ public Object getProperty(String key)
+ {
+ return handleDelimiters(applet.getParameter(key));
+ }
+
+ public Iterator<String> getKeys()
+ {
+ String[][] paramsInfo = applet.getParameterInfo();
+ String[] keys = new String[paramsInfo != null ? paramsInfo.length : 0];
+ for (int i = 0; i < keys.length; i++)
+ {
+ keys[i] = paramsInfo[i][0];
+ }
+
+ return Arrays.asList(keys).iterator();
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/web/BaseWebConfiguration.java b/src/main/java/org/apache/commons/configuration/web/BaseWebConfiguration.java
new file mode 100644
index 0000000..49be510
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/web/BaseWebConfiguration.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.web;
+
+import java.util.List;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.PropertyConverter;
+
+/**
+ * <p>
+ * An abstract base class for all web configurations.
+ * </p>
+ * <p>
+ * This class implements common functionality used by all web based
+ * configurations. E.g. some methods are not supported by configurations of this
+ * type, so they throw a {@code UnsupportedOperationException} exception.
+ * </p>
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: BaseWebConfiguration.java 1211122 2011-12-06 20:55:45Z oheger $
+ * @since 1.2
+ */
+abstract class BaseWebConfiguration extends AbstractConfiguration
+{
+ /**
+ * Checks if this configuration is empty. This implementation makes use of
+ * the {@code getKeys()} method (which must be defined by concrete
+ * sub classes) to find out whether properties exist.
+ *
+ * @return a flag whether this configuration is empty
+ */
+ public boolean isEmpty()
+ {
+ return !getKeys().hasNext();
+ }
+
+ /**
+ * Checks whether the specified key is stored in this configuration.
+ *
+ * @param key the key
+ * @return a flag whether this key exists in this configuration
+ */
+ public boolean containsKey(String key)
+ {
+ return getProperty(key) != null;
+ }
+
+ /**
+ * Removes the property with the given key. <strong>This operation is not
+ * supported and will throw an UnsupportedOperationException.</strong>
+ *
+ * @param key the key of the property to be removed
+ * @throws UnsupportedOperationException because this operation is not
+ * allowed
+ */
+ @Override
+ public void clearProperty(String key)
+ {
+ throw new UnsupportedOperationException("Read only configuration");
+ }
+
+ /**
+ * Adds a property to this configuration. <strong>This operation is not
+ * supported and will throw an UnsupportedOperationException.</strong>
+ *
+ * @param key the key of the property
+ * @param obj the value to be added
+ * @throws UnsupportedOperationException because this operation is not
+ * allowed
+ */
+ @Override
+ protected void addPropertyDirect(String key, Object obj)
+ {
+ throw new UnsupportedOperationException("Read only configuration");
+ }
+
+ /**
+ * Takes care of list delimiters in property values. This method checks if
+ * delimiter parsing is enabled and the passed in value contains a delimiter
+ * character. If this is the case, a split operation is performed.
+ *
+ * @param value the property value to be examined
+ * @return the processed value
+ */
+ protected Object handleDelimiters(Object value)
+ {
+ if (!isDelimiterParsingDisabled() && value instanceof String)
+ {
+ List<String> list = PropertyConverter.split((String) value,
+ getListDelimiter());
+ value = list.size() > 1 ? list : list.get(0);
+ }
+
+ return value;
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/web/ServletConfiguration.java b/src/main/java/org/apache/commons/configuration/web/ServletConfiguration.java
new file mode 100644
index 0000000..e1742d7
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/web/ServletConfiguration.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.web;
+
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletConfig;
+
+/**
+ * A configuration wrapper around a {@link ServletConfig}. This configuration
+ * is read only, adding or removing a property will throw an
+ * UnsupportedOperationException.
+ *
+ * @author <a href="mailto:ebourg at apache.org">Emmanuel Bourg</a>
+ * @version $Id: ServletConfiguration.java 1211128 2011-12-06 20:56:41Z oheger $
+ * @since 1.1
+ */
+public class ServletConfiguration extends BaseWebConfiguration
+{
+ /** Stores a reference to the wrapped {@code ServletConfig}.*/
+ protected ServletConfig config;
+
+ /**
+ * Create a ServletConfiguration using the initialization parameter of
+ * the specified servlet.
+ *
+ * @param servlet the servlet
+ */
+ public ServletConfiguration(Servlet servlet)
+ {
+ this(servlet.getServletConfig());
+ }
+
+ /**
+ * Create a ServletConfiguration using the servlet initialization parameters.
+ *
+ * @param config the servlet configuration
+ */
+ public ServletConfiguration(ServletConfig config)
+ {
+ this.config = config;
+ }
+
+ public Object getProperty(String key)
+ {
+ return handleDelimiters(config.getInitParameter(key));
+ }
+
+ public Iterator<String> getKeys()
+ {
+ // According to the documentation of getInitParameterNames() the
+ // enumeration is of type String.
+ @SuppressWarnings("unchecked")
+ Enumeration<String> en = config.getInitParameterNames();
+ return Collections.list(en).iterator();
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/web/ServletContextConfiguration.java b/src/main/java/org/apache/commons/configuration/web/ServletContextConfiguration.java
new file mode 100644
index 0000000..e826e9e
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/web/ServletContextConfiguration.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.web;
+
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletContext;
+
+/**
+ * A configuration wrapper to read the initialization parameters of a servlet
+ * context. This configuration is read only, adding or removing a property will
+ * throw an UnsupportedOperationException.
+ *
+ * @author <a href="mailto:ebourg at apache.org">Emmanuel Bourg</a>
+ * @version $Id: ServletContextConfiguration.java 1211129 2011-12-06 20:57:01Z oheger $
+ * @since 1.1
+ */
+public class ServletContextConfiguration extends BaseWebConfiguration
+{
+ /** Stores the wrapped servlet context.*/
+ protected ServletContext context;
+
+ /**
+ * Create a ServletContextConfiguration using the context of
+ * the specified servlet.
+ *
+ * @param servlet the servlet
+ */
+ public ServletContextConfiguration(Servlet servlet)
+ {
+ this.context = servlet.getServletConfig().getServletContext();
+ }
+
+ /**
+ * Create a ServletContextConfiguration using the servlet context
+ * initialization parameters.
+ *
+ * @param context the servlet context
+ */
+ public ServletContextConfiguration(ServletContext context)
+ {
+ this.context = context;
+ }
+
+ public Object getProperty(String key)
+ {
+ return handleDelimiters(context.getInitParameter(key));
+ }
+
+ public Iterator<String> getKeys()
+ {
+ // According to the documentation of getInitParameterNames() the
+ // enumeration is of type String.
+ @SuppressWarnings("unchecked")
+ Enumeration<String> en = context.getInitParameterNames();
+ return Collections.list(en).iterator();
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/web/ServletFilterConfiguration.java b/src/main/java/org/apache/commons/configuration/web/ServletFilterConfiguration.java
new file mode 100644
index 0000000..b52a771
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/web/ServletFilterConfiguration.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.web;
+
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+
+import javax.servlet.FilterConfig;
+
+/**
+ * A configuration wrapper around a {@link FilterConfig}. This configuration is
+ * read only, adding or removing a property will throw an
+ * UnsupportedOperationException.
+ *
+ * @author <a href="mailto:ebourg at apache.org">Emmanuel Bourg</a>
+ * @version $Id: ServletFilterConfiguration.java 1211130 2011-12-06 20:57:19Z oheger $
+ * @since 1.1
+ */
+public class ServletFilterConfiguration extends BaseWebConfiguration
+{
+ /** Stores the wrapped filter config.*/
+ protected FilterConfig config;
+
+ /**
+ * Create a ServletFilterConfiguration using the filter initialization parameters.
+ *
+ * @param config the filter configuration
+ */
+ public ServletFilterConfiguration(FilterConfig config)
+ {
+ this.config = config;
+ }
+
+ public Object getProperty(String key)
+ {
+ return handleDelimiters(config.getInitParameter(key));
+ }
+
+ public Iterator<String> getKeys()
+ {
+ // According to the documentation of getInitParameterNames() the
+ // enumeration is of type String.
+ @SuppressWarnings("unchecked")
+ Enumeration<String> en = config.getInitParameterNames();
+ return Collections.list(en).iterator();
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/web/ServletRequestConfiguration.java b/src/main/java/org/apache/commons/configuration/web/ServletRequestConfiguration.java
new file mode 100644
index 0000000..6c6a909
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/web/ServletRequestConfiguration.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.web;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.ServletRequest;
+
+/**
+ * A configuration wrapper to read the parameters of a servlet request. This
+ * configuration is read only, adding or removing a property will throw an
+ * UnsupportedOperationException.
+ *
+ * @author <a href="mailto:ebourg at apache.org">Emmanuel Bourg</a>
+ * @version $Id: ServletRequestConfiguration.java 1211131 2011-12-06 20:57:37Z oheger $
+ * @since 1.1
+ */
+public class ServletRequestConfiguration extends BaseWebConfiguration
+{
+ /** Stores the wrapped request.*/
+ protected ServletRequest request;
+
+ /**
+ * Create a ServletRequestConfiguration using the request parameters.
+ *
+ * @param request the servlet request
+ */
+ public ServletRequestConfiguration(ServletRequest request)
+ {
+ this.request = request;
+ }
+
+ public Object getProperty(String key)
+ {
+ String[] values = request.getParameterValues(key);
+
+ if (values == null || values.length == 0)
+ {
+ return null;
+ }
+ else if (values.length == 1)
+ {
+ return handleDelimiters(values[0]);
+ }
+ else
+ {
+ // ensure that escape characters in all list elements are removed
+ List<Object> result = new ArrayList<Object>(values.length);
+ for (int i = 0; i < values.length; i++)
+ {
+ Object val = handleDelimiters(values[i]);
+ if (val instanceof Collection)
+ {
+ result.addAll((Collection<?>) val);
+ }
+ else
+ {
+ result.add(val);
+ }
+ }
+ return result;
+ }
+ }
+
+ public Iterator<String> getKeys()
+ {
+ // According to the documentation of getParameterMap(), keys are Strings.
+ @SuppressWarnings("unchecked")
+ Map<String, ?> parameterMap = request.getParameterMap();
+ return parameterMap.keySet().iterator();
+ }
+}
diff --git a/src/main/java/org/apache/commons/configuration/web/package.html b/src/main/java/org/apache/commons/configuration/web/package.html
new file mode 100644
index 0000000..744e7c4
--- /dev/null
+++ b/src/main/java/org/apache/commons/configuration/web/package.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+<head>
+</head>
+<body>
+
+<p>
+This package contains some implementations of the <code>Configuration</code>
+interface that are useful in web environments.
+</p>
+<p>
+<font size="-2">$Id: package.html 439648 2006-09-02 20:42:10Z oheger $</font>
+</p>
+
+</body>
+</html>
diff --git a/src/main/javacc/PropertyListParser.jj b/src/main/javacc/PropertyListParser.jj
new file mode 100644
index 0000000..eaf1567
--- /dev/null
+++ b/src/main/javacc/PropertyListParser.jj
@@ -0,0 +1,291 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+options {
+ STATIC = false;
+}
+
+
+PARSER_BEGIN(PropertyListParser)
+
+package org.apache.commons.configuration.plist;
+
+import java.util.Date;
+import java.util.List;
+import java.util.ArrayList;
+
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.apache.commons.configuration.HierarchicalConfiguration.Node;
+
+import org.apache.commons.codec.binary.Hex;
+
+/**
+ * JavaCC based parser for the PropertyList format.
+ *
+ * @author Emmanuel Bourg
+ * @version $Revision$, $Date$
+ */
+class PropertyListParser {
+
+ /**
+ * Remove the quotes at the beginning and at the end of the specified String.
+ */
+ protected String removeQuotes(String s)
+ {
+ if (s == null)
+ {
+ return null;
+ }
+
+ if (s.startsWith("\"") && s.endsWith("\"") && s.length() >= 2)
+ {
+ s = s.substring(1, s.length() - 1);
+ }
+
+ return s;
+ }
+
+ protected String unescapeQuotes(String s)
+ {
+ return s.replaceAll("\\\\\"", "\"");
+ }
+
+ /**
+ * Remove the white spaces and the data delimiters from the specified
+ * string and parse it as a byte array.
+ */
+ protected byte[] filterData(String s) throws ParseException
+ {
+ if (s == null)
+ {
+ return null;
+ }
+
+ // remove the delimiters
+ if (s.startsWith("<") && s.endsWith(">") && s.length() >= 2)
+ {
+ s = s.substring(1, s.length() - 1);
+ }
+
+ // remove the white spaces
+ s = s.replaceAll("\\s", "");
+
+ // add a leading 0 to ensure well formed bytes
+ if (s.length() % 2 != 0)
+ {
+ s = "0" + s;
+ }
+
+ // parse and return the bytes
+ try
+ {
+ return Hex.decodeHex(s.toCharArray());
+ }
+ catch (Exception e)
+ {
+ throw (ParseException) new ParseException("Unable to parse the byte[] : " + e.getMessage());
+ }
+ }
+
+ /**
+ * Parse a date formatted as <*D2002-03-22 11:30:00 +0100>
+ */
+ protected Date parseDate(String s) throws ParseException
+ {
+ return PropertyListConfiguration.parseDate(s);
+ }
+
+}
+
+PARSER_END(PropertyListParser)
+
+SKIP : { " " | "\t" | "\n" | "\r" }
+
+// Handle comments
+MORE : { "/*": IN_COMMENT }
+< IN_COMMENT > MORE : { < ~[] > }
+< IN_COMMENT > SKIP : { "*/": DEFAULT }
+
+MORE : { "//": IN_SINGLE_LINE_COMMENT }
+< IN_SINGLE_LINE_COMMENT > SPECIAL_TOKEN : {
+ < SINGLE_LINE_COMMENT: "\n"|"\r"|"\r\n" > : DEFAULT }
+< IN_SINGLE_LINE_COMMENT > MORE : { < ~[] > }
+
+TOKEN : { <ARRAY_BEGIN : "(" > }
+TOKEN : { <ARRAY_END : ")" > }
+TOKEN : { <ARRAY_SEPARATOR : "," > }
+
+TOKEN : { <DICT_BEGIN : "{" > }
+TOKEN : { <DICT_END : "}" > }
+TOKEN : { <DICT_SEPARATOR : ";" > }
+TOKEN : { <EQUAL : "=" > }
+
+TOKEN : { <DATA_START : "<" > }
+TOKEN : { <DATA_END : ">" > }
+
+TOKEN : { <DATE_START : "<*D" > }
+
+TOKEN : { < QUOTE : "\"" > }
+TOKEN : { < #LETTER : ~[" ", "\t", "\n", "\r", "(", ")", ",", "{", "}", ";", "=", "\""] > }
+TOKEN : { < #WHITE : " " | "\t" | "\n" | "\r" > }
+TOKEN : { < #HEXA : ["0"-"9", "a"-"f", "A"-"F"] > }
+TOKEN : { < DATA : <DATA_START> (<HEXA> | <WHITE>)* <DATA_END> > }
+TOKEN : { < DATE : <DATE_START> (["0"-"9"] | ":" | " " | "+" | "-" | "Z")* <DATA_END> > }
+TOKEN : { < STRING : (<LETTER>)+ > }
+TOKEN : { < QUOTED_STRING :
+ <QUOTE>
+ (<LETTER> | <WHITE> | <ESCAPED_QUOTE> | <EQUAL>
+ | <ARRAY_BEGIN> | <ARRAY_END> | <ARRAY_SEPARATOR>
+ | <DICT_BEGIN> | <DICT_END> | <DICT_SEPARATOR>)* <QUOTE> > }
+TOKEN : { < ESCAPED_QUOTE : "\\\"" > }
+
+PropertyListConfiguration parse() :
+{
+ PropertyListConfiguration configuration = null;
+}
+{
+ configuration = Dictionary()
+ <EOF>
+ { return configuration; }
+}
+
+PropertyListConfiguration Dictionary() :
+{
+ PropertyListConfiguration configuration = new PropertyListConfiguration();
+ List<Node> children = new ArrayList<Node>();
+ Node child = null;
+}
+{
+ <DICT_BEGIN>
+ (
+ child = Property()
+ {
+ if (child.getValue() instanceof HierarchicalConfiguration)
+ {
+ // prune & graft the nested configuration to the parent configuration
+ HierarchicalConfiguration conf = (HierarchicalConfiguration) child.getValue();
+ Node root = conf.getRoot();
+ root.setName(child.getName());
+ children.add(root);
+ }
+ else
+ {
+ children.add(child);
+ }
+ }
+ )*
+ <DICT_END>
+ {
+ for (int i = 0; i < children.size(); i++)
+ {
+ child = children.get(i);
+ configuration.getRoot().addChild(child);
+ }
+
+ return configuration;
+ }
+}
+
+Node Property() :
+{
+ String key = null;
+ Object value = null;
+ Node node = new Node();
+}
+{
+ key = String()
+ { node.setName(key); }
+ <EQUAL>
+ value = Element()
+ { node.setValue(value); }
+ (<DICT_SEPARATOR>)?
+ { return node; }
+}
+
+Object Element() :
+{
+ Object value = null;
+}
+{
+ LOOKAHEAD(2)
+
+ value = Array()
+ { return value; }
+ |
+ value = Dictionary()
+ { return value; }
+ |
+ value = String()
+ { return value; }
+ |
+ value = Data()
+ { return value; }
+ |
+ value = Date()
+ { return value; }
+}
+
+List Array() :
+{
+ List<Object> list = new ArrayList<Object>();
+ Object element = null;
+}
+{
+ <ARRAY_BEGIN>
+ (
+ element = Element()
+ { list.add(element); }
+ (
+ <ARRAY_SEPARATOR>
+ element = Element()
+ { list.add(element); }
+ )*
+ )?
+ <ARRAY_END>
+ { return list; }
+}
+
+String String() :
+{
+ Token token = null;
+ String value = null;
+}
+{
+ token = <QUOTED_STRING>
+ { return unescapeQuotes(removeQuotes(token.image)); }
+ |
+ token = <STRING>
+ { return token.image; }
+}
+
+byte[] Data() :
+{
+ Token token;
+}
+{
+ token = <DATA>
+ { return filterData(token.image); }
+}
+
+Date Date() :
+{
+ Token token;
+}
+{
+ token = <DATE>
+ { return parseDate(token.image); }
+}
diff --git a/src/main/resources/PropertyList-1.0.dtd b/src/main/resources/PropertyList-1.0.dtd
new file mode 100644
index 0000000..5924f4d
--- /dev/null
+++ b/src/main/resources/PropertyList-1.0.dtd
@@ -0,0 +1,35 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!ENTITY % plistObject "(array | data | date | dict | real | integer | string | true | false )" >
+<!ELEMENT plist %plistObject;>
+<!ATTLIST plist version CDATA "1.0" >
+
+<!-- Collections -->
+<!ELEMENT array (%plistObject;)*>
+<!ELEMENT dict (key, %plistObject;)*>
+<!ELEMENT key (#PCDATA)>
+
+<!--- Primitive types -->
+<!ELEMENT string (#PCDATA)>
+<!ELEMENT data (#PCDATA)> <!-- Contents interpreted as Base-64 encoded -->
+<!ELEMENT date (#PCDATA)> <!-- Contents should conform to a subset of ISO 8601 (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'. Smaller units may be omitted with a loss of precision) -->
+
+<!-- Numerical primitives -->
+<!ELEMENT true EMPTY> <!-- Boolean constant true -->
+<!ELEMENT false EMPTY> <!-- Boolean constant false -->
+<!ELEMENT real (#PCDATA)> <!-- Contents should represent a floating point number matching ("+" | "-")? d+ ("."d*)? ("E" ("+" | "-") d+)? where d is a digit 0-9. -->
+<!ELEMENT integer (#PCDATA)> <!-- Contents should represent a (possibly signed) integer number in base 10 -->
diff --git a/src/main/resources/digesterRules.xml b/src/main/resources/digesterRules.xml
new file mode 100644
index 0000000..fe23648
--- /dev/null
+++ b/src/main/resources/digesterRules.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<!DOCTYPE digester-rules PUBLIC "-//Jakarta Apache //DTD digester-rules XML V1.0//EN" "http://jakarta.apache.org/commons/digester/dtds/digester-rules.dtd">
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<digester-rules>
+
+ <pattern value="configuration/properties">
+ <object-create-rule classname="org.apache.commons.configuration.PropertiesConfiguration"/>
+ <set-properties-rule/>
+ <set-next-rule methodname="addConfiguration" paramtype="org.apache.commons.configuration.Configuration"/>
+ <call-method-rule methodname="load"/>
+ </pattern>
+
+ <pattern value="configuration/xml">
+ <object-create-rule classname="org.apache.commons.configuration.XMLConfiguration"/>
+ <set-properties-rule/>
+ <set-next-rule methodname="addConfiguration" paramtype="org.apache.commons.configuration.Configuration"/>
+ <call-method-rule methodname="load"/>
+ </pattern>
+
+ <pattern value="configuration/jndi">
+ <object-create-rule classname="org.apache.commons.configuration.JNDIConfiguration"/>
+ <set-properties-rule/>
+ <set-next-rule methodname="addConfiguration" paramtype="org.apache.commons.configuration.Configuration"/>
+ </pattern>
+
+</digester-rules>
diff --git a/src/main/resources/properties.dtd b/src/main/resources/properties.dtd
new file mode 100644
index 0000000..9d2040f
--- /dev/null
+++ b/src/main/resources/properties.dtd
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- DTD for properties -->
+
+<!ELEMENT properties ( comment?, entry* ) >
+
+<!ATTLIST properties version CDATA #FIXED "1.0">
+
+<!ELEMENT comment (#PCDATA)>
+
+<!ELEMENT entry (#PCDATA)>
+
+<!ATTLIST entry key CDATA #REQUIRED>
diff --git a/src/site/resources/images/logo.png b/src/site/resources/images/logo.png
new file mode 100644
index 0000000..ee0a162
Binary files /dev/null and b/src/site/resources/images/logo.png differ
diff --git a/src/site/resources/images/logo.xcf b/src/site/resources/images/logo.xcf
new file mode 100644
index 0000000..ed5e17d
Binary files /dev/null and b/src/site/resources/images/logo.xcf differ
diff --git a/src/site/resources/profile.cobertura b/src/site/resources/profile.cobertura
new file mode 100644
index 0000000..e69de29
diff --git a/src/site/site.xml b/src/site/site.xml
new file mode 100644
index 0000000..477c29e
--- /dev/null
+++ b/src/site/site.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<project name="Commons Configuration">
+ <bannerRight>
+ <name>Commons Configuration</name>
+ <src>/images/logo.png</src>
+ <href>/index.html</href>
+ </bannerRight>
+
+ <body>
+ <menu name="Configuration">
+ <item name="Home" href="/index.html"/>
+ <item name="Download" href="http://commons.apache.org/configuration/download_configuration.cgi"/>
+ <item name="Release History" href="/changes-report.html"/>
+ <item name="User's Guide" href="/userguide/user_guide.html"/>
+ <item name="Runtime Dependencies" href="/dependencies.html"/>
+ <item name="Javadoc" href="/apidocs/index.html"/>
+ </menu>
+
+ <menu name="Development">
+ <item name="Building" href="/building.html"/>
+ <item name="Issue Tracking" href="issue-tracking.html"/>
+ </menu>
+
+ </body>
+
+</project>
diff --git a/src/site/xdoc/building.xml b/src/site/xdoc/building.xml
new file mode 100644
index 0000000..8660ebc
--- /dev/null
+++ b/src/site/xdoc/building.xml
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- ===================================================================== -->
+<!-- $Id: building.xml 1197981 2011-11-05 16:10:56Z oheger $ -->
+<!-- ===================================================================== -->
+
+<document>
+
+ <properties>
+ <title>Building</title>
+ </properties>
+
+ <body>
+
+ <section name="Overview">
+ <p>
+ Commons Configuration can be built using
+ <a href="http://maven.apache.org">maven</a> or
+ <a href="http://ant.apache.org">ant</a>.
+ </p>
+ </section>
+
+ <section name="Maven 2">
+ <p>
+ Commons Configuration uses Maven 2 as its build tool. The recommended
+ version is Maven 2.2.1. To build the Configuration
+ jar, change into the directory where the source distribution resides and run
+ "mvn install". This will compile the source and tests, run the tests, and then
+ package the jar. The jar will also be copied into the local maven repository
+ for use by other builds.
+ </p>
+ <p>
+ This build requires a JDK 1.5 or higher. It is possible to build
+ Commons Configuration on a JDK 1.4 by specifying the "java-1.4"
+ profile: "mvn install -Pjava-1.4". This profile and the settings required are described in
+ detail in <a href="http://commons.apache.org/commons-parent-pom.html#Testing_with_different_Java_versions">
+ Testing with different Java versions</a>. If this profile is active,
+ some classes depending on Commons VFS (which is available for Java
+ 1.5+ only) are excluded from compilation.
+ </p>
+ <p>
+ To build the web site run "mvn site". When it completes the web site will reside in
+ the target/site directory and may be viewed by opening target/site/index.html.
+ </p>
+ </section>
+ <section name="Running Functional Tests">
+ <p>
+ <code>TestWebdavConfigurationBuilder</code> is a functional test that tests
+ DefaultConfigurationBuilder with the configuration files stored in a WebDAV
+ server. To run the test the steps that follow
+ <ol>
+ <li>Copy all the files in the conf directory to the WebDAV server. Not all the
+ files are required but it is generally easier to use a tool like cadaver
+ and do an mput than try to copy the required files individually.</li>
+ <li>Add the following to the profiles section of settings.xml in the Maven home
+ directory. Modify the urls to match your setup.
+<source><![CDATA[
+ <profile>
+ <profile>
+ <id>webdav</id>
+ <activation>
+ <activeByDefault>false</activeByDefault>
+ </activation>
+ <properties>
+ <test.webdav.base>webdav://vfsusr:vfstest@192.168.10.133:80/conf</test.webdav.base>
+ </properties>
+ </profile>]]></source></li>
+ <li>run "mvn -P webdav test -Dtest=TestWebdavConfigurationBuilder". The test
+ can also be run using "mvn -P webdav test" but this will run all the unit tests
+ in addition to the WebDAV test.</li>
+ </ol>
+ </p>
+ </section>
+
+ <section name="Building with Ant">
+ <p>
+ In order to build the project with Apache Ant, some manual preparations
+ have to be done. The problem is that Commons Configuration uses
+ classes generated by <a href="http://javacc.java.net/">JavaCC</a>
+ to process configuration files in specific formats. These classes
+ are produced dynamically during the build process. While Maven is
+ able to download all required artifacts automatically, the Ant build
+ is not that smart. Therefore, it is required to download and install
+ JavaCC manually. Then a file named <code>build.properties</code> has
+ to be created in the root directory of Commons Configuration which
+ defines the <em>javacc_home</em> property; here the path to the
+ JavaCC installation has to be set. Commons Configuration already
+ ships with a <code>build.properties.sample</code> file which can be
+ copied and adapted correspondingly.
+ </p>
+ <p>
+ After these preparations have been done, the following Ant goals
+ can be used:
+ <ul>
+ <li>To build a jar file, change into Configuration's root directory
+ and run "ant jar". The result will be in the "target" subdirectory.
+ </li>
+ <li>To build the Javadocs, run "ant javadoc". The result will be
+ in "target/site/apidocs".</li>
+ </ul>
+ </p>
+ </section>
+ </body>
+
+</document>
diff --git a/src/site/xdoc/dependencies.xml b/src/site/xdoc/dependencies.xml
new file mode 100644
index 0000000..b49bc0d
--- /dev/null
+++ b/src/site/xdoc/dependencies.xml
@@ -0,0 +1,173 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- ===================================================================== -->
+<!-- $Id: dependencies.xml 1368230 2012-08-01 20:08:27Z oheger $ -->
+<!-- ===================================================================== -->
+
+<document>
+
+ <properties>
+ <title>Runtime dependencies</title>
+ </properties>
+
+ <body>
+
+ <section name="Runtime dependencies">
+
+ <p>
+ Commons Configuration requires Java 5 or later.
+ </p>
+ <p>
+ A lot of dependencies are declared in the Maven POM. These are all
+ needed during compile time. On runtime however you only need to
+ add the dependencies to your classpath that are required by the
+ parts of the Commons Configuration package you are using. The
+ following table helps you to determine which dependencies you
+ have to include based on the components you intend to use:
+ </p>
+
+ <table>
+ <thead>
+ <tr>
+ <th width="30%">Component</th>
+ <th>Dependencies</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>Core</td>
+ <td>
+ commons-lang<br/>
+ commons-logging
+ </td>
+ </tr>
+ <tr>
+ <td>DefaultConfigurationBuilder</td>
+ <td>commons-beanutils</td>
+ </tr>
+ <tr>
+ <td>ConfigurationFactory (deprecated)</td>
+ <td>commons-digester</td>
+ </tr>
+ <tr>
+ <td>ConfigurationConverter</td>
+ <td>commons-collections</td>
+ </tr>
+ <tr>
+ <td>
+ PropertyListConfiguration<br/>
+ XMLPropertyListConfiguration
+ </td>
+ <td>commons-codec</td>
+ </tr>
+ <tr>
+ <td>ConfigurationDynaBean</td>
+ <td>commons-beanutils</td>
+ </tr>
+ <tr>
+ <td>XPathExpressionEngine</td>
+ <td>commons-jxpath</td>
+ </tr>
+ <tr>
+ <td>CatalogResolver</td>
+ <td>xml-resolver</td>
+ </tr>
+ <tr>
+ <td>Web configurations</td>
+ <td>servlet-api</td>
+ </tr>
+ <tr>
+ <td>ExprLookup</td>
+ <td>commons-jexl</td>
+ </tr>
+ <tr>
+ <td>VFSFileSystem, VFSFileChangedReloadingStrategy</td>
+ <td>commons-vfs</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <p>
+ <strong>Notes</strong>
+ </p>
+ <p>
+ <ul>
+ <li>Commons Configuration makes use of other
+ Commons components. You should be able to use the current
+ versions of these components together with Commons Configuration.
+ In some cases, when no specific features are used, older
+ versions will work, too. Below is a table with the version
+ numbers that have been tested:
+ <table>
+ <thead>
+ <tr>
+ <th>Component</th>
+ <th>Version</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>commons-lang</td>
+ <td>2.2, 2.3, 2.4, 2.5, 2.6</td>
+ </tr>
+ <tr>
+ <td>commons-collections</td>
+ <td>3.1, 3.2, 3.2.1</td>
+ </tr>
+ <tr>
+ <td>commons-logging</td>
+ <td>1.0.4, 1.1, 1.1.1</td>
+ </tr>
+ <tr>
+ <td>commons-digester</td>
+ <td>1.6, 1.7, 1.8, 1.8.1</td>
+ </tr>
+ <tr>
+ <td>commons-beanutils</td>
+ <td>1.7.0, 1.8.0, 1.8.2, 1.8.3</td>
+ </tr>
+ <tr>
+ <td>commons-codec</td>
+ <td>1.3, 1.5, 1.6</td>
+ </tr>
+ <tr>
+ <td>commons-jxpath</td>
+ <td>1.2, 1.3</td>
+ </tr>
+ <tr>
+ <td>commons-jexl</td>
+ <td>2.1.1</td>
+ </tr>
+ <tr>
+ <td>commons-vfs</td>
+ <td>2.0</td>
+ </tr>
+ <tr>
+ <td>xml-resolver</td>
+ <td>1.2</td>
+ </tr>
+ </tbody>
+ </table>
+ </li>
+ </ul>
+ </p>
+ </section>
+
+ </body>
+
+</document>
diff --git a/src/site/xdoc/download_configuration.xml b/src/site/xdoc/download_configuration.xml
new file mode 100644
index 0000000..ea36ab5
--- /dev/null
+++ b/src/site/xdoc/download_configuration.xml
@@ -0,0 +1,138 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<!--
+ +======================================================================+
+ |**** ****|
+ |**** THIS FILE IS GENERATED BY THE COMMONS BUILD PLUGIN ****|
+ |**** DO NOT EDIT DIRECTLY ****|
+ |**** ****|
+ +======================================================================+
+ | TEMPLATE FILE: download-page-template.xml |
+ | commons-build-plugin/trunk/src/main/resources/commons-xdoc-templates |
+ +======================================================================+
+ | |
+ | 1) Re-generate using: mvn commons:download-page |
+ | |
+ | 2) Set the following properties in the component's pom: |
+ | - commons.componentid (required, alphabetic, lower case) |
+ | - commons.release.version (required) |
+ | - commons.binary.suffix (optional) |
+ | (defaults to "-bin", set to "" for pre-maven2 releases) |
+ | |
+ | 3) Example Properties |
+ | |
+ | <properties> |
+ | <commons.componentid>math</commons.componentid> |
+ | <commons.release.version>1.2</commons.release.version> |
+ | </properties> |
+ | |
+ +======================================================================+
+-->
+<document>
+ <properties>
+ <title>Download Commons Configuration</title>
+ <author email="dev at commons.apache.org">Commons Documentation Team</author>
+ </properties>
+ <body>
+ <section name="Download Commons Configuration">
+ <subsection name="Using a Mirror">
+ <p>
+ We recommend you use a mirror to download our release
+ builds, but you <strong>must</strong> verify the integrity of
+ the downloaded files using signatures downloaded from our main
+ distribution directories. Recent releases (48 hours) may not yet
+ be available from the mirrors.
+ </p>
+
+ <p>
+ You are currently using <b>[preferred]</b>. If you
+ encounter a problem with this mirror, please select another
+ mirror. If all mirrors are failing, there are <i>backup</i>
+ mirrors (at the end of the mirrors list) that should be
+ available.
+ <br></br>
+ [if-any logo]<a href="[link]"><img align="right" src="[logo]" border="0"></img></a>[end]
+ </p>
+
+ <form action="[location]" method="get" id="SelectMirror">
+ <p>
+ Other mirrors:
+ <select name="Preferred">
+ [if-any http]
+ [for http]<option value="[http]">[http]</option>[end]
+ [end]
+ [if-any ftp]
+ [for ftp]<option value="[ftp]">[ftp]</option>[end]
+ [end]
+ [if-any backup]
+ [for backup]<option value="[backup]">[backup] (backup)</option>[end]
+ [end]
+ </select>
+ <input type="submit" value="Change"></input>
+ </p>
+ </form>
+
+ <p>
+ The <a href="http://www.apache.org/dist/commons/KEYS">KEYS</a>
+ link links to the code signing keys used to sign the product.
+ The <code>PGP</code> link downloads the OpenPGP compatible signature from our main site.
+ The <code>MD5</code> link downloads the checksum from the main site.
+ </p>
+ </subsection>
+ </section>
+ <section name="Commons Configuration 1.10 ">
+ <subsection name="Binaries">
+ <table>
+ <tr>
+ <td><a href="[preferred]/commons/configuration/binaries/commons-configuration-1.10-bin.tar.gz">commons-configuration-1.10-bin.tar.gz</a></td>
+ <td><a href="http://www.apache.org/dist/commons/configuration/binaries/commons-configuration-1.10-bin.tar.gz.md5">md5</a></td>
+ <td><a href="http://www.apache.org/dist/commons/configuration/binaries/commons-configuration-1.10-bin.tar.gz.asc">pgp</a></td>
+ </tr>
+ <tr>
+ <td><a href="[preferred]/commons/configuration/binaries/commons-configuration-1.10-bin.zip">commons-configuration-1.10-bin.zip</a></td>
+ <td><a href="http://www.apache.org/dist/commons/configuration/binaries/commons-configuration-1.10-bin.zip.md5">md5</a></td>
+ <td><a href="http://www.apache.org/dist/commons/configuration/binaries/commons-configuration-1.10-bin.zip.asc">pgp</a></td>
+ </tr>
+ </table>
+ </subsection>
+ <subsection name="Source">
+ <table>
+ <tr>
+ <td><a href="[preferred]/commons/configuration/source/commons-configuration-1.10-src.tar.gz">commons-configuration-1.10-src.tar.gz</a></td>
+ <td><a href="http://www.apache.org/dist/commons/configuration/source/commons-configuration-1.10-src.tar.gz.md5">md5</a></td>
+ <td><a href="http://www.apache.org/dist/commons/configuration/source/commons-configuration-1.10-src.tar.gz.asc">pgp</a></td>
+ </tr>
+ <tr>
+ <td><a href="[preferred]/commons/configuration/source/commons-configuration-1.10-src.zip">commons-configuration-1.10-src.zip</a></td>
+ <td><a href="http://www.apache.org/dist/commons/configuration/source/commons-configuration-1.10-src.zip.md5">md5</a></td>
+ <td><a href="http://www.apache.org/dist/commons/configuration/source/commons-configuration-1.10-src.zip.asc">pgp</a></td>
+ </tr>
+ </table>
+ </subsection>
+ </section>
+ <section name="Archives">
+ <p>
+ Older releases can be obtained from the archives.
+ </p>
+ <ul>
+ <li class="download"><a href="[preferred]/commons/configuration/">browse download area</a></li>
+ <li><a href="http://archive.apache.org/dist/commons/configuration/">archives...</a></li>
+ </ul>
+ </section>
+ </body>
+</document>
diff --git a/src/site/xdoc/index.xml b/src/site/xdoc/index.xml
new file mode 100644
index 0000000..0aa8893
--- /dev/null
+++ b/src/site/xdoc/index.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- ===================================================================== -->
+<!-- $Id: index.xml 1164913 2011-09-03 19:16:04Z oheger $ -->
+<!-- ===================================================================== -->
+<document>
+
+ <properties>
+ <author email="pete at kazmier.com">Pete Kazmier</author>
+ <author email="mpoeschl at marmot.at">Martin Poeschl</author>
+ <author email="jason at zenplex.com">Jason van Zyl</author>
+ <author email="epugh at upstate.com">Eric Pugh</author>
+ <author email="tobrien at discursive.com">Tim O'Brien</author>
+ <author email="rgoers at apache.org">Ralph Goers</author>
+ <title>Java Configuration API</title>
+ </properties>
+
+ <body>
+ <section name="Intro">
+ <p>
+ The Commons Configuration software library provides a generic configuration interface which enables
+ a Java application to read configuration data from a variety of sources. Commons Configuration
+ provides typed access to single, and multi-valued configuration parameters as demonstrated
+ by the following code:
+
+<source><![CDATA[
+Double double = config.getDouble("number");
+Integer integer = config.getInteger("number");
+]]></source>
+
+ </p>
+ <p>
+ Configuration parameters may be loaded from the following sources:
+
+ <ul>
+ <li>Properties files</li>
+ <li>XML documents</li>
+ <li>Windows INI files</li>
+ <li>Property list files (plist)</li>
+ <li>JNDI</li>
+ <li>JDBC Datasource</li>
+ <li>System properties</li>
+ <li>Applet parameters</li>
+ <li>Servlet parameters</li>
+ </ul>
+
+ Different configuration sources can be mixed using a <code>ConfigurationFactory</code> and
+ a <code>CompositeConfiguration</code>. Additional sources of configuration parameters can
+ be created by using custom configuration objects. This customization can be achieved by
+ extending <code>AbstractConfiguration</code> or <code>AbstractFileConfiguration</code>.
+ </p>
+ <p>
+ The full Javadoc API documentation is available <a href="apidocs/index.html">here</a>.
+ </p>
+ </section>
+
+ <section name="Latest Release">
+ <p>
+ The latest release of Apache Commons Configuration is available from the
+ <a href="http://commons.apache.org/configuration/download_configuration.cgi">Apache download area</a>.
+ It is also available from the <a href="http://repo1.maven.org/maven2/commons-configuration/commons-configuration/">Maven repository</a>.
+ The <a href="changes-report.html">Changes Report</a> explains all of the changes and bug fixes that have been made.
+ </p>
+ </section>
+
+ <section name="History">
+ <p>
+ Commons Configuration started as code in Apache JServ. The JServ code was subsequently
+ added to <a href="http://jakarta.apache.org/turbine">Jakarta Turbine</a>. After Jakarta
+ Turbine, this configuration interface moved to <a href="http://jakarta.apache.org/velocity">Jakarta Velocity</a>
+ and underwent various improvements. After Velocity, this code was introduced to the
+ <a href="http://commons.apache.org">Apache Commons</a> as <code>ExtendedProperties</code>.
+ Configuration began life in the Commons as a Sandbox component, and was promoted to the
+ Commons Proper in late 2003.
+ </p>
+ </section>
+
+ <section name="Bugs">
+ <p>
+ Bugs may be reported via the <a href="http://issues.apache.org/jira/browse/CONFIGURATION">ASF JIRA</a>
+ system. Detailed information can be found on the <a href="issue-tracking.html">issue tracking page</a>.
+ </p>
+ </section>
+
+ </body>
+</document>
diff --git a/src/site/xdoc/issue-tracking.xml b/src/site/xdoc/issue-tracking.xml
new file mode 100644
index 0000000..0542086
--- /dev/null
+++ b/src/site/xdoc/issue-tracking.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<!--
+ +======================================================================+
+ |**** ****|
+ |**** THIS FILE IS GENERATED BY THE COMMONS BUILD PLUGIN ****|
+ |**** DO NOT EDIT DIRECTLY ****|
+ |**** ****|
+ +======================================================================+
+ | TEMPLATE FILE: issue-tracking-template.xml |
+ | commons-build-plugin/trunk/src/main/resources/commons-xdoc-templates |
+ +======================================================================+
+ | |
+ | 1) Re-generate using: mvn commons:jira-page |
+ | |
+ | 2) Set the following properties in the component's pom: |
+ | - commons.jira.id (required, alphabetic, upper case) |
+ | - commons.jira.pid (required, numeric) |
+ | |
+ | 3) Example Properties |
+ | |
+ | <properties> |
+ | <commons.jira.id>MATH</commons.jira.id> |
+ | <commons.jira.pid>12310485</commons.jira.pid> |
+ | </properties> |
+ | |
+ +======================================================================+
+-->
+<document>
+ <properties>
+ <title>Commons Configuration Issue tracking</title>
+ <author email="dev at commons.apache.org">Commons Documentation Team</author>
+ </properties>
+ <body>
+
+ <section name="Commons Configuration Issue tracking">
+ <p>
+ Commons Configuration uses <a href="http://issues.apache.org/jira/">ASF JIRA</a> for tracking issues.
+ See the <a href="http://issues.apache.org/jira/browse/CONFIGURATION">Commons Configuration JIRA project page</a>.
+ </p>
+
+ <p>
+ To use JIRA you may need to <a href="http://issues.apache.org/jira/secure/Signup!default.jspa">create an account</a>
+ (if you have previously created/updated Commons issues using Bugzilla an account will have been automatically
+ created and you can use the <a href="http://issues.apache.org/jira/secure/ForgotPassword!default.jspa">Forgot Password</a>
+ page to get a new password).
+ </p>
+
+ <p>
+ If you would like to report a bug, or raise an enhancement request with
+ Commons Configuration please do the following:
+ <ol>
+ <li><a href="http://issues.apache.org/jira/secure/IssueNavigator.jspa?reset=true&pid=12310467&sorter/field=issuekey&sorter/order=DESC&status=1&status=3&status=4">Search existing open bugs</a>.
+ If you find your issue listed then please add a comment with your details.</li>
+ <li><a href="mail-lists.html">Search the mailing list archive(s)</a>.
+ You may find your issue or idea has already been discussed.</li>
+ <li>Decide if your issue is a bug or an enhancement.</li>
+ <li>Submit either a <a href="http://issues.apache.org/jira/secure/CreateIssueDetails!init.jspa?pid=12310467&issuetype=1&priority=4&assignee=-1">bug report</a>
+ or <a href="http://issues.apache.org/jira/secure/CreateIssueDetails!init.jspa?pid=12310467&issuetype=4&priority=4&assignee=-1">enhancement request</a>.</li>
+ </ol>
+ </p>
+
+ <p>
+ Please also remember these points:
+ <ul>
+ <li>the more information you provide, the better we can help you</li>
+ <li>test cases are vital, particularly for any proposed enhancements</li>
+ <li>the developers of Commons Configuration are all unpaid volunteers</li>
+ </ul>
+ </p>
+
+ <p>
+ For more information on subversion and creating patches see the
+ <a href="http://www.apache.org/dev/contributors.html">Apache Contributors Guide</a>.
+ </p>
+
+ <p>
+ You may also find these links useful:
+ <ul>
+ <li><a href="http://issues.apache.org/jira/secure/IssueNavigator.jspa?reset=true&pid=12310467&sorter/field=issuekey&sorter/order=DESC&status=1&status=3&status=4">All Open Commons Configuration bugs</a></li>
+ <li><a href="http://issues.apache.org/jira/secure/IssueNavigator.jspa?reset=true&pid=12310467&sorter/field=issuekey&sorter/order=DESC&status=5&status=6">All Resolved Commons Configuration bugs</a></li>
+ <li><a href="http://issues.apache.org/jira/secure/IssueNavigator.jspa?reset=true&pid=12310467&sorter/field=issuekey&sorter/order=DESC">All Commons Configuration bugs</a></li>
+ </ul>
+ </p>
+ </section>
+ </body>
+</document>
diff --git a/src/site/xdoc/javadocarchive.xml b/src/site/xdoc/javadocarchive.xml
new file mode 100644
index 0000000..79016b1
--- /dev/null
+++ b/src/site/xdoc/javadocarchive.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<document>
+ <properties>
+ <title>Javadoc Archives</title>
+ </properties>
+ <body>
+
+ <section name="Javadoc Archives">
+
+ <p>Commons Configuration 1.7 (<a href="apidocs_1.7">javadoc</a>)</p>
+
+ <p>Commons Configuration 1.6 (<a href="apidocs_1.6">javadoc</a>)</p>
+
+ <p>Commons Configuration 1.5 (<a href="apidocs_1.5">javadoc</a>)</p>
+
+ <p>Commons Configuration 1.4 (<a href="apidocs_1.4">javadoc</a>)</p>
+
+ <p>Commons Configuration 1.3 (<a href="apidocs_1.3">javadoc</a>)</p>
+
+ <p>Commons Configuration 1.2 (<a href="apidocs_1.2">javadoc</a>)</p>
+
+ <p>Commons Configuration 1.1 (<a href="apidocs_1.1">javadoc</a>)</p>
+
+ <p>Commons Configuration 1.0 (<a href="apidocs_1.0">javadoc</a>)</p>
+
+ </section>
+
+ </body>
+</document>
diff --git a/src/site/xdoc/mail-lists.xml b/src/site/xdoc/mail-lists.xml
new file mode 100644
index 0000000..3b64284
--- /dev/null
+++ b/src/site/xdoc/mail-lists.xml
@@ -0,0 +1,202 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<!--
+ +======================================================================+
+ |**** ****|
+ |**** THIS FILE IS GENERATED BY THE COMMONS BUILD PLUGIN ****|
+ |**** DO NOT EDIT DIRECTLY ****|
+ |**** ****|
+ +======================================================================+
+ | TEMPLATE FILE: mail-lists-template.xml |
+ | commons-build-plugin/trunk/src/main/resources/commons-xdoc-templates |
+ +======================================================================+
+ | |
+ | 1) Re-generate using: mvn commons:mail-page |
+ | |
+ | 2) Set the following properties in the component's pom: |
+ | - commons.componentid (required, alphabetic, lower case) |
+ | |
+ | 3) Example Properties |
+ | |
+ | <properties> |
+ | <commons.componentid>math</commons.componentid> |
+ | </properties> |
+ | |
+ +======================================================================+
+-->
+<document>
+ <properties>
+ <title>Commons Configuration Mailing Lists</title>
+ <author email="dev at commons.apache.org">Commons Documentation Team</author>
+ </properties>
+ <body>
+
+ <section name="Overview">
+ <p>
+ <a href="index.html">Commons Configuration</a> shares mailing lists with all the other
+ <a href="http://commons.apache.org/components.html">Commons Components</a>.
+ To make it easier for people to only read messages related to components they are interested in,
+ the convention in Commons is to prefix the subject line of messages with the component's name,
+ for example:
+ <ul>
+ <li>[configuration] Problem with the ...</li>
+ </ul>
+ </p>
+ <p>
+ Questions related to the usage of Commons Configuration should be posted to the
+ <a href="http://mail-archives.apache.org/mod_mbox/commons-user/">User List</a>.
+ <br />
+ The <a href="http://mail-archives.apache.org/mod_mbox/commons-dev/">Developer List</a>
+ is for questions and discussion related to the development of Commons Configuration.
+ <br />
+ Please do not cross-post; developers are also subscribed to the user list.
+ </p>
+ <p>
+ <strong>Note:</strong> please don't send patches or attachments to any of the mailing lists.
+ Patches are best handled via the <a href="issue-tracking.html">Issue Tracking</a> system.
+ Otherwise, please upload the file to a public server and include the URL in the mail.
+ </p>
+ </section>
+
+ <section name="Commons Configuration Mailing Lists">
+ <p>
+ <strong>Please prefix the subject line of any messages for <a href="index.html">Commons Configuration</a>
+ with <i>[configuration]</i></strong> - <i>thanks!</i>
+ <br />
+ <br />
+ </p>
+
+ <table>
+ <tr>
+ <th>Name</th>
+ <th>Subscribe</th>
+ <th>Unsubscribe</th>
+ <th>Post</th>
+ <th>Archive</th>
+ <th>Other Archives</th>
+ </tr>
+
+
+ <tr>
+ <td>
+ <strong>Commons User List</strong>
+ <br /><br />
+ Questions on using Commons Configuration.
+ <br /><br />
+ </td>
+ <td><a href="mailto:user-subscribe at commons.apache.org">Subscribe</a></td>
+ <td><a href="mailto:user-unsubscribe at commons.apache.org">Unsubscribe</a></td>
+ <td><a href="mailto:user at commons.apache.org?subject=[configuration]">Post</a></td>
+ <td><a href="http://mail-archives.apache.org/mod_mbox/commons-user/">mail-archives.apache.org</a></td>
+ <td><a href="http://markmail.org/list/org.apache.commons.users/">markmail.org</a><br />
+ <a href="http://www.mail-archive.com/user@commons.apache.org/">www.mail-archive.com</a><br />
+ <a href="http://news.gmane.org/gmane.comp.jakarta.commons.devel">news.gmane.org</a>
+ </td>
+ </tr>
+
+
+ <tr>
+ <td>
+ <strong>Commons Developer List</strong>
+ <br /><br />
+ Discussion of development of Commons Configuration.
+ <br /><br />
+ </td>
+ <td><a href="mailto:dev-subscribe at commons.apache.org">Subscribe</a></td>
+ <td><a href="mailto:dev-unsubscribe at commons.apache.org">Unsubscribe</a></td>
+ <td><a href="mailto:dev at commons.apache.org?subject=[configuration]">Post</a></td>
+ <td><a href="http://mail-archives.apache.org/mod_mbox/commons-dev/">mail-archives.apache.org</a></td>
+ <td><a href="http://markmail.org/list/org.apache.commons.dev/">markmail.org</a><br />
+ <a href="http://www.mail-archive.com/dev@commons.apache.org/">www.mail-archive.com</a><br />
+ <a href="http://news.gmane.org/gmane.comp.jakarta.commons.devel">news.gmane.org</a>
+ </td>
+ </tr>
+
+
+ <tr>
+ <td>
+ <strong>Commons Issues List</strong>
+ <br /><br />
+ Only for e-mails automatically generated by the <a href="issue-tracking.html">issue tracking</a> system.
+ <br /><br />
+ </td>
+ <td><a href="mailto:issues-subscribe at commons.apache.org">Subscribe</a></td>
+ <td><a href="mailto:issues-unsubscribe at commons.apache.org">Unsubscribe</a></td>
+ <td><i>read only</i></td>
+ <td><a href="http://mail-archives.apache.org/mod_mbox/commons-issues/">mail-archives.apache.org</a></td>
+ <td><a href="http://markmail.org/list/org.apache.commons.issues/">markmail.org</a><br />
+ <a href="http://www.mail-archive.com/issues@commons.apache.org/">www.mail-archive.com</a>
+ </td>
+ </tr>
+
+
+ <tr>
+ <td>
+ <strong>Commons Commits List</strong>
+ <br /><br />
+ Only for e-mails automatically generated by the <a href="source-repository.html">source control</a> sytem.
+ <br /><br />
+ </td>
+ <td><a href="mailto:commits-subscribe at commons.apache.org">Subscribe</a></td>
+ <td><a href="mailto:commits-unsubscribe at commons.apache.org">Unsubscribe</a></td>
+ <td><i>read only</i></td>
+ <td><a href="http://mail-archives.apache.org/mod_mbox/commons-commits/">mail-archives.apache.org</a></td>
+ <td><a href="http://markmail.org/list/org.apache.commons.commits/">markmail.org</a><br />
+ <a href="http://www.mail-archive.com/commits@commons.apache.org/">www.mail-archive.com</a>
+ </td>
+ </tr>
+
+ </table>
+
+ </section>
+ <section name="Apache Mailing Lists">
+ <p>
+ Other mailing lists which you may find useful include:
+ </p>
+
+ <table>
+ <tr>
+ <th>Name</th>
+ <th>Subscribe</th>
+ <th>Unsubscribe</th>
+ <th>Post</th>
+ <th>Archive</th>
+ <th>Other Archives</th>
+ </tr>
+ <tr>
+ <td>
+ <strong>Apache Announce List</strong>
+ <br /><br />
+ General announcements of Apache project releases.
+ <br /><br />
+ </td>
+ <td><a class="externalLink" href="mailto:announce-subscribe at apache.org">Subscribe</a></td>
+ <td><a class="externalLink" href="mailto:announce-unsubscribe at apache.org">Unsubscribe</a></td>
+ <td><i>read only</i></td>
+ <td><a class="externalLink" href="http://mail-archives.apache.org/mod_mbox/www-announce/">mail-archives.apache.org</a></td>
+ <td><a class="externalLink" href="http://markmail.org/list/org.apache.announce/">markmail.org</a><br />
+ <a class="externalLink" href="http://old.nabble.com/Apache-News-and-Announce-f109.html">old.nabble.com</a><br />
+ <a class="externalLink" href="http://www.mail-archive.com/announce@apache.org/">www.mail-archive.com</a><br />
+ <a class="externalLink" href="http://news.gmane.org/gmane.comp.apache.announce">news.gmane.org</a>
+ </td>
+ </tr>
+ </table>
+
+ </section>
+ </body>
+</document>
diff --git a/src/site/xdoc/userguide/howto_basicfeatures.xml b/src/site/xdoc/userguide/howto_basicfeatures.xml
new file mode 100644
index 0000000..47a9f17
--- /dev/null
+++ b/src/site/xdoc/userguide/howto_basicfeatures.xml
@@ -0,0 +1,361 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<document>
+
+ <properties>
+ <title>Basic Features</title>
+ <author email="oheger at apache.org">Oliver Heger</author>
+ </properties>
+
+<body>
+ <section name="Basic features and AbstractConfiguration">
+ <p>
+ The <code>Configuration</code> interface defines a whole bunch of methods.
+ Implementing these methods all from scratch can be quite hard. Because of
+ that the <code><a href="../apidocs/org/apache/commons/configuration/AbstractConfiguration.html">
+ AbstractConfiguration</a></code> class exists. This class serves as a
+ common base class for most of the <code>Configuration</code> implementations
+ in <em>Commons Configuration</em> and provides a great deal of the
+ functionality required by the interface. So for creating a custom
+ <code>Configuration</code> implementation this class will be a good
+ starting point.
+ </p>
+ <p>
+ In addition to base implementations for lots of the methods declared in
+ the <code>Configuration</code> interface the <code>AbstractConfiguration</code>
+ class provides some other handy and convenient features. Because this
+ class is at the very root of the class hierarchy in <em>Commons
+ Configuration</em> these features are available in most of the specific
+ implementations of the <code>Configuration</code> interface provided by
+ this library. We will cover some of these basic features in this section.
+ </p>
+
+ <subsection name="Handling of missing properties">
+ <p>
+ What is a configuration object supposed to do if you pass in a key to one
+ of its get methods that does not map to an existing property? Well, the
+ default behavior as implemented in <code>AbstractConfiguration</code> is
+ to return <b>null</b> if the return value is an object type. For primitive
+ types as return values returning <b>null</b> (or any other special
+ value) is not possible, so in this case a <code>NoSuchElementException</code>
+ is thrown:
+ </p>
+ <source><![CDATA[
+// This will return null if no property with key "NonExistingProperty" exists
+String strValue = config.getString("NonExistingProperty");
+
+// This will throw a NoSuchElementException exception if no property with
+// key "NonExistingProperty" exists
+long longValue = config.getLong("NonExistingProperty");
+]]></source>
+ <p>
+ For object types like <code>String</code>, <code>BigDecimal</code>, or
+ <code>BigInteger</code> this default behavior can be changed: If the
+ <code>setThrowExceptionOnMissing()</code> method is called with an
+ argument of <b>true</b>, these methods will behave like their primitive
+ counter parts and also throw an exception if the passed in property key
+ cannot be resolved.
+ </p>
+ <p>
+ <em>Note:</em> Unfortunately support for the <code>throwExceptionOnMissing</code>
+ property is not always consistent: The methods <code>getList()</code> and
+ <code>getStringArray()</code> do not evaluate this flag, but return an
+ empty list or array if the requested property cannot be found. Maybe this
+ behavior will be changed in a future major release.
+ </p>
+ </subsection>
+
+ <subsection name="List handling">
+ <p>
+ With <code>getList()</code> and <code>getStringArray()</code> the
+ <code>Configuration</code> interface defines methods for dealing with
+ properties that have multiple values. When a configuration source (e.g.
+ a properties file, an XML document, or a JNDI context) is processed the
+ corresponding <code>Configuration</code> implementation detects such
+ properties with multiple values and ensures that the data is correctly
+ stored.
+ </p>
+ <p>
+ When modifying properties the <code>addProperty()</code> and
+ <code>setProperty()</code> methods of <code>AbstractConfiguration</code>
+ also implement special list handling. The property value that is passed
+ to these methods can be a list or an array resulting in a property
+ with multiple values. If the property value is a string, it is checked
+ whether it contains the <em>list delimiter character</em>. If this is
+ the case, the string is splitted, and its single parts are added one
+ by one. The list delimiter character is the comma by default. It is
+ also taken into account when the configuration source is loaded
+ (i.e. string values of properties will be checked whether they contain
+ this delimiter). By using the <code>setListDelimiter()</code>
+ method you can set it to a different character. Here are some examples:
+ </p>
+<source>
+// Change the list delimiter character to a slash
+config.setListDelimiter('/');
+// Now add some properties
+config.addProperty("greeting", "Hello, how are you?");
+config.addProperty("colors.pie",
+ new String[] { "#FF0000", "#00FF00", "#0000FF" });
+config.addProperty("colors.graph", "#808080/#00FFCC/#6422FF");
+
+// Access data
+String salut = config.getString("greeting");
+List<Object> colPie = config.getList("colors.pie");
+String[] colGraph = config.getStringArray("colors.graph");
+
+String firstPieColor = config.getString("colors.pie");
+</source>
+ <p>
+ In this example the list delimiter character is changed from a comma
+ to a slash. Because of this the <code>greeting</code> property won't
+ be split, but remains a single string. The string passed as value
+ for the <code>colors.graph</code> property in opposite contains the
+ new delimiter character and thus will result in a property with three
+ values. Note that lists are of type <code>Object</code>. This is because
+ the concrete class of the values of a property is not known. For instance,
+ if you call <code>addProperty("answer", 42)</code>, an
+ <code>Integer</code> object will be stored in the configuration.
+ </p>
+ <p>
+ Of interest is also the last line of the example fragment. Here the
+ <code>getString()</code> method is called for a property that has
+ multiple values. This call will return the first value of the list.
+ </p>
+ <p>
+ If you want to change the list delimiter character for all configuration
+ objects, you can use the static <code>setDefaultListDelimiter()</code>
+ method of <code>AbstractConfiguration</code>. It is also possible to
+ disable splitting of string properties at all for a Configuration
+ instance by calling its <code>setDelimiterParsingDisabled()</code>
+ method with a value of <b>true</b>.
+ </p>
+ </subsection>
+
+ <subsection name="Variable Interpolation">
+ <p>
+ If you are familiar with Ant or Maven, you have most certainly already encountered
+ the variables (like <code>${token}</code>) that are automatically expanded when the
+ configuration file is loaded. Commons Configuration supports this feature as well,
+ here is an example (we use a properties file in this example, but other
+ configuration sources work the same way; you can learn more about
+ properties files in the chapter <a href="howto_properties.html">Properties
+ files</a>):
+ </p>
+<source>
+application.name = Killer App
+application.version = 1.6.2
+
+application.title = ${application.name} ${application.version}
+</source>
+ <p>
+ If you now retrieve the value for the <code>application.title</code>
+ property, the result will be <code>Killer App 1.6.2</code>. So per default
+ variables are interpreted as the keys of other properties. This is only a
+ special case, the general syntax of a variable name is
+ <code>${prefix:name}</code>. The prefix tells Commons Configuration that
+ the variable is to be evaluated in a certain context. We have already seen
+ that the context is the current configuration instance if the prefix is
+ missing. The following other prefix names are supported by default:
+ <table border="1">
+ <tr>
+ <th>Prefix</th>
+ <th>Description</th>
+ </tr>
+ <tr>
+ <td valign="top">sys</td>
+ <td>This prefix marks a variable to be a system property. Commons
+ Configuration will search for a system property with the given name and
+ replace the variable by its value. This is a very easy means for
+ accessing the values of system properties in every configuration
+ implementation.</td>
+ </tr>
+ <tr>
+ <td valign="top">const</td>
+ <td>The <code>const</code> prefix indicates that a variable is to be
+ interpreted as a constant member field of a class (i.e. a field with the
+ <b>static final</b> modifiers). The name of the variable must be of the form
+ <code><full qualified class name>.<field name></code>. The
+ specified class will be loaded and the value of this field will be
+ obtained.</td>
+ </tr>
+ <tr>
+ <td valign="top">env</td>
+ <td>Variables can also reference OS-specific environment properties.
+ This is indicated by the <code>env</code> prefix.</td>
+ </tr>
+ </table>
+ Here are some examples (again using properties syntax):
+ </p>
+ <source><![CDATA[
+user.file = ${sys:user.home}/settings.xml
+
+action.key = ${const:java.awt.event.KeyEvent.VK_CANCEL}
+
+java.home = ${env:JAVA_HOME}
+]]></source>
+ <p>
+ If a variable cannot be resolved, e.g. because the name is invalid or an
+ unknown prefix is used, it won't be replaced, but is returned as is
+ including the dollar sign and the curly braces.
+ </p>
+ </subsection>
+
+ <subsection name="Customizing interpolation">
+ <p>
+ This sub section goes a bit behind the scenes of interpolation and
+ explains some approaches how you can add your own interpolation facilities.
+ Under the hood interpolation is implemented using the
+ <code>StrSubstitutor</code> class of the <code>text</code> package of
+ <a href="http://commons.apache.org/lang/">Commons Lang</a>. This
+ class uses objects derived from the <code>StrLookup</code> class for
+ resolving variables. <code>StrLookup</code> defines a simple
+ <code>lookup()</code> method that must be implemented by custom
+ implementations; it expects the name of a variable as argument and
+ returns the corresponding value (further details can be found in the
+ documentation of Commons Lang). The standard prefixes for variables we
+ have covered so far are indeed realized by special classes derived from
+ <code>StrLookup</code>.
+ </p>
+ <p>
+ It is now possible to create your own implementation of <code>StrLookup</code>
+ and make it available for all configuration objects under a custom
+ prefix. We will show how this can be achieved. The first step is to
+ create a new class derived from <code>StrLookup</code>, which must
+ implement the <code>lookup()</code> method. As an example we implement a
+ rather dull lookup object that simply returns a kind of "echo"
+ for the variable passed in:
+ </p>
+ <source><![CDATA[
+import org.apache.commons.lang.text.StrLookup;
+
+public class EchoLookup extends StrLookup
+{
+ public String lookup(String varName)
+ {
+ return "Value of variable " + varName;
+ }
+}
+]]></source>
+ <p>
+ Now we want this class to be called for variables with the prefix
+ <code>echo</code>. For this purpose the <code>EchoLookup</code> class
+ has to be registered at the
+ <code><a href="../apidocs/org/apache/commons/configuration/interpol/ConfigurationInterpolator.html">
+ ConfigurationInterpolator</a></code> class with the desired prefix.
+ <code>ConfigurationInterpolator</code> implements a thin wrapper over the
+ <code>StrLookup</code> API defined by Commons Lang. It has a static
+ <code>registerGlobalLookup()</code> method, which we have to call as
+ follows:
+ </p>
+ <source><![CDATA[
+// Place this code somewhere in an initialization section of your application
+ConfigurationInterpolator.registerGlobalLookup("echo", new EchoLookup());
+ ]]></source>
+ <p>
+ Each <code>AbstractConfiguration</code> object that is created after this
+ line is executed will contain the new lookup class and can thus resolve
+ variables of the form <code>${echo:my_variable}</code>.
+ </p>
+ <p>
+ Each instance of <code>AbstractConfiguration</code> is associated with a
+ <code>ConfigurationInterpolator</code> object. This object is created by
+ the <code>createInterpolator()</code> method on first access of one of
+ the interpolation features. By overriding this method even deeper
+ intervention in the interpolation mechanism is possible. For instance
+ a custom implementation can add further lookup objects to the interpolator,
+ which are then only used by this configuration instance.
+ </p>
+ </subsection>
+ <subsection name="Using Expressions">
+ <p>
+ In addition to the simple lookup mechanisms previously described, Commond Configuration
+ provides <code>ExprLookup</code> which uses <a href="http://commons.apache.org/jexl">Apache
+ Commons Jexl</a> to allow expressions to be resolved wherever a StrLookup is allowed. This
+ example shows an alternate way of obtaining a system property if the ExprLookup is
+ configured.
+ </p>
+<source><![CDATA[
+user.file = ${expr:System.getProperty("user.home"}/settings.xml
+]]></source>
+ <p>
+ <code>ExprLookup</code> is not enabled by default, it must be manually added or configured via
+ <code>DefaultConfigurationBuilder</code>. Builds that use Maven 2 and reference Commons
+ Configuration will not include a dependency on Jexl, so if this feature is used the
+ dependency on Jexl must be manually added to the project.
+ </p>
+ <p>
+ When using <code>DefaultConfigurationBuilder</code> adding <code>ExprLookup</code> is
+ straightforward.
+ </p>
+<source><![CDATA[
+<configuration>
+ <header>
+ <result/>
+ <lookups>
+ <lookup config-prefix="expr"
+ config-class="org.apache.commons.configuration.interpol.ExprLookup">
+ <variables>
+ <variable name="System" value="Class:java.lang.System"/>
+ <variable name"net" value="Class:java.net.InetAddress"/>
+ <variable name="String" value="Class:org.apache.commons.lang.StringUtils"/>
+ </variables>
+ </lookup>
+ </lookups>
+ </header>
+ <override>
+ <xml fileName="${expr:System.getProperty('basePath') +
+ String.lowercase(net.localHost.hostName) + '/testMultiConfiguration_default.xml'}"
+ config-name="defaultConfig" delimiterParsingDisabled="true">
+ </xml>
+ </override>
+</configuration>
+]]></source>
+ <p>
+ The example above shows how to invoke static methods during expression evaluation. The
+ next example shows mixing expression evaluation with a subordinate lookup to
+ obtain the "basePath" system property. Note the difference in how the
+ system property was obtained in the previous example.
+ </p>
+<source><![CDATA[
+<configuration>
+ <header>
+ <result/>
+ <lookups>
+ <lookup config-prefix="expr"
+ config-class="org.apache.commons.configuration.interpol.ExprLookup">
+ <variables>
+ <variable name"net" value="Class:java.net.InetAddress"/>
+ <variable name="String" value="Class:org.apache.commons.lang.StringUtils"/>
+ </variables>
+ </lookup>
+ </lookups>
+ </header>
+ <override>
+ <xml fileName="${expr:$[sys:basePath] +
+ String.lowercase(net.localHost.hostName) + '/testMultiConfiguration_default.xml'}"
+ config-name="defaultConfig" delimiterParsingDisabled="true">
+ </xml>
+ </override>
+</configuration>
+]]></source>
+ </subsection>
+ </section>
+</body>
+
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/userguide/howto_beans.xml b/src/site/xdoc/userguide/howto_beans.xml
new file mode 100644
index 0000000..85d55df
--- /dev/null
+++ b/src/site/xdoc/userguide/howto_beans.xml
@@ -0,0 +1,358 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<document>
+
+ <properties>
+ <title>Declaring Beans Howto</title>
+ <author email="oheger at apache.org">Oliver Heger</author>
+ </properties>
+
+<body>
+ <section name="Declaring and Creating Beans">
+ <p>
+ Often it is good practice to make heavy use of Java interfaces and program
+ an application or components against these interfaces rather than concrete
+ implementation classes. This makes it possible to switch to different implementations
+ without having to modify calling code. However the problem remains how a
+ concrete implementation of an interface is obtained. Simply using the
+ <b>new</b> operator on a specific implementation class would somehow break
+ the interface concept because then the code would have an explicit reference
+ to a concrete implementation.
+ </p>
+ <p>
+ A solution to this problem is to define the concrete implementation class
+ that should be used in a configuration file. Client code would obtain an
+ object (or a bean) from the configuration and cast it to the service
+ interface. This way the caller would have no knowledge about which concrete
+ implementation is used; it would only interact with the service through
+ the interface. By changing the configuration file and entering a different
+ class name for the implementation class the behavior of the application
+ can be altered, e.g. to inject a test stub for the used service.
+ </p>
+ <p>
+ <em>Note: The concept of defining service objects in configuration files
+ and let them be created by a special container has grown popular these
+ days. Especially IoC containers like <a href="http://jakarta.apache.org/hivemind/">HiveMind</a>
+ or <a href="http://www.springframework.org/">Spring</a> offer wide
+ functionality related to this topic. Commons Configuration is not and has
+ no ambitions to become an IoC container. The provided functionality for
+ declaring and creating beans is very basic and limited compared to the
+ specialists. So if you are in need of enhanced features like the creation
+ of complete networks of service objects, life cycle handling and such things,
+ you should in any case use a real IoC container. For simple use cases
+ however the functionality of Commons Configuration might be sufficient,
+ and we have tried to provide hooks for extending the predefined mechanisms.</em>
+ </p>
+
+ <subsection name="Basic Concepts">
+ <p>
+ Beans (we use the term <em>bean</em> here to name any plain old Java
+ object that is defined in a configuration file and can be instantiated
+ by Commons Configuration) are defined in configuration files in a specific
+ format, a so called <em>Bean declaration</em>. Such a declaration contains
+ all information needed to create an instance of this bean class, e.g.
+ the full qualified name of the class and initialization parameters. We will
+ explain how a bean declaration looks like in short.
+ </p>
+ <p>
+ On the Java side three entities are involved in the creation of a bean:
+ <ul>
+ <li>A <em>bean factory</em>: This is an object that implements the
+ <code><a href="../apidocs/org/apache/commons/configuration/beanutils/BeanFactory.html">BeanFactory</a></code>
+ interface and knows how to create an instance of a bean class. In most
+ cases calling code does not directly deal with a bean factory.</li>
+ <li>An implementation of the
+ <code><a href="../apidocs/org/apache/commons/configuration/beanutils/BeanDeclaration.html">BeanDeclaration</a></code>
+ interface. This object knows how the bean declaration in the configuration
+ file is organized and how the needed information can be extracted. So
+ the way the bean is declared in the configuration file must match the
+ expectations of this object.</li>
+ <li>The utility class
+ <code><a href="../apidocs/org/apache/commons/configuration/beanutils/BeanHelper.html">BeanHelper</a></code>
+ brings all these together and performs the bean creation operation.
+ Usually client code will create a <code>BeanDeclaration</code> object
+ from a <code>Configuration</code> implementation and then pass it to
+ one of the <code>createBean()</code> methods of <code>BeanHelper</code>.
+ That's it!</li>
+ </ul>
+ For all of the interfaces mentioned above default implementations are
+ provided, which in many cases can be used out of the box.
+ </p>
+ </subsection>
+
+ <subsection name="An Example">
+ <p>
+ After this theory let's get into practice using an example. Consider a
+ GUI application that makes use of a <em>Window manager</em> to display
+ its windows and dialogs to the user. There is a <code>WindowManager</code>
+ interface containing methods for opening, displaying, hiding, and
+ disposing windows. Different implementations of this interface exist, e.g.
+ providing different look & feel or special functionality. The concrete
+ set of methods of the interface does not matter for this example.
+ </p>
+ <p>
+ Now in the application's configuration it shall be specified that the
+ concrete implementation <code>DefaultWindowManager</code> should be
+ used as <code>WindowManager</code>. This is a plain Java class implementing
+ the <code>WindowManager</code> interface. Some fragments are shown in
+ the following listing:
+ </p>
+<source><![CDATA[
+package examples.windows;
+
+public class DefaultWindowManager implements WindowManager
+{
+ // are windows allowed to be resized?
+ private boolean resizable;
+ // do windows have a close button?
+ private boolean closable;
+
+ // Default size of new windows
+ private int defaultWidth;
+ private int defaultHeight;
+
+ WindowStyleDefinition styleDefinition;
+
+ // getters and setters ommitted, also the WindowManager methods
+}
+]]></source>
+ <p>
+ As you can see, the <code>DefaultWindowManager</code> class has some
+ simple properties for defining the windows. There is also a property
+ named <code>StyleDefinition</code> whose type is another bean (such
+ a style definition may contain information about themes, colors, fonts
+ of the window and so on). How can we now write a configuration file so
+ that a bean of the <code>DefaultWindowManager</code> class is declared
+ and initialization properties are defined? In an XML configuration file
+ this will look as follows:
+ </p>
+<source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<config>
+ <gui>
+ <windowManager config-class="examples.windows.DefaultWindowManager"
+ closable="false" resizable="true" defaultWidth="400"
+ defaultHeight="250">
+ <styleDefinition config-class="examples.windows.WindowStyleDefinition"
+ backColor="#ffffff" foreColor="0080ff" iconName="myicon" />
+ </windowManager>
+ </gui>
+</config>
+]]></source>
+ <p>
+ This XML document contains a valid bean declaration starting with the
+ <code>windowManager</code> element and including its sub elements. Note
+ the following points:
+ <ul>
+ <li>The (full qualified) class of the bean is specified using the
+ <code>config-class</code> attribute. (Attributes starting with the
+ prefix "config-" are reserved; they contain special meta
+ data for the bean creation process.)</li>
+ <li>Other attributes of the <code>windowManager</code> element correspond
+ to properties of the <code>DefaultWindowManager</code> class. These
+ properties will be initialized with the values specified here.</li>
+ <li>For the <code>styleDefinition</code> property, which is itself a
+ bean, a sub element (matching the property's name) exists. The structure
+ of this element is analogous to the structure of the <code>windowManager</code>
+ element; indeed it could even have further sub elements defining
+ bean properties of the <code>WindowStyleDefinition</code> class.</li>
+ </ul>
+ The basic structure of a bean declaration should have become clear by
+ this example.
+ </p>
+ <p>
+ Now let's see how we can access this declaration and create an instance.
+ This is demonstrated in the code fragment below:
+ </p>
+<source><![CDATA[
+XMLConfiguration config = new XMLConfiguration("windowconfig.xml");
+BeanDeclaration decl = new XMLBeanDeclaration(config, "gui.windowManager");
+WindowManager wm = (WindowManager) BeanHelper.createBean(decl);
+]]></source>
+ <p>
+ This fragment loads the configuration file using a <code>XMLConfiguration</code>
+ object. Then a bean declaration object is created, in this case an
+ instance of the <code><a href="../apidocs/org/apache/commons/configuration/beanutils/XMLBeanDeclaration.html">XMLBeanDeclaration</a></code>
+ class, which can deal with bean declarations in XML documents. This
+ declaration is passed to the static <code>createBean()</code> method of
+ the <code><a href="../apidocs/org/apache/commons/configuration/beanutils/BeanHelper.html">BeanHelper</a></code>
+ class, which returns the new bean instance.
+ </p>
+ <p>
+ <code>BeanHelper</code> defines some overloaded versions of the
+ <code>createBean()</code> method. Some allow to pass in a default bean
+ class; then it is not necessary to define the class in the bean declaration
+ - an instance of this default class will be created if it is lacking in
+ the configuration file. If the bean cannot be created for some reason
+ (e.g. a wrong class name was specified), a <code>ConfigurationRuntimeException</code>
+ will be thrown.
+ </p>
+ </subsection>
+
+ <subsection name="Extending the Basic Mechanism">
+ <p>
+ As was pointed out in the introduction of this chapter support for creating
+ beans is focused on the basics. But there are some possibilities of hooking
+ in and add custom extensions. This can be done in the following ways:
+ <ul>
+ <li>By defining a custom <code>BeanDeclaration</code> implementation</li>
+ <li>By providing a custom <code>BeanFactory</code> implementation</li>
+ </ul>
+ </p>
+ <p>
+ A specialized bean declaration is needed when you have to deal with
+ configuration files that contain bean declarations in a different format
+ than the ones supported by the available default implementations. Then it
+ is the responsibility of your implementation to parse the configuration
+ data and extract the required information to create the bean. Basically
+ your <code><a href="../apidocs/org/apache/commons/configuration/beanutils/BeanDeclaration.html">BeanDeclaration</a></code>
+ implementation must be able to provide the following data:
+ <ul>
+ <li>The name of the class for which an instance is to be created.</li>
+ <li>The name of the bean factory that is used to create the bean. Here
+ <b>null</b> can be returned, then a default factory is used. (See
+ below for more information about working with custom bean factories.)</li>
+ <li>An optional parameter to be passed to the bean factory. If a factory
+ is used that supports additional parameters, the current parameter
+ values are also obtained from the bean declaration.</li>
+ <li>A map with the properties to be set on the newly created bean.
+ This map's keys are names of properties, its values are the corresponding
+ property values. The default bean factory will process this map and
+ call the corresponding setter methods on the newly created bean object.</li>
+ <li>A map with further <code>BeanDeclaration</code> objects for
+ initializing properties of the new bean that are itself beans. These
+ bean declarations are treated exactly as the one that is currently
+ processed. The resulting beans will then be set as properties on the
+ processed bean (the names of these properties are again obtained from
+ the keys of the map).</li>
+ </ul>
+ </p>
+ <p>
+ While creating a custom <code>BeanDeclaration</code> implementation
+ allows you to adapt the format of bean declarations in configuration files,
+ you can manipulate the bean creation mechanism itself by creating a
+ specialized implementation of the
+ <code><a href="../apidocs/org/apache/commons/configuration/beanutils/BeanFactory.html">BeanFactory</a></code>
+ interface. For this purpose the following steps are necessary:
+ <ol>
+ <li>Create a class implementing the <code>BeanFactory</code> interface.
+ This interface is quite simple. It defines one method for creating an
+ instance of a class whose <code>Class</code> object is provided, and
+ another method, which is called for querying a default class.</li>
+ <li>Register this new factory class at the <code>BeanHelper</code>
+ class.</li>
+ <li>In the bean declaration in your configuration file refer to the
+ factory that should be used for creating the bean.</li>
+ </ol>
+ </p>
+ <p>
+ We will provide an example that covers all these steps. This example
+ deals with a <em>singleton</em> factory, i.e. an implementation of
+ <code>BeanFactory</code> that returns always the same instance of a
+ provided bean class.
+ </p>
+ <p>
+ We start with the creation of the factory class. The basic idea is that
+ the functionality for creating and initializing beans is already provided
+ by the <code><a href="../apidocs/org/apache/commons/configuration/beanutils/DefaultBeanFactory.html">DefaultBeanFactory</a></code>
+ class, so we extend this class. Our implementation only has to deal with
+ the singleton stuff: We keep a map that stores already created bean
+ instances and can be accessed by the name of their classes. In the
+ factory's <code>createBean()</code> method we check if for the passed in
+ class already an instance exists. If this is the case, it is directly
+ returned. Otherwise we call the inherited <code>createBean()</code> method
+ and store its result in the map. (Note that this implementation is a bit
+ simplicistic; a real world implementation would also have to take the
+ initialization parameters into account. But for the purpose of an example
+ it should be good enough). Here is the code:
+ </p>
+<source><![CDATA[
+public class SingletonBeanFactory extends DefaultBeanFactory
+{
+ /** A map for the so far created instances.*/
+ private Map beans;
+
+ public SingletonBeanFactory()
+ {
+ super();
+ beans = new HashMap();
+ }
+
+ // Creates the bean. Checks if already an instance exists.
+ public synchronized Object createBean(Class beanClass, BeanDeclaration decl,
+ Object param) throws Exception
+ {
+ Object bean = beans.get(beanClass.getName());
+ if (bean != null)
+ {
+ // Yes, there is already an instance
+ return bean;
+ }
+ else
+ {
+ // No, create it now (done by the super class)
+ bean = super.createBean(beanClass, decl, param);
+ // Store it in map
+ beans.put(beanClass.getName(), bean);
+ return bean;
+ }
+ }
+}
+]]></source>
+ <p>
+ Note the <b>synchronized</b> key word, which is necessary because the
+ method can be accessed by multiple threads concurrently. Now we have to
+ register an instance of this class at the <code>BeanHelper</code> class.
+ This can be done in the initialization phase of your application and
+ looks as follows:
+ </p>
+<source><![CDATA[
+BeanHelper.registerBeanFactory("SINGLETON", new SingletonBeanFactory());
+]]></source>
+ <p>
+ To make use of the new factory a bean declaration must contain an
+ attribute that refers to the name under which the factory was registered.
+ This is demonstrated by the fragment below:
+ </p>
+<source><![CDATA[
+<config>
+...
+ <services>
+ <fileService config-class="my.package.services.FileServiceImpl"
+ config-factory="SINGLETON"
+ property1="value1" property2="value2">
+ <!-- Here can be nested bean declarations -->
+ </fileService>
+ ...
+</config>
+]]></source>
+ <p>
+ In this fragment the <code>fileService</code> element contains a bean
+ declaration for some service object. Apart from the <code>config-class</code>
+ attribute the important part is the <code>config-factory</code> attribute.
+ This attribute tells the <code>BeanHelper</code> class that it should
+ use a special factory when it processes this bean declaration. As was
+ demonstrated by this example, it should not be too difficult to extend
+ the custom mechanism for creating beans.
+ </p>
+ </subsection>
+ </section>
+</body>
+
+</document>
diff --git a/src/site/xdoc/userguide/howto_combinedconfiguration.xml b/src/site/xdoc/userguide/howto_combinedconfiguration.xml
new file mode 100644
index 0000000..d143d94
--- /dev/null
+++ b/src/site/xdoc/userguide/howto_combinedconfiguration.xml
@@ -0,0 +1,758 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<document>
+
+ <properties>
+ <title>Combined Configurations</title>
+ <author email="oheger at apache.com">Oliver Heger</author>
+ </properties>
+
+<body>
+ <section name="Combined Configuration">
+ <p>
+ The <code><a href="../apidocs/org/apache/commons/configuration/CombinedConfiguration.html">
+ CombinedConfiguration</a></code> class provides an alternative for handling
+ multiple configuration sources. Its API is very similar to the
+ <code>CompositeConfiguration</code> class, which was discussed in the
+ <a href="howto_compositeconfiguration.html#Composite Configuration Details">last
+ section</a>. There are the following differences however:
+ </p>
+ <p>
+ <ul>
+ <li>A <code>CombinedConfiguration</code> is a truely
+ <a href="howto_xml.html#Hierarchical properties">hierarchical
+ configuration</a>. This means that all the enhanced facilities
+ provided by <code>HierarchicalConfiguration</code> (e.g. expression
+ engines) can be used.</li>
+ <li>A <code>CombinedConfiguration</code> is not limited to implementing
+ an override semantics for the properties of the contained configurations.
+ Instead it has the concept of so-called <em>node combiners</em>, which
+ know how properties of multiple configuration sources can be combined.
+ Node combiners are discussed later in detail. For instance, there is a
+ node combiner implementation available that constructs a union of the
+ contained configurations.</li>
+ <li>Contained configurations can be assigned a name. They can later be
+ accessed by their name.</li>
+ <li>Each contained configuration can have an optional prefix. Its
+ properties are then added under this prefix to the combined
+ configuration.</li>
+ <li>There is no concept of an <em>in memory configuration</em>. Changes
+ to a combined configuration are handled in a different way.</li>
+ </ul>
+ </p>
+
+ <subsection name="How it works">
+ <p>
+ A <code>CombinedConfiguration</code> provides a logic view on the
+ properties of the configurations it contains. This view is determined
+ by the associated node combiner object. Because of that it must be
+ re-constructed whenever one of these contained configurations is
+ changed.
+ </p>
+ <p>
+ To achieve this, a <code>CombinedConfiguration</code> object registers
+ itself as an event listener at the configurations that are added to it.
+ It will then be notified for every modification that occurs. If such a
+ notification is received, the internally managed view is invalidated.
+ When a property of the combined configuration is to be accessed, the view
+ is checked whether it is valid. If this is the case, the property's value
+ can be directly fetched. Otherwise the associated node combiner is asked
+ to re-construct the view.
+ </p>
+ </subsection>
+
+ <subsection name="Node combiners">
+ <p>
+ A <em>node combiner</em> is an object of a class that inherits from the
+ abstract <code><a href="../apidocs/org/apache/commons/configuration/tree/NodeCombiner.html">NodeCombiner</a></code>
+ class. This class defines an abstract <code>combine()</code> method, which
+ takes the root nodes of two hierarchical configurations and returns the
+ root node of the combined node structure. It is up to a concrete
+ implementation how this combined structure will look like. Commons
+ Configuration ships with three concrete implementations
+ <code><a href="../apidocs/org/apache/commons/configuration/tree/OverrideCombiner.html">OverrideCombiner</a></code>,
+ <code><a href="../apidocs/org/apache/commons/configuration/tree/MergeCombiner.html">MergeCombiner</a></code>
+ and <code><a href="../apidocs/org/apache/commons/configuration/tree/UnionCombiner.html">UnionCombiner</a></code>,
+ which implement an override, merge, and union semantics respectively.
+ </p>
+ <p>
+ Constructing a combination of multiple node hierarchies is not a trivial
+ task. The available implementations descend the passed in node hierarchies
+ in a recursive manner to decide, which nodes have to be copied into the
+ resulting structure. Under certain circumstances two nodes of the source
+ structures can be combined into a single result node, but unfortunately
+ this process cannot be fully automated, but sometimes needs some hints
+ from the developer. As an example consider the following XML configuration
+ sources:
+ </p>
+ <source><![CDATA[
+<configuration>
+ <database>
+ <tables>
+ <table>
+ <name>users</name>
+ <fields>
+ <field>
+ <name>user_id</name>
+ </field>
+ ...
+ </fields>
+ </table>
+ </tables>
+ </database>
+</configuration>
+]]></source>
+ <p>and</p>
+ <source><![CDATA[
+<configuration>
+ <database>
+ <tables>
+ <table>
+ <name>documents</name>
+ <fields>
+ <field>
+ <name>document_id</name>
+ </field>
+ ...
+ </fields>
+ </table>
+ </tables>
+ </database>
+</configuration>
+]]></source>
+ <p>
+ These two configuration sources define database tables. Each source
+ defines one table. When constructing a union for these sources the result
+ should look as follows:
+ </p>
+ <source><![CDATA[
+<configuration>
+ <database>
+ <tables>
+ <table>
+ <name>users</name>
+ <fields>
+ <field>
+ <name>user_id</name>
+ </field>
+ ...
+ </fields>
+ </table>
+ <table>
+ <name>documents</name>
+ <fields>
+ <field>
+ <name>document_id</name>
+ </field>
+ ...
+ </fields>
+ </table>
+ </tables>
+ </database>
+</configuration>
+]]></source>
+ <p>
+ As you can see, the resulting structure contains two <code>table</code>
+ nodes while the nodes <code>database</code> and <code>tables</code> appear
+ only once. For a human being this is quite logic because <code>database</code>
+ and <code>tables</code> define the overall structure of the configuration
+ data, and there can be multiple tables. A node combiner however does not
+ know anything about structure nodes, list nodes, or whatever. From its point of
+ view there is no detectable difference between the <code>tables</code>
+ nodes and the <code>table</code> nodes in the source structures: both
+ appear once in each source file and have no values. So without any
+ assistance the result constructed by the <code>UnionCombiner</code> when
+ applied on the two example sources would be a bit different:
+ </p>
+ <source><![CDATA[
+<configuration>
+ <database>
+ <tables>
+ <table>
+ <name>users</name>
+ <fields>
+ <field>
+ <name>user_id</name>
+ </field>
+ ...
+ </fields>
+ <name>documents</name>
+ <fields>
+ <field>
+ <name>document_id</name>
+ </field>
+ ...
+ </fields>
+ </table>
+ </tables>
+ </database>
+</configuration>
+]]></source>
+ <p>
+ Note that the <code>table</code> node would be considered a structure
+ node, too, and would not be duplicated. This is probably not what was
+ desired. To deal with such situations it is possible to tell the node
+ combiner that certain nodes are list nodes and thus should not be
+ combined. So in this concrete example the <code>table</code> node should
+ be declared as a list node, then we would get the expected result. We will
+ see below how this is done. Note that this explicit declaration of a list
+ node is necessary only in situations where there is ambiguity. If in one
+ of our example configuration sources multiple tables had been defined, the
+ node combiner would have concluded itself that <code>table</code> is a list
+ node and would have acted correspondigly.
+ </p>
+ <p>
+ The examples the follow are provided to further illustrate the differences
+ between the combiners that are delivered with Commons Configuration. The first
+ two files are the files that will be combined.
+ </p>
+ <table border='0'>
+ <tr>
+ <th width="50%">testfile1.xml</th>
+ <th width="50%">testfile2.xml</th>
+ </tr>
+ <tr><td width="50%">
+<source><![CDATA[<config>
+ <gui>
+ <bgcolor>green</bgcolor>
+ <selcolor>yellow</selcolor>
+ <level default="2">1</level>
+ </gui>
+ <net>
+ <proxy>
+ <url>http://www.url1.org</url>
+ <url>http://www.url2.org</url>
+ <url>http://www.url3.org</url>
+ </proxy>
+ <service>
+ <url>http://service1.org</url>
+ </service>
+ <server>
+ </server>
+ </net>
+ <base>
+ <services>
+ <security>
+ <login>
+ <user>Admin</user>
+ <passwd type="secret"/>
+ </login>
+ </security>
+ </services>
+ </base>
+ <database>
+ <tables>
+ <table id="1">
+ <name>documents</name>
+ <fields>
+ <field>
+ <name>docid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>docname</name>
+ <type>varchar</type>
+ </field>
+ <field>
+ <name>authorID</name>
+ <type>int</type>
+ </field>
+ </fields>
+ </table>
+ </tables>
+ </database>
+ <Channels>
+ <Channel id="1" type="half">
+ <Name>My Channel</Name>
+ </Channel>
+ <Channel id="2">
+ <MoreChannelData>more test 2 data</MoreChannelData>
+ </Channel>
+ <Channel id="3" type="half">
+ <Name>Test Channel</Name>
+ </Channel>
+ <Channel id="4">
+ <Name>Channel 4</Name>
+ </Channel>
+ </Channels>
+</config>
+]]></source></td><td width="50%">
+<source><![CDATA[<config>
+ <base>
+ <services>
+ <security>
+ <login>
+ <user type="default">scotty</user>
+ <passwd>BeamMeUp</passwd>
+ </login>
+ </security>
+ </services>
+ </base>
+ <gui>
+ <bgcolor>black</bgcolor>
+ <fgcolor>blue</fgcolor>
+ <level min="1">4</level>
+ </gui>
+ <net>
+ <server>
+ <url>http://appsvr1.com</url>
+ <url>http://appsvr2.com</url>
+ <url>http://testsvr.com</url>
+ <url>http://backupsvr.com</url>
+ </server>
+ <service>
+ <url type="2">http://service2.org</url>
+ <url type="2">http://service3.org</url>
+ </service>
+ </net>
+ <database>
+ <tables>
+ <table id="2">
+ <name>tasks</name>
+ <fields>
+ <field>
+ <name>taskid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>taskname</name>
+ <type>varchar</type>
+ </field>
+ </fields>
+ </table>
+ </tables>
+ </database>
+ <Channels>
+ <Channel id="1">
+ <Name>Channel 1</Name>
+ <ChannelData>test 1 data</ChannelData>
+ </Channel>
+ <Channel id="2" type="full">
+ <Name>Channel 2</Name>
+ <ChannelData>test 2 data</ChannelData>
+ </Channel>
+ <Channel id="3" type="full">
+ <Name>Channel 3</Name>
+ <ChannelData>test 3 data</ChannelData>
+ </Channel>
+ <Channel id="4" type="half">
+ <Name>Test Channel 1</Name>
+ </Channel>
+ <Channel id="4" type="full">
+ <Name>Test Channel 2</Name>
+ </Channel>
+ </Channels>
+</config>
+]]></source></td></tr></table>
+ <p>
+ The first listing shows the result of using the <code>OverrideCombiner</code>.
+ </p>
+ <table>
+ <tr><th width="40%">OverrideCombiner Results</th><th width="60%">Notes</th></tr>
+ <tr><td width="40%">
+ <source><![CDATA[<config>
+ <gui>
+ <bgcolor>green</bgcolor>
+ <selcolor>yellow</selcolor>
+ <level default='2' min='1'>1</level>
+ <fgcolor>blue</fgcolor>
+ </gui>
+ <net>
+ <proxy>
+ <url>http://www.url1.org</url>
+ <url>http://www.url2.org</url>
+ <url>http://www.url3.org</url>
+ </proxy>
+ <service>
+ <url>http://service1.org</url>
+ </service>
+ <server>
+ <url>http://appsvr1.com</url>
+ <url>http://appsvr2.com</url>
+ <url>http://testsvr.com</url>
+ <url>http://backupsvr.com</url>
+ </server>
+ </net>
+ <base>
+ <services>
+ <security>
+ <login>
+ <user type='default'>Admin</user>
+ <passwd type='secret'>BeamMeUp</passwd>
+ </login>
+ </security>
+ </services>
+ </base>
+ <database>
+ <tables>
+ <table id='1'>
+ <name>documents</name>
+ <fields>
+ <field>
+ <name>docid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>docname</name>
+ <type>varchar</type>
+ </field>
+ <field>
+ <name>authorID</name>
+ <type>int</type>
+ </field>
+ </fields>
+ </table>
+ </tables>
+ </database>
+ <Channels>
+ <Channel id='1' type='half'>
+ <Name>My Channel</Name>
+ </Channel>
+ <Channel id='2'>
+ <MoreChannelData>more test 2 data</MoreChannelData>
+ </Channel>
+ <Channel id='3' type='half'>
+ <Name>Test Channel</Name>
+ </Channel>
+ </Channels>
+</config>
+]]></source></td><td width="60%">
+ <p>
+ The features that are significant in this file are:
+ <ul>
+ <li>In the gui section each of the child elements only appears once. The level element
+ merges the attributes from the two files and uses the element value of the first file.</li>
+ <li>In the security section the user type attribute was obtained from the second file
+ while the user value came from the first file. Alternately, the password type was
+ obtained from the first file while the value came from the second.</li>
+ <li>Only the data from table 1 was included.</li>
+ <li>Channel 1 in the first file completely overrode Channel 1 in the second file.</li>
+ <li>Channel 2 in the first file completely overrode Channel 2 in the second file. While
+ the attributes were merged in the case of the login elements the type attribute
+ was not merged in this case.</li>
+ <li>Again, only Channel 3 from the first file was included.</li>
+ </ul>
+ </p>
+ <p>
+ How the Channel elements ended up may not at first be obvious. The <code>OverrideCombiner</code>
+ simply noticed that the Channels element had three child elements named Channel and
+ used that to determine that only the contents of the Channels element in the first file
+ would be used.
+ </p></td></tr></table>
+ <p>
+ The next file is the the result of using the <code>UnionCombiner</code>
+ </p>
+ <table>
+ <tr>
+ <th width="40%">UnionCombiner Results</th>
+ <th width="60%">Notes</th>
+ </tr>
+ <tr><td width="40%">
+ <source><![CDATA[<config>
+ <gui>
+ <bgcolor>green</bgcolor>
+ <selcolor>yellow</selcolor>
+ <level default='2'>1</level>
+ <bgcolor>black</bgcolor>
+ <fgcolor>blue</fgcolor>
+ <level min='1'>4</level>
+ </gui>
+ <net>
+ <proxy>
+ <url>http://www.url1.org</url>
+ <url>http://www.url2.org</url>
+ <url>http://www.url3.org</url>
+ </proxy>
+ <service>
+ <url>http://service1.org</url>
+ <url type='2'>http://service2.org</url>
+ <url type='2'>http://service3.org</url>
+ </service>
+ <server></server>
+ <server>
+ <url>http://appsvr1.com</url>
+ <url>http://appsvr2.com</url>
+ <url>http://testsvr.com</url>
+ <url>http://backupsvr.com</url>
+ </server>
+ </net>
+ <base>
+ <services>
+ <security>
+ <login>
+ <user>Admin</user>
+ <passwd type='secret'></passwd>
+ <user type='default'>scotty</user>
+ <passwd>BeamMeUp</passwd>
+ </login>
+ </security>
+ </services>
+ </base>
+ <database>
+ <tables>
+ <table id='1' id='2'>
+ <name>documents</name>
+ <fields>
+ <field>
+ <name>docid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>docname</name>
+ <type>varchar</type>
+ </field>
+ <field>
+ <name>authorID</name>
+ <type>int</type>
+ </field>
+ <field>
+ <name>taskid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>taskname</name>
+ <type>varchar</type>
+ </field>
+ </fields>
+ <name>tasks</name>
+ </table>
+ </tables>
+ </database>
+ <Channels>
+ <Channel id='1' type='half'>
+ <Name>My Channel</Name>
+ </Channel>
+ <Channel id='2'>
+ <MoreChannelData>more test 2 data</MoreChannelData>
+ </Channel>
+ <Channel id='3' type='half'>
+ <Name>Test Channel</Name>
+ </Channel>
+ <Channel id='1'>
+ <Name>Channel 1</Name>
+ <ChannelData>test 1 data</ChannelData>
+ </Channel>
+ <Channel id='2' type='full'>
+ <Name>Channel 2</Name>
+ <ChannelData>test 2 data</ChannelData>
+ </Channel>
+ <Channel id='3' type='full'>
+ <Name>Channel 3</Name>
+ <ChannelData>test 3 data</ChannelData>
+ </Channel>
+ </Channels>
+</config>
+]]></source></td><td width="60%">
+ <p>
+ The feature that is significant in this file is rather obvious. It is just a simple
+ union of the contents of the two files.
+ </p>
+ </td></tr></table>
+ <p>
+ Finally, the last file is the result of using the <code>MergeCombiner</code>
+ </p>
+ <table>
+ <tr>
+ <th width="40%">MergeCombiner Results</th>
+ <th width="60%">Notes</th>
+ </tr>
+ <tr><td width="40%">
+<source><![CDATA[<config>
+ <gui>
+ <bgcolor>green</bgcolor>
+ <selcolor>yellow</selcolor>
+ <level default='2' min='1'>1</level>
+ <fgcolor>blue</fgcolor>
+ </gui>
+ <net>
+ <proxy>
+ <url>http://www.url1.org</url>
+ <url>http://www.url2.org</url>
+ <url>http://www.url3.org</url>
+ </proxy>
+ <service>
+ <url>http://service1.org</url>
+ </service>
+ <server>
+ <url>http://appsvr1.com</url>
+ <url>http://appsvr2.com</url>
+ <url>http://testsvr.com</url>
+ <url>http://backupsvr.com</url>
+ </server>
+ </net>
+ <base>
+ <services>
+ <security>
+ <login>
+ <user type='default'>Admin</user>
+ <passwd type='secret'></passwd>
+ </login>
+ </security>
+ </services>
+ </base>
+ <database>
+ <tables>
+ <table id='1'>
+ <name>documents</name>
+ <fields>
+ <field>
+ <name>docid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>docname</name>
+ <type>varchar</type>
+ </field>
+ <field>
+ <name>authorID</name>
+ <type>int</type>
+ </field>
+ </fields>
+ </table>
+ <table id='2'>
+ <name>tasks</name>
+ <fields>
+ <field>
+ <name>taskid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>taskname</name>
+ <type>varchar</type>
+ </field>
+ </fields>
+ </table>
+ </tables>
+ </database>
+ <Channels>
+ <Channel id='1' type='half'>
+ <Name>My Channel</Name>
+ <ChannelData>test 1 data</ChannelData>
+ </Channel>
+ <Channel id='2' type='full'>
+ <MoreChannelData>more test 2 data</MoreChannelData>
+ <Name>Channel 2</Name>
+ <ChannelData>test 2 data</ChannelData>
+ </Channel>
+ <Channel id='3' type='half'>
+ <Name>Test Channel</Name>
+ </Channel>
+ <Channel id='3' type='full'>
+ <Name>Channel 3</Name>
+ <ChannelData>test 3 data</ChannelData>
+ </Channel>
+ </Channels>
+</config>
+]]></source></td><td width="60%">
+ <p>
+ The features that are significant in this file are:
+ <ul>
+ <li>In the gui section the elements were merged.</li>
+ <li>In the net section the elements were merged, with the exception of the urls.</li>
+ <li>In the security section the user and password were merged. Notice that the
+ empty value for the password from the first file overrode the password in the
+ second file.</li>
+ <li>Both table elements appear</li>
+ <li>Channel 1 and Channel 2 were merged</li>
+ <li>Both Channel 3 elements appear as they were determined to not be the same.</li>
+ </ul>
+ </p>
+ <p>
+ When merging elements attributes play a critical role. If an element has an attribute that
+ appears in both sources, the value of that attribute must be the same for the elements to be
+ merged.
+ </p>
+ <p>
+ Merging is only allowed between a single node in each of the files, so if an element
+ in the first file matches more than one element in the second file no merging will take
+ place and the element from the first file (and its contents) are included and the elements
+ in the second file are not. If the element is marked as a list node then the elements from
+ the second file will also be included.
+ </p></td></tr></table>
+ </subsection>
+
+ <subsection name="Constructing a CombinedConfiguration">
+ <p>
+ To create a <code>CombinedConfiguration</code> object you specify the node
+ combiner to use and then add an arbitrary number of configurations. We will
+ show how to construct a union configuration from the two example sources
+ introduced earlier:
+ </p>
+<source><![CDATA[
+// Load the source configurations
+XMLConfiguration conf1 = new XMLConfiguration("table1.xml");
+XMLConfiguration conf2 = new XMLConfiguration("table2.xml");
+
+// Create and initialize the node combiner
+NodeCombiner combiner = new UnionCombiner();
+combiner.addListNode("table"); // mark table as list node
+ // this is needed only if there are ambiguities
+
+// Construct the combined configuration
+CombinedConfiguration cc = new CombinedConfiguration(combiner);
+cc.addConfiguration(conf1, "tab1");
+cc.addConfiguration(conf2);
+]]></source>
+ <p>
+ Here we also specified a name for one of the configurations, so it can
+ later be accessed by <code>cc.getConfiguration("tab1");</code>. Access by
+ index is also supported. After that the properties in the combined
+ configuration can be accessed as if it were a normal hierarchical
+ configuration
+ </p>
+ </subsection>
+
+ <subsection name="Dealing with changes">
+ <p>
+ There is nothing that prevents you from updating a combined configuration,
+ e.g. by calling methods like <code>addProperty()</code> or
+ <code>removeProperty()</code>. The problem is that depending on the used
+ node combiner it might no be clear, which of the contained configurations
+ will be modified or whether one is modified at all.
+ </p>
+ <p>
+ Typical node combiners work by copying parts of the node structures of
+ the source configurations into the target structure and linking them
+ togehter using special link nodes. So updates of the combined node structure
+ will either effect nodes from one of the contained configuration (then
+ the changes are directly visible in this configuration) or one of the link
+ nodes (then they cannot really be saved).
+ </p>
+ <p>
+ It is also possible that a change is done at the combined node structure,
+ which is not compatible with the current node combiner. Imagine that an
+ <code>OverrideCombiner</code> is used and that a
+ property should be removed. This property will then be removed from one
+ of the contained configurations. Now it may happen that this removed
+ property had hidden property values of other contained configurations.
+ Their values won't become visible automatically, but only after the
+ combined view was re-constructed.
+ </p>
+ <p>
+ Because of that it is recommended that changes are not done at the
+ combined configuration, but only at contained configurations. This way
+ the correct configuration to be updated can unambigously be identified.
+ Obtaining the configuration to be updated from the combined configuration
+ is easy when it was given a name.
+ </p>
+ </subsection>
+ </section>
+</body>
+
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/userguide/howto_compositeconfiguration.xml b/src/site/xdoc/userguide/howto_compositeconfiguration.xml
new file mode 100644
index 0000000..e1e6ed2
--- /dev/null
+++ b/src/site/xdoc/userguide/howto_compositeconfiguration.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<document>
+
+ <properties>
+ <title>Composite Configuration Details</title>
+ <author email="epugh at opensourceconnections.com">Eric Pugh</author>
+ </properties>
+
+<body>
+
+ <section name="Composite Configuration Details">
+ <p>
+ There are many use cases when you want to collect the properties
+ of several configuration sources and access them like a single
+ configuration object. One way to do that is using the
+ <code><a href="../apidocs/org/apache/commons/configuration/CompositeConfiguration.html">
+ CompositeConfiguration</a></code> class.
+ </p>
+ <p>
+ A <code>CompositeConfiguration</code> object contains a list of
+ other configuration objects. When properties are accessed from a
+ composite configuration the object takes the passed in property key
+ and iterates over the list of the contained configurations. As soon
+ as a value is found for the key it is returned. This means that a
+ <code>CompositeConfiguration</code> implements a kind of override
+ semantics, i.e. the properties of configurations that were added
+ first hide the property values of configurations added later.
+ </p>
+ <p>
+ We will discuss how you can establish a "default" choice for your
+ Composite Configuration as well as save changes made to your
+ Composite Configuration.
+ </p>
+ <subsection name="Setting Up Defaults">
+ <p>
+ Defaults are very simple. You can just add them as your last configuration object,
+ either through the ConfigurationFactory or manually:
+ </p>
+ <source><![CDATA[
+Configuration defaults = new PropertiesConfiguration(fileToDefaults);
+Configuration otherProperties = new PropertiesConfiguration(fileToOtherProperties);
+CompositeConfiguration cc = new CompositeConfiguration();
+cc.addConfiguration(otherProperties);
+cc.addConfiguration(fileToDefaults);
+]]></source>
+ </subsection>
+
+ <subsection name="Saving Changes">
+ <p>
+ If you have a non static Configuration where you want to
+ save changes made to a configuration, and you are using a
+ CompositeConfiguration, then you will need to pass into
+ the constructor of the CompositeConfiguration what Configuration
+ to save the changes via.
+ </p>
+ <source><![CDATA[
+PropertiesConfiguration saveConfiguration = new PropertiesConfiguration(fileToSaveChangesIn);
+Configuration cc = new CompositeConfiguration(saveConfiguration);
+cc.setProperty("newProperty","new value");
+
+saveConfiguration.save();
+]]></source>
+ <p>
+ Alternatively, you can just request the
+ in-memory configuration that stores the changes. The following
+ example fragment copies all properties from the in-memory
+ configuration into a <code>DatabaseConfiguration</code>, so
+ that they are made persistent:
+ <source><![CDATA[
+Configuration changes = myCompositeConfiguration.getInMemoryConfiguration();
+DatabaseConfiguration config = new DatabaseConfiguration(datasource, "configuration", "key", "value");
+for (Iterator<String> i = changes.getKeys(); i.hasNext()){
+ String key = i.next();
+ Object value = changes.get(key);
+ config.setProperty(key,value);
+}
+]]></source>
+ </p>
+
+ </subsection>
+ </section>
+</body>
+
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/userguide/howto_configurationbuilder.xml b/src/site/xdoc/userguide/howto_configurationbuilder.xml
new file mode 100644
index 0000000..cc90ea1
--- /dev/null
+++ b/src/site/xdoc/userguide/howto_configurationbuilder.xml
@@ -0,0 +1,894 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- $Id: howto_configurationbuilder.xml 1156501 2011-08-11 06:22:49Z oheger $ -->
+
+<document>
+
+ <properties>
+ <title>Configuration Builder Howto</title>
+ </properties>
+
+<body>
+ <section name="Using DefaultConfigurationBuilder">
+ <p>
+ This section explains how a
+ <code><a href="../apidocs/org/apache/commons/configuration/DefaultConfigurationBuilder.html">
+ DefaultConfigurationBuilder</a></code>object is setup that provides
+ access to a collection of different configuration sources.
+ <code>DefaultConfigurationBuilder</code> is the option of choice for
+ applications that have to deal with multiple configuration sources. It
+ provides the following features:
+ <ul>
+ <li>Various configuration sources can be combined to a single
+ <a href="howto_combinedconfiguration.html#Combined Configuration">
+ CombinedConfiguration</a> object. This is a truly hierarchical
+ configuration supporting enhanced query facilities.</li>
+ <li>As configuration sources the most relevant <code>Configuration</code>
+ implementations provided by this library are supported. Sources are
+ defined as <a href="howto_beans.html#Declaring and Creating Beans">bean
+ declarations</a>, so complex initializations are possible.</li>
+ <li>Meta data can be provided to fine-tune the constructed
+ configuration.</li>
+ <li><code>DefaultConfigurationBuilder</code> is extensible. Custom
+ configuration sources can be added.</li>
+ </ul>
+ </p>
+ <p>
+ This document starts with some explanations of
+ <code>DefaultConfigurationBuilder</code> basics. Then the <em>configuration
+ definition files</em> processed by <code>DefaultConfigurationBuilder</code>
+ are discussed in detail. Finally an advanced example is presented.
+ </p>
+
+ <subsection name="The configuration definition file">
+ <p>
+ In previous chapters we have already seen how specific configuration
+ classes like <code>PropertiesConfiguration</code> or
+ <code>XMLConfiguration</code> can be used to load configuration data from
+ a single source. This may be sufficient for small applications, but if
+ requirements for configuration become more complex, additional support
+ for managing a set of different configuration sources is desired. This is
+ the domain of <code>DefaultConfigurationBuilder</code> which allows
+ combining multiple configuration sources. The properties defined in these
+ sources can then be accessed as if they were defined in a single
+ configuration file. The sources to be loaded have to be defined in a
+ XML document with a specific structure, a so-called <em>configuration
+ definition file</em>. The following listing shows a simple example of such
+ a definition file:
+ </p>
+ <source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+
+<configuration>
+ <properties fileName="usergui.properties"/>
+</configuration>
+]]></source>
+ <p>
+ A configuration definition file can contain an arbitrary number of
+ elements declaring the configuration sources to load. The
+ <code><properties></code> element is one of these; it is used to
+ include properties files. For this example we store the definition file
+ in the same directory as the properties file and call it
+ <code>config.xml</code>. The properties file used in this example is the
+ same as in the section about <a href="howto_properties.html">properties
+ files</a>.
+ </p>
+ </subsection>
+
+ <subsection name="Setting up a DefaultConfigurationBuilder">
+ <p>
+ Now we have to create a <code>DefaultConfigurationBuilder</code> object
+ and let it read this definition file. This is quite simple: Just create a
+ new instance and set the name of the definition file
+ (<code>DefaultConfigurationBuilder</code> is derived from
+ <code>XMLConfiguration</code>, so all options for specifying the document
+ to load are available here, too). The combined configuration collecting
+ all sources defined in the configuration definition file can then be
+ obtained by calling the <code>getConfiguration()</code> method:
+ </p>
+ <source><![CDATA[
+DefaultConfigurationBuilder builder = new DefaultConfigurationBuilder();
+builder.setFile(new File("config.xml"));
+Configuration config = builder.getConfiguration(true);
+]]></source>
+ <p>
+ Now the <em>config</em> object can be accessed in the usual way to query
+ configuration properties, e.g. by using methods like <code>getString()</code>,
+ or <code>getInt()</code>. We will see in a moment how properties defined
+ in different configuration sources are accessed.
+ </p>
+ </subsection>
+
+ <subsection name="Overriding properties">
+ <p>
+ Using <code>DefaultConfigurationBuilder</code> to collect configuration
+ sources does not make much sense if there is only a single source to be
+ loaded. So let's add another one! This time we will embedd a XML file:
+ <em>gui.xml</em> which is shown in the next listing:
+ </p>
+ <source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<gui-definition>
+ <colors>
+ <background>#808080</background>
+ <text>#000000</text>
+ <header>#008000</header>
+ <link normal="#000080" visited="#800080"/>
+ </colors>
+ <rowsPerPage>15</rowsPerPage>
+</gui-definition>
+]]></source>
+ <p>
+ To make this XML document part of our global configuration we have to
+ modify our configuration definition file to also include the new file. For
+ XML documents the element <code><xml></code> can be used so that we
+ ave now:
+ </p>
+ <source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+
+<configuration>
+ <properties fileName="usergui.properties"/>
+ <xml fileName="gui.xml"/>
+</configuration>
+]]></source>
+ <p>
+ The code for setting up the <code>DefaultConfigurationBuilder</code>
+ object remains the same. From the <code>Configuration</code> object
+ returned by the factory the new properties can be accessed in the usual
+ way.
+ </p>
+ <p>
+ There is one problem with this example configuration setup: The
+ <code>color.background</code> property is defined in both the properties
+ and the XML file, and - to make things worse - with different values.
+ Which value will be returned by a call to <code>getString()</code>?
+ </p>
+ <p>
+ The answer is that the configuration sources are searched in the order
+ they are defined in the configuration definition file. Here the properties
+ file is included first, then comes the XML file. Because the
+ <code>color.background</code> property can be found in the properties file
+ the value specified there will be returned (which happens to be
+ <code>#FFFFFF</code>).
+ </p>
+ <p>
+ It might not be obvious why it makes sense to define the value of one and
+ the same property in multiple configuration sources. But consider the
+ following scenario: An application comes with a set of default properties
+ and allows the user to override some or all of them. This can now easily
+ be realized by saving the user's settings in one file and the default
+ settings in another. Then in the configuration definition file the file
+ with the user settings is included first and after that the file with the
+ default values. The application code that queries these settings needs no
+ be aware whether a property was overriden by the user. <code>DefaultConfigurationBuilder</code>
+ takes care that properties defined in the first file (the user file) are
+ found; other properties which the user has not changed will still be
+ returned from the second file (the defaults file).
+ </p>
+ </subsection>
+
+ <subsection name="Optional configuration sources">
+ <p>
+ The example above with two configuration sources - one for user settings
+ and one with default values - raises an interesting question: What happens
+ if the user has not defined specific properties yet? Or what if a new user
+ starts our application for the first time and thus no user specific
+ properties exist?
+ </p>
+ <p>
+ The default behavior of <code>DefaultConfigurationBuilder</code> is to
+ throw a <code>ConfigurationException</code> exception if one of the sources
+ defined in the configuration definition file cannot be loaded. For our
+ example this behavior is not desired: the properties file with specific user
+ settings is not required. If it cannot be loaded, the example application
+ should still work because a complete set of configuration properties is
+ defined in the second file.
+ </p>
+ <p>
+ <code>DefaultConfigurationBuilder</code> supports such optional configuration
+ sources. For this purpose in the definition of a configuration source the
+ <code>config-optional</code> attribute can be placed. An example of this
+ is shown below:
+ </p>
+ <source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+
+<configuration>
+ <properties fileName="usersettings.properties" config-optional="true"/>
+ <properties fileName="default.properties"/>
+</configuration>
+]]></source>
+ <p>
+ In this configuration definition file the first properties file with user
+ specific settings is marked as optional. This means that if it cannot be
+ loaded, <code>DefaultConfigurationBuilder</code> will not throw an exception,
+ but only write a warning message to its logger. Note that the
+ <code>config-optional</code> attribute is absent for the second properties
+ file. Thus it is mandatory, and the <code>getConfiguration()</code> method
+ of <code>DefaultConfigurationBuilder</code> would throw an exception if it
+ could not be found.
+ </p>
+ </subsection>
+
+ <subsection name="Union configuration">
+ <p>
+ In an earlier section about the configuration definition file for
+ <code>DefaultConfigurationBuilder</code> it was stated that configuration
+ files included first can override properties in configuraton files
+ included later, and an example use case for this behaviour was given. There
+ may be cases when there are other requirements.
+ </p>
+ <p>
+ Let's continue the example with the application that somehow process
+ database tables and that reads the definitions of the affected tables from
+ its configuration. This example and the corresponding XML configuration
+ files were introduced in the section about <a href="howto_xml.html">XMLConfiguration</a>.
+ Now consider that this application grows larger and must be maintained by
+ a team of developers. Each developer works on a separated set of tables.
+ In such a scenario it would be problematic if the definitions for all
+ tables would be kept in a single file. It can be expected that this file
+ needs to be changed very often and thus can be a bottleneck for team
+ development when it is nearly steadily checked out. It would be much better
+ if each developer had an associated file with table definitions and all
+ these information could be linked together at the end.
+ </p>
+ <p>
+ <code>DefaultConfigurationBuilder</code> provides support for such a use case,
+ too. It is possible to specify in the configuration definition file that
+ from a set of configuration sources a logic union configuration is to be
+ constructed. Then all properties defined in the provided sources are
+ collected and can be accessed as if they had been defined in a single source.
+ To demonstrate this feature let us assume that a developer of the database
+ application has defined a specific XML file with a table definition named
+ <code>tasktables.xml</code>:
+ </p>
+ <source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+
+<config>
+ <table tableType="application">
+ <name>tasks</name>
+ <fields>
+ <field>
+ <name>taskid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>name</name>
+ <type>java.lang.String</type>
+ </field>
+ <field>
+ <name>description</name>
+ <type>java.lang.String</type>
+ </field>
+ <field>
+ <name>responsibleID</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>creatorID</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>startDate</name>
+ <type>java.util.Date</type>
+ </field>
+ <field>
+ <name>endDate</name>
+ <type>java.util.Date</type>
+ </field>
+ </fields>
+ </table>
+</config>
+]]></source>
+ <p>
+ This file defines the structure of an additional table, which should be
+ added to the so far existing table definitions. To achieve this the
+ configuration definition file has to be changed: A new section is added
+ that contains the include elements of all configuration sources which
+ are to be combined.
+ </p>
+ <source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!-- Configuration definition file that demonstrates the
+ override and additional sections -->
+
+<configuration>
+ <override>
+ <properties fileName="usergui.properties"/>
+ <xml fileName="gui.xml"/>
+ </override>
+
+ <additional>
+ <xml fileName="tables.xml"/>
+ <xml fileName="tasktables.xml" config-at="tables"/>
+ </additional>
+</configuration>
+]]></source>
+ <p>
+ Compared to the older versions of this file some changes have been done.
+ One major difference is that the elements for including configuration
+ sources are no longer direct children of the root element, but are now
+ contained in either an <code><override></code> or <code><additional></code>
+ section. The names of these sections already imply their purpose.
+ </p>
+ <p>
+ The <code>override</code> section is not strictly necessary. Elements in
+ this section are treated as if they were children of the root element, i.e.
+ properties in the included configuration sources override properties in
+ sources included later. So the <code><override></code> tags could have
+ been ommitted, but for the sake of clearity it is recommended to use them
+ if there is also an <code><additional></code> section.
+ </p>
+ <p>
+ It is the <code><additonal></code> section that introduces a new behaviour.
+ All configuration sources listed here are combined to a union configuration.
+ In our example we have put two <code>xml</code> elements in this area
+ that load the available files with database table definitions. The syntax
+ of elements in the <code>additional</code> section is analogous to the
+ syntax described so far. In this example the <code>config-at</code>
+ attribute is introduced. It specifies the position in the logic union
+ configuration where the included properties are to be added. Here it is set
+ for the second element to the value <em>tables</em>. This is because the
+ file starts with a <code><table></code> element, but to be compatible
+ with the other table definition file it should be accessable under the key
+ <code>tables.table</code>.
+ </p>
+ <p>
+ After these modifications have been performed, the configuration obtained
+ from <code>DefaultConfigurationBuilder</code> allows access to three database
+ tables. A call of <code>config.getString("tables.table(2).name");</code>
+ results in a value of <em>tasks</em>. In an analogous way it is possible
+ to retrieve the fields of the third table.
+ </p>
+ <p>
+ Note that it is also possible to override properties defined in an
+ <code>additonal</code> section. This can be done by placing a configuration
+ source in the <code>override</code> section that defines properties that
+ are also defined in one of the sources listed in the <code>additional</code>
+ section. The example does not make use of that. Note also that the order of
+ the <code>override</code> and <code>additional</code> sections in a
+ configuration definition file does not matter. Sources in an <code>override</code>
+ section are always treated with higher priority (otherwise they could not
+ override the values of other sources).
+ </p>
+ </subsection>
+
+ <subsection name="Configuration definition file reference">
+ <p>
+ Configuration definition files are XML documents telling
+ <code>DefaultConfigurationBuilder</code> which configuration sources to
+ load and how to process them in order to create the resulting combined
+ configuration.
+ </p>
+ <p>
+ <strong>Overall structure of a configuration definition file</strong>
+ </p>
+ <p>
+ A configuration definition file for <code>DefaultConfigurationBuilder</code>
+ can contain three sections, all of which are optional. A skeleton looks as
+ follows:
+ </p>
+ <source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+
+<configuration systemProperties="path to property file">
+ <header>
+ <!-- Meta data about the resulting combined configuration -->
+ </header>
+ <override>
+ <!-- Configuration declarations with override semantics -->
+ </override>
+ <additional>
+ <!-- Configuration declarations that form a union configuration -->
+ </additional>
+</configuration>
+]]></source>
+ <p>
+ <strong>Declaring configuration sources</strong>
+ </p>
+ <p>
+ The <code>override</code> and <code>additional</code> sections have already
+ been introduced when the basics of <code>DefaultConfigurationBuilder</code>
+ were discussed. They contain declarations for the configuration sources to be
+ embedded. For convenience reasons it is also possible to declare
+ configuration sources outside these sections; they are then treated as if
+ they were placed inside the <code>override</code> section.
+ </p>
+ <p>
+ Each declaration of a configuration source is represented by an XML
+ element whose name determines the type of the configuration source.
+ Attributes or nested elements can be used to provide additional
+ configuration options for the sources to be included (e.g. a name of a
+ file to be loaded or a reloading strategy). Below is a list of all
+ tags which can be used out of the box:
+ </p>
+ <p>
+ <dl>
+ <dt>properties</dt>
+ <dd>With this element properties files can be included. The name of
+ the file to load is specified using the <code>fileName</code>
+ attribute. Which configuration class is created by this tag
+ depends on the extension of the file to load: If the extension
+ is ".xml", a <code>XMLPropertiesConfiguration</code> object is
+ created, which is able to process the XML properties format
+ introduced in Java 5.0. Otherwise a <code>PropertiesConfiguration</code>
+ object is created, the default reader for properties files.</dd>
+ <dt>xml</dt>
+ <dd>The <code>xml</code> element can be used to load XML configuration
+ files. It also uses the <code>fileName</code> attribute to
+ determine the name of the file to load and creates an instance
+ of <code>XMLConfiguration</code>.</dd>
+ <dt>jndi</dt>
+ <dd>As the name implies, with this element JNDI resources can be
+ included in the resulting configuration. Under the hood this is
+ done by an instance of the <code>JNDIConfiguration</code>
+ class. The <code>prefix</code> attribute can be used to
+ select a subset of the JNDI tree.</dd>
+ <dt>plist</dt>
+ <dd>The <code>plist</code> element allows to embedd configuration
+ files in the NeXT / OpenStep or Mac OS X format. Again the
+ name of the file to load is specified through the
+ <code>fileName</code> attribute. If a XML file is specified,
+ a <code>XMLPropertyListConfiguration</code> object is created
+ to process the file. Otherwise this task is delegated to a
+ <code>PropertyListConfiguration</code> instance.</dd>
+ <dt>system</dt>
+ <dd>With this element an instance of <code>SystemConfiguration</code>
+ is added to the resulting configuration allowing access to
+ system properties. <em>Note:</em> Using this element system properties
+ are directly made available. Alternatively the
+ interpolation features introduced in version 1.4 (see
+ <a href="howto_basicfeatures.html#Variable_Interpolation">
+ Variable Interpolation</a> for more details) can be used for referencing
+ system properties.</dd>
+ <dt>configuration</dt>
+ <dd>The <code>configuration</code> tag allows other configuration
+ definition files to be included. This makes it possible to nest these
+ definition files up to an arbitrary depth. In fact, this tag will
+ create another <code>DefaultConfigurationBuilder</code> object,
+ initialize it, and obtain the <code>CombinedConfiguation</code> from it.
+ This combined configuration will then be added to the resulting
+ combined configuration. Like all file-based configurations the
+ <code>fileName</code> attribute can be used to specify the configuration
+ definition file to be loaded. This file must be an XML document that
+ conforms to the format described here. Some of the most important
+ settings are copied from the original <code>DefaultConfigurationBuilder</code>
+ object to the newly created builder:
+ <ul>
+ <li>the base path under which configuration files are searched</li>
+ <li>some flags, e.g. for controlling delimiter parsing or throwing
+ exceptions on missing properties</li>
+ <li>the logger</li>
+ <li>the configuration and error listeners</li>
+ </ul>
+ </dd>
+ <dt>ini</dt>
+ <dd>This tag can be used to include an ini file into the resulting
+ combined configuration. Behind the scenes an instance of
+ <code><a href="../apidocs/org/apache/commons/configuration/HierarchicalINIConfiguration.html">
+ HierarchicalINIConfiguration</a></code> is used to load the ini file.</dd>
+ <dt>env</dt>
+ <dd>With this tag direct access to environment properties can be enabled.
+ This works in the same way as the <code><system></code> tag for
+ Java system properties.</dd>
+ </dl>
+ </p>
+ <p>
+ In the declaration of a configuration source it is possible to set
+ properties on the corresponding configuration objects. Configuration
+ declarations are indeed
+ <a href="howto_beans.html#Declaring and Creating Beans">Bean
+ declarations</a>. That means they can have attributes matching simple
+ properties of the configuration object to create, and sub elements
+ matching complex properties. The following example fragment shows how
+ complex initialization can be performed in a configuration declaration:
+ </p>
+ <source><![CDATA[
+ <properties fileName="test.properties" throwExceptionOnMissing="true">
+ <reloadingStrategy refreshDelay="10000"
+ config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"/>
+ </properties>
+ <xml fileName="test.xml" delimiterParsingDisabled="true">
+ <expressionEngine config-class="org.apache.commons.configuration.tree.DefaultExpressionEngine"
+ propertyDelimiter="/" indexStart="[" indexEnd="]"/>
+ </xml>
+]]></source>
+ <p>
+ In this example a configuration source for a properties file and one for
+ an XML document are defined. For the properties source the
+ <code>throwExceptionOnMissing</code> property is set to <b>true</b>,
+ which means that it should throw an exception if a requested property is
+ not found. In addition it is assigned a reloading strategy, which is
+ declared and configured in a sub element. The XML configuration source is
+ initialized in a similar way: a simple property is set, and an expression
+ engine is assigned. More information about the format for declaring objects
+ and initializing their properties can be found in the section about
+ <a href="howto_beans.html#Declaring and Creating Beans">bean
+ declarations</a>.
+ </p>
+ <p>
+ In addition to the attributes that correspond to properties of the
+ configuration object to be created, a configuration declaration can have a
+ set of special attributes that are evaluated by
+ <code>DefaultConfigurationBuilder</code> when it creates the objects.
+ These attributes are listed in the following table:
+ </p>
+ <p>
+ <table border="1">
+ <tr>
+ <th>Attribute</th>
+ <th>Meaning</th>
+ </tr>
+ <tr>
+ <td valign="top"><code>config-name</code></td>
+ <td>Allows a name to be specified for this configuration. This name can
+ be used to obtain a reference to the configuration from the resulting
+ combined configuration (see below).</td>
+ </tr>
+ <tr>
+ <td valign="top"><code>config-at</code></td>
+ <td>With this attribute an optional prefix can be specified for the
+ properties of the corresponding configuration.</td>
+ </tr>
+ <tr>
+ <td valign="top"><code>config-optional</code></td>
+ <td>Declares a configuration as optional. This means that errors that
+ occur when creating the configuration are silently ignored. The default
+ behavior when an error occurs is that no configuration is added to
+ the resulting combined configuration. This behavior can be used to find
+ out whether an optional configuration could be successfully created or
+ not. If you specify a name for the optional configuration (using the
+ <code>config-name</code> attribute), you can later check whether the
+ combined configuration contains a configuration with this name. With the
+ <code>config-forceCreate</code> attribute (see below) this default
+ behavior can be changed.</td>
+ </tr>
+ <tr>
+ <td valign="top"><code>config-forceCreate</code></td>
+ <td>This boolean attribute is only evaluated for configurations declared as
+ optional. It determines the behavior of the configuration builder when
+ the optional configuration could not be created. If set to <b>true</b>,
+ the builder tries to create an empty, uninitialized configuration of the
+ correct type and add it to the resulting combined configuration. This is
+ especially useful for file-based configurations. Consider a use case
+ where an application wants to store user specific configuration files in
+ the users' home directories. When a user starts this application for the
+ first time, the user configuration does not exist yet. If it is declared
+ as <em>optional</em> and <em>forceCreate</em>, the missing configuration
+ file won't cause an error, but an empty configuration will be created.
+ The application can then obtain this configuration, add properties to it
+ (e.g. user specific settings) and save it. Without the
+ <code>config-forceCreate</code> attribute the application would have to
+ check whether the user configuration exists in the combined configuration
+ and eventually create it manually. Note that not all configuration
+ providers support this mechanism. Sometimes it may not be possible to
+ create an empty configuration if the standard initialization fails. In
+ this case no configuration will be added to the combined configuration
+ (with other words: the <code>config-forceCreate</code> attribute will not
+ have any effect).</td>
+ </tr>
+ </table>
+ </p>
+ <p>
+ <em>Note:</em> In older versions of Commons Configuration the attributes
+ <code>config-at</code> and <code>config-optional</code> were named
+ <code>at</code> and <code>optional</code> respective. They have been
+ renamed in order to avoid possible name clashes with property names for
+ configuration sources. However, for reasons of backwards compatibility,
+ the old attribute names can still be used.
+ </p>
+ <p>
+ Another useful feature is the built-in support for interpolation (i.e.
+ variable substitution): You can use variables in your configuration
+ definition file that are defined in declared configuration sources. For
+ instance, if the name of a configuration file to be loaded is defined by
+ the system property <code>CONFIG_FILE</code>, you can do something like
+ this:
+ </p>
+ <source><![CDATA[
+<configuration>
+ <!-- Load the system properties -->
+ <system/>
+ <!-- Now load the config file, using a system property as file name -->
+ <properties fileName="${CONFIG_FILE}"/>
+</configuration>
+]]></source>
+ <p>
+ Note that you can refer only to properties that have already been loaded.
+ If you change the order of the <code><system></code> and the
+ <code><properties></code> elements in the example above, an error
+ will occur because the <code>${CONFIG_FILE}</code> variable will then be
+ undefined at the moment it is evaluated.
+ </p>
+ <source><![CDATA[
+<configuration systemProperties="systemProperties.xml">
+ <!-- Load the system properties -->
+ <system/>
+ <!-- Now load the config file, using a system property as file name -->
+ <properties fileName="${CONFIG_FILE}"/>
+</configuration>
+]]></source>
+ <p>
+ This example differs from the previous one by the <code>systemProperties</code>
+ attribute added to the root element. It causes the specified to be read
+ and all properties defined therein to be added to the system properties.
+ So properties like <em>CONFIG_FILE</em> can be defined in a properties
+ file and are then treated as if they were system properties.
+ </p>
+ <p>
+ <strong>The header section</strong>
+ </p>
+ <p>
+ In the header section properties of the resulting combined configuration
+ object can be set. The main part of this section is a bean declaration
+ that is used for creating the resulting configuration object. Other
+ elements can be used for customizing the
+ <a href="howto_combinedconfiguration.html#Node combiners">Node combiners</a>
+ used by the override and the union combined configuration. The following
+ example shows a header section that uses all supported properties:
+ </p>
+ <source><![CDATA[
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.OverrideCombiner"/>
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <combiner>
+ <override>
+ <list-nodes>
+ <node>table</node>
+ <node>list</node>
+ </list-nodes>
+ </override>
+ <additional>
+ <list-nodes>
+ <node>table</node>
+ </list-nodes>
+ </additional>
+ </combiner>
+ </header>
+]]></source>
+ <p>
+ The <code>result</code> element points to the bean declaration for the
+ resulting combined configuration. In this example we set some attributes
+ and initialize the node combiner (which is not necessary because the
+ default override combiner is specified), and the expression engine to be
+ used. Note that the <code>config-class</code> attribute makes it
+ possible to inject custom classes for the resulting configuration or the
+ node combiner.
+ </p>
+ <p>
+ The <code>combiner</code> section allows nodes to be defined as list nodes.
+ This can be necessary for certain node combiner implementations to work
+ correctly. More information can be found in the section about
+ <a href="howto_combinedconfiguration.html#Node combiners">Node combiners</a>.
+ </p>
+ <p>
+ <em>Note:</em> From time to time the question is raised whether there is a
+ document type definition or a schema defining exactly the structure of a
+ configuration definition file. Frankly, the answer is no. This is due to
+ the fact that the format is extensible. As will be shown below, it is
+ possible to register yout own tags in order to embedd custom configuration
+ sources.
+ </p>
+ </subsection>
+
+ <subsection name="An example">
+ <p>
+ After all that theory let's go through a more complex example! We start
+ with the configuration definition file that looks like the following:
+ </p>
+ <source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true">
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <combiner>
+ <additional>
+ <list-nodes>
+ <node>table</node>
+ </list-nodes>
+ </additional>
+ </combiner>
+ </header>
+ <override>
+ <properties fileName="user.properties" throwExceptionOnMissing="true"
+ config-name="properties" config-optional="true">
+ <reloadingStrategy refreshDelay="10000"
+ config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"/>
+ </properties>
+ <xml fileName="settings.xml" config-name="xml"/>
+ </override>
+ <additional>
+ <xml config-name="tab1" fileName="table1.xml" config-at="database.tables"/>
+ <xml config-name="tab2" fileName="table2.xml" config-at="database.tables"
+ validating="true"/>
+ </additional>
+</configuration>
+]]></source>
+ <p>
+ This configuration definition file includes four configuration sources and
+ sets some properties for the resulting <code>CombinedConfiguration</code>.
+ Of special interest is the <code>forceReloadCheck</code> property, which
+ enables a special check for detecting property changes in the contained
+ configuration sources. If this property is not set, reloading won't work.
+ Because we have configured a reloading strategy for one of the included
+ configuration sources we have to set this flag so that this reloading
+ strategy can function properly. More details about this topic can be
+ found in the Javadocs for
+ <code><a href="../apidocs/org/apache/commons/configuration/CombinedConfiguration.html">
+ CombinedConfiguration</a></code>. We also set some properties for the
+ configurations to be loaded; for instance we declare that one of the XML
+ configurations should be validated.
+ </p>
+ <p>
+ With the following code we can create a <code>DefaultConfigurationBuilder</code>
+ and load this file:
+ </p>
+ <source><![CDATA[
+DefaultConfigurationBuilder builder = new DefaultConfigurationBuilder();
+builder.setFile(new File("configuration.xml"));
+CombinedConfiguration cc = builder.getConfiguration(true);
+]]></source>
+ <p>
+ It would have been possible to specify the location of the configuration
+ definition file in multiple other ways, e.g. as a URL. The boolean argument
+ in the call to <code>getConfiguration()</code> determines whether the
+ configuration definition file should be loaded. For our simple example we
+ want this to happen, but it would also be possible to load the file
+ manually (by calling the <code>load()</code> method), and after that
+ updating the configuration. (Remember that <code>DefaultConfigurationBuilder</code>
+ is derived from <code>XMLConfiguration</code>, that means you can use all methods
+ provided by this class to alter its data, e.g. to add further configuration
+ sources.) If the configuration's data was manually changed, you should
+ call <code>getConfiguration()</code> with the argument <b>false</b>.
+ <code>XMLConfiguration</code> also provides the <code>registerEntityId()</code>
+ method that can be used to define the location of DTD files (refer to the
+ section <a href="howto_xml.html#Validation_of_XML_configuration_files">
+ Validation of XML configuration files</a> for more details). This method
+ is available for <code>DefaultConfigurationBuilder</code>, too. The
+ entities registered here will be passed to the loaded child XML
+ configurations. So you can register the DTDs of all child XML configurations
+ globally at the configuration builder.
+ </p>
+ <p>
+ In the <code>header</code> section we have chosen an XPATH expression
+ engine for the resulting configuration. So we can query our properties
+ using the convenient XPATH syntax. By providing the <code>config-name</code>
+ attribute we have given all configuration sources a name. This name can
+ be used to obtain the corresponding sources from the combined
+ configuration. For configurations in the override section this is
+ directly possible:
+ </p>
+ <source><![CDATA[
+Configuration propertiesConfig = cc.getConfiguration("properties");
+Configuration xmlConfig = cc.getConfiguration("xml");
+]]></source>
+ <p>
+ Configurations in the <code>additional</code> section are treated a bit
+ differently: they are all packed together in another combined configuration
+ and then added to the resulting combined configuration. So in our example
+ the combined configuration <code>cc</code> will contain three configurations:
+ the two configurations from the override section, and the combined
+ configuration with the <code>additional</code> configurations. The latter
+ is stored under a name determined by the <code>ADDITIONAL_NAME</code>
+ constant of <code>DefaultConfigurationBuilder</code>. The following
+ code shows how the configurations of the <code>additional</code> section
+ can be accessed:
+ </p>
+ <source><![CDATA[
+CombinedConfiguration ccAdd = (CombinedConfiguration)
+ cc.getConfiguration(DefaultConfigurationBuilder.ADDITIONAL_NAME);
+Configuration tab1Config = ccAdd.getConfiguration("tab1");
+Configuration tab2Config = ccAdd.getConfiguration("tab2");
+]]></source>
+ </subsection>
+
+ <subsection name="Extending the configuration definition file format">
+ <p>
+ If you have written a custom configuration class, you might want to
+ declare instances of this class in a configuration definition file, too.
+ With <code>DefaultConfigurationBuilder</code> this is now possible by
+ registering a <em>ConfigurationProvider</em>.
+ </p>
+ <p>
+ <code>ConfigurationProvider</code> is an inner class defined in
+ <code>DefaultConfigurationBuilder</code>. Its task is to create and
+ initialize a configuration object. Whenever <code>DefaultConfigurationBuilder</code>
+ encounters a tag in the <code>override</code> or the <code>additional</code>
+ section it checks whether for this tag a <code>ConfigurationProvider</code>
+ was registered. If this is the case, the provider is asked to create a
+ new configuration instance; otherwise an exception will be thrown.
+ </p>
+ <p>
+ So for adding support for a new configuration class you have to create an
+ instance of <code>ConfigurationProvider</code> (or a derived class) and
+ register it at the configuration builder using the
+ <code>addConfigurationProvider()</code> method. This method expects the
+ name of the associated tag and the provider instance as arguments.
+ </p>
+ <p>
+ If your custom configuration class does not need any special initialization,
+ you can use the <code>ConfigurationProvider</code> class directly. It is
+ able of creating an instance of a specified class (which must be derived
+ from <code>AbstractConfiguration</code>). Let's take a look at an example
+ where we want to add support for a configuration class called
+ <code>MyConfiguration</code>. The corresponding tag in the configuration
+ definition file should have the name <code>myconfig</code>. The code for
+ registering the new provider and loading the configuration definition file
+ looks as follows:
+ </p>
+ <source><![CDATA[
+DefaultConfigurationBuilder builder = new DefaultConfigurationBuilder();
+DefaultConfigurationBuilder.ConfigurationProvider provider = new
+ DefaultConfigurationBuilder.ConfigurationProvider(MyConfiguration.class);
+builder.addConfigurationProvider("myconfig", provider);
+
+builder.setFileName("configuration.xml");
+Configuration config = builder.getConfiguration();
+]]></source>
+ <p>
+ If your configuration provider is registered this way, your configuration
+ definition file can contain the <code>myconfig</code> tag just as any
+ other tag for declaring a configuration source:
+ </p>
+ <source><![CDATA[
+<configuration>
+ <additional>
+ <xml fileName="settings.xml"/>
+ <myconfig delimiterParsingDisabled="true"/>
+ </additional>
+</configuration>
+]]></source>
+ <p>
+ As is demonstrated in this example, it is possible to specify attributes
+ for initializing properties of your configuration object. In this example
+ we set the default <code>delimiterParsingDisabled</code> property
+ inherited from <code>AbstractConfiguration</code>. Of course you can set
+ custom properties of your configuration class, too.
+ </p>
+ <p>
+ If your custom configuration class is a file-based configuration, you
+ should use the <code>FileConfigurationProvider</code> class instead of
+ <code>ConfigurationProvider</code>. <code>FileConfigurationProvider</code>
+ is another inner class of <code>DefaultConfigurationBuilder</code> that
+ knows how to deal with file-based configurations: it ensures that the
+ correct base path is set and takes care of invoking the <code>load()</code>
+ method.
+ </p>
+ <p>
+ If your custom configuration class requires special initialization, you
+ need to create your own provider class that extends
+ <code>ConfigurationProvider</code>. Here you will have to override the
+ <code>getConfiguration(ConfigurationDeclaration)</code> method, which is
+ responsible for creating the configuration instance (all information
+ necessary for this purpose can be obtained from the passed in declaration
+ object). It is recommended that you call the inherited method first,
+ which will instantiate and initialize the new configuration object. Then
+ you can perform your specific initialization.
+ </p>
+ </subsection>
+ </section>
+</body>
+
+</document>
diff --git a/src/site/xdoc/userguide/howto_events.xml b/src/site/xdoc/userguide/howto_events.xml
new file mode 100644
index 0000000..ab8a951
--- /dev/null
+++ b/src/site/xdoc/userguide/howto_events.xml
@@ -0,0 +1,240 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<document>
+
+ <properties>
+ <title>Configuration Events Howto</title>
+ <author email="oheger at apache.org">Oliver Heger</author>
+ </properties>
+
+<body>
+ <section name="Configuration Events">
+ <p>
+ All configuration classes derived from
+ <code><a href="../apidocs/org/apache/commons/configuration/AbstractConfiguration.html">
+ AbstractConfiguration</a></code> allow to register event listeners, which
+ are notified whenever the configuration's data is changed. This provides
+ an easy means for tracking updates on a configuration.
+ </p>
+
+ <subsection name="Configuration listeners">
+ <p>
+ Objects that are interested in update events triggered by configurations
+ must implement the
+ <code><a href="../apidocs/org/apache/commons/configuration/event/ConfigurationListener.html">
+ ConfigurationListener</a></code> interface. This interface defines a
+ single method <code>configurationChanged()</code>, which is passed a
+ <code><a href="../apidocs/org/apache/commons/configuration/event/ConfigurationEvent.html">
+ ConfigurationEvent</a></code> object. The event object contains all
+ information available about the modification, including:
+ <ul>
+ <li>A source object, which is usually the configuration object that was
+ modified.</li>
+ <li>The event's type. This is a numeric value that corresponds to
+ constant declarations in concrete configuration classes. It describes
+ what exactly has happended.</li>
+ <li>If available, the name of the property whose modification caused the
+ event.</li>
+ <li>If available, the value of the property that caused this event.</li>
+ <li>A flag whether this event was generated before or after the update
+ of the source configuration. A modification of a configuration typically
+ causes two events: one event before and one event after the modification
+ is performed. This allows event listeners to react at the correct point
+ of time.</li>
+ </ul>
+ Depending on the event type not all of this data may be available.
+ </p>
+ <p>
+ For resolving the numeric event type use constants defined in
+ <code>AbstractConfiguration</code> or derived classes. These constants
+ start with the prefix <code>EVENT_</code> and have a speaking name. Here
+ is an incomplete list of available event types with the configuration
+ classes, in which they are defined:
+ </p>
+ <p>
+ <dl>
+ <dt>AbstractConfiguration</dt>
+ <dd>EVENT_ADD_PROPERTY (a property was added; the name of the affected
+ property and the value that was added can be obtained from the
+ event object), EVENT_SET_PROPERTY (a property's value was changed; the
+ event object stores the name of the affected property and its new value),
+ EVENT_CLEAR_PROPERTY (a property was removed from the configuration;
+ its name is stored in the event object), EVENT_CLEAR (the configuration
+ was cleared)</dd>
+ <dt>AbstractFileConfiguration</dt>
+ <dd>EVENT_RELOAD (the configuration was reloaded)</dd>
+ <dt>HierarchicalConfiguration</dt>
+ <dd>EVENT_ADD_NODES (the <code>addNodes()</code> method was called;
+ the event object contains the key, to which the nodes were added, and
+ a collection with the new nodes as value),
+ EVENT_CLEAR_TREE (the <code>clearTree()</code> method was called; the
+ event object stores the key of the removed sub tree),
+ EVENT_SUBNODE_CHANGED (a <code>SubnodeConfiguration</code> that was
+ created from this configuration has been changed. The value property
+ of the event object contains the original event object as it was sent by
+ the subnode configuration. <em>Note: At the moment it is not possible
+ to map the property key as it was received from the subnode configuration
+ into the namespace of the parent configuration.)</em></dd>
+ </dl>
+ </p>
+ </subsection>
+
+ <subsection name="An example">
+ <p>
+ Implementing an event listener is quite easy. As an example we are going
+ to define an event listener, which logs all received configuration events
+ to the console. The class could look as follows:
+ </p>
+ <source><![CDATA[
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+
+public class ConfigurationLogListener implements ConfigurationListener
+{
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ if (!event.isBeforeUpdate())
+ {
+ // only display events after the modification was done
+ System.out.println("Received event!");
+ System.out.println("Type = " + event.getType());
+ if (event.getPropertyName() != null)
+ {
+ System.out.println("Property name = " + event.getPropertyName());
+ }
+ if (event.getPropertyValue() != null)
+ {
+ System.out.println("Property value = " + event.getPropertyValue());
+ }
+ }
+ }
+}
+]]></source>
+ <p>
+ Now an instance of this event listener class has to be registered at a
+ configuration object:
+ </p>
+ <source><![CDATA[
+AbstractConfiguration config = ... // somehow create the configuration
+ConfigurationListener listener = new ConfigurationLogListener();
+config.addConfigurationListener(listener);
+...
+config.addProperty("newProperty", "newValue"); // will fire an event
+]]></source>
+ </subsection>
+ <subsection name="Error listeners">
+ <p>
+ Some implementations of the <code>Configuration</code> interface operate
+ on underlying storages that can throw exceptions on each property access.
+ As an example consider <code>
+ <a href="../apidocs/org/apache/commons/configuration/DatabaseConfiguration.html">
+ DatabaseConfiguration</a></code>: this configuration class issues an SQL
+ statement for each accessed property, which can potentially cause a
+ <code>SQLException</code>.
+ </p>
+ <p>
+ In earlier versions of <em>Commons Configuration</em> such exceptions
+ were simply logged and then swallowed. So for clients it was impossible
+ to find out if something went wrong. From version 1.4 on there is a new
+ way of dealing with those internal errors: the concept of <em>error
+ listeners</em>.
+ </p>
+ <p>
+ A configuration error listener is very similar to a regular configuration
+ event listener. Instead of the <code>ConfigurationListener</code>
+ interface it has to implement the
+ <code><a href="../apidocs/org/apache/commons/configuration/event/ConfigurationErrorListener.html">
+ ConfigurationErrorListener</a></code> interface, which defines a single method
+ <code>configurationError()</code>. In case of an internal error this
+ method is invoked, and a
+ <code><a href="../apidocs/org/apache/commons/configuration/event/ConfigurationErrorEvent.html">
+ ConfigurationErrorEvent</a></code> with information about that error is
+ passed. By inheriting from <code>ConfigurationEvent</code>
+ <code>ConfigurationErrorEvent</code> supports all information that is
+ available for normal configuration listeners, too (e.g. the event type or
+ the property that was accessed when the problem occurred; note that the
+ <code>isBefore()</code> method does not really make sense for error
+ events because an error can only occur after something was done, so it
+ returns always <b>false</b> is this context). This data can
+ be used to find out when and where the error happened. In addition there
+ is the <code>getCause()</code> method that returns the <code>Throwable</code>
+ object, which generated this event (i.e. the causing exception).
+ </p>
+ <p>
+ We can now continue our example from the previous section and make our
+ example configuration listener also capable of tracing error events. To
+ achieve this we let the <code>ConfigurationLogListener</code> class also
+ implement the <code>ConfigurationErrorListener</code> interface:
+ </p>
+ <source>
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+<b>import org.apache.commons.configuration.event.ConfigurationListener;</b>
+
+public class ConfigurationLogListener
+ implements ConfigurationListener, <b>ConfigurationErrorListener</b>
+{
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ // remains unchanged, see above
+ ...
+ }
+
+ <b>public void configurationError(ConfigurationErrorEvent event)
+ {
+ System.out.println("An internal error occurred!");
+ // Log the standard properties of the configuration event
+ configurationChanged(event);
+ // Now log the exception
+ event.getCause().printStackTrace();
+ }</b>
+}
+</source>
+ <p>
+ Now the listener object has to be registered as an error listener, too.
+ For this purpose <code>AbstractConfiguration</code> provides the
+ <code>addErrorListener()</code> method. The following example fragment
+ shows the registration of the log listener object:
+ </p>
+ <source>
+AbstractConfiguration config = ... // somehow create the configuration
+ConfigurationListener listener = new ConfigurationLogListener();
+config.addConfigurationListener(listener);
+<b>config.addErrorListener((ConfigurationErrorListener) listener);</b>
+...
+config.addProperty("newProperty", "newValue"); // will fire an event
+</source>
+ <p>
+ Note: <code>AbstractConfiguration</code> already implements a mechanism
+ for writing internal errors to a logger object: It has the protected
+ <code>addErrorLogListener()</code> method that can be called by derived
+ classes to register a listener that will output all occurring internal
+ errors using the default logger. Configuration implementations like
+ <code>DatabaseConfiguration</code> that are affected by potential internal
+ errors call this method during their initialization. So the default
+ behavior of <em>Commons Configuration</em> for these classes is not
+ changed: they still catch occurring exceptions and log them. However by
+ registering specific error listeners it is now possible for clients to
+ implement their own handling of such errors.
+ </p>
+ </subsection>
+ </section>
+</body>
+
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/userguide/howto_filebased.xml b/src/site/xdoc/userguide/howto_filebased.xml
new file mode 100644
index 0000000..21fa2d2
--- /dev/null
+++ b/src/site/xdoc/userguide/howto_filebased.xml
@@ -0,0 +1,244 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<document>
+
+ <properties>
+ <title>File-based Configurations</title>
+ <author email="oheger at apache.org">Oliver Heger</author>
+ </properties>
+
+<body>
+ <section name="File-based Configurations">
+ <p>
+ Often configuration properties are stored in files on the user's hard
+ disk, e.g. in .properties files or as XML documents. Configuration
+ classes that deal with such properties need to provide typical operations
+ like loading or saving files. The files to be processed can be specified
+ in several different flavors like <code>java.io.File</code> objects,
+ relative or absolute path names, or URLs.
+ </p>
+ <p>
+ To provide a consistent way of dealing with configuration files in
+ Commons Configuration the <code><a href="../apidocs/org/apache/commons/configuration/FileConfiguration.html">FileConfiguration</a></code>
+ interface exists. <code>FileConfiguration</code> defines a standard
+ API for accessing files and is implemented by many configuration
+ implementations, including <code>PropertiesConfiguration</code> and
+ <code>XMLConfiguration</code>.
+ </p>
+ <p>
+ In the following sections we take a closer look at the methods of the
+ <code>FileConfiguration</code> interface and how they are used.
+ </p>
+
+ <subsection name="Specifying the file">
+ <p>
+ The <code>FileConfiguration</code> interface contains several
+ methods for specifying the file to be loaded. The following variants
+ are supported:
+ <ul>
+ <li>With the <code>setFile()</code> method the data file can be
+ specified as a <code>java.io.File</code> object.</li>
+ <li>The <code>setURL()</code> takes a <code>java.net.URL</code>
+ as argument; the file will be loaded from this URL.</li>
+ <li>The methods <code>setFileName()</code> and <code>setBasePath()</code>
+ allows to specify the path of the data file. The base path is
+ important if relative paths are to be resolved based on this file.</li>
+ </ul>
+ </p>
+ <p>
+ While a <code>File</code> or a URL uniquely identify a file, the
+ situation is a bit ambigous when only a base path and a file name are
+ set. These can be arbitrary strings (even full URLs) whose exact
+ meaning must be detected when the file is loaded. For this purpose
+ file-based configurations perform the following checks (in this
+ order):
+ <ul>
+ <li>If the combination from base path and file name is a full URL
+ that points to an existing file, this URL will be used to load
+ the file.</li>
+ <li>If the combination from base path and file name is an absolute
+ file name and this file exists, it will be loaded.</li>
+ <li>If the combination from base path and file name is a relative
+ file path that points to an existing file, this file will be loaded.</li>
+ <li>If a file with the specified name exists in the user's home
+ directory, this file will be loaded.</li>
+ <li>Otherwise the file name is interpreted as a resource name, and
+ it is checked whether the data file can be loaded from the classpath.</li>
+ </ul>
+ If all these checks fail, a <code>ConfigurationException</code> will
+ be thrown.
+ </p>
+ </subsection>
+
+ <subsection name="Loading">
+ <p>
+ After the file name has been defined using one of the methods mentioned
+ above, the <code>load()</code> method can be called. This method tries
+ to locate the file and open it. If this fails, a <code>ConfigurationException</code>
+ is thrown.
+ </p>
+ <p>
+ The <code>FileConfiguration</code> interface defines multiple overloaded
+ <code>load()</code> methods. The one that takes no argument will
+ always operate on the file name that has been set earlier. All
+ other methods allow to specify the source to be loaded. This can be
+ done as <code>java.io.File</code>, <code>java.net.URL</code>, string
+ (containing either an absolute or relative path), input stream, or
+ reader. When using these variants of the <code>load()</code> method
+ be aware of two things:
+ <ol>
+ <li>They do not change the configuration's file name. To do this
+ you have to explicitely call one of the setter methods.</li>
+ <li>The <code>load()</code> methods do not empty the
+ configuration before new data is loaded. This makes it easy to
+ construct union configurations by simply calling <code>load()</code>
+ multiple times. But if you want to reuse a <code>Configuration</code>
+ object and load a different file, remember to call the
+ <code>clear()</code> method first to ensure that old properties are
+ wiped out.</li>
+ </ol>
+ </p>
+ <p>
+ File-based configurations typically define a set of constructors that
+ correspond to the various setter methods for defining the data file.
+ These constructors will set the file and then invoke the <code>load()</code>
+ method. So creating a file-based configuration object and loading its
+ content can be done in a single step.
+ </p>
+ </subsection>
+
+ <subsection name="Saving">
+ <p>
+ Saving is implemented analogously to loading: There is a no argument
+ <code>save()</code> method that will use the internal file name. Then
+ for each <code>load()</code> method a corresponding <code>save()</code>
+ method exists that will write the data contained in the configuration
+ to different targets.
+ </p>
+ <p>
+ An example for loading, manipulating, and saving a configuration
+ (based on a <a href="howto_properties.html"><code>PropertiesConfiguration</code></a>)
+ could look as follows:
+ </p>
+<source>
+PropertiesConfiguration config = new PropertiesConfiguration("usergui.properties");
+config.setProperty("colors.background", "#000000");
+config.save();
+</source>
+ <p>
+ You can also save a copy of the configuration to another file:
+ </p>
+<source>
+PropertiesConfiguration config = new PropertiesConfiguration("usergui.properties");
+config.setProperty("colors.background", "#000000");
+config.save("usergui.backup.properties);
+</source>
+ </subsection>
+
+ <subsection name="Automatic Saving">
+ <p>
+ If you want to ensure that every modification of a configuration
+ object is immideately written to disk, you can enable the automatic
+ saving mode. This is done through the <code>setAutoSave()</code>
+ method as shown in the following example:
+ </p>
+<source>
+PropertiesConfiguration config = new PropertiesConfiguration("usergui.properties");
+config.setAutoSave(true);
+config.setProperty("colors.background", "#000000"); // the configuration is saved after this call
+</source>
+ <p>
+ Be careful with this mode when you have many updates on your
+ configuration. This will lead to many I/O operations, too.
+ </p>
+ </subsection>
+
+ <subsection name="Automatic Reloading">
+ <p>
+ A common issue with file-based configurations is to handle the
+ reloading of the data file when it changes. This is especially important
+ if you have long running applications and do not want to restart them
+ when a configuration file was updated. Commons Configuration has the
+ concept of so called <em>reloading strategies</em> that can be
+ associated with a file-based configuration. Such a strategy monitors
+ a configuration file and is able to detect changes. A default reloading
+ strategy is <code><a href="../apidocs/org/apache/commons/configuration/reloading/FileChangedReloadingStrategy.html">FileChangedReloadingStrategy</a></code>.
+ It can be set on a file-based configuration as follows:
+ </p>
+<source>
+PropertiesConfiguration config = new PropertiesConfiguration("usergui.properties");
+config.setReloadingStrategy(new FileChangedReloadingStrategy());
+</source>
+ <p>
+ <code>FileChangedReloadingStrategy</code> works as follows: On every
+ property access the configuration checks its associated reloading
+ strategy. <code>FileChangedReloadingStrategy</code> will then obtain
+ the last modification date of the configuration file and check whether
+ it has changed since the last access. If this is the case, a reload is
+ triggered. To avoid often disk access when multiple properties are
+ queried from the configuration, a <em>refresh delay</em> can be set on
+ the reloading strategy. This is a time in milli seconds with the meaning
+ that the reloading strategy will only once check the file's last
+ modification time in the period specified here.
+ </p>
+ </subsection>
+
+ <subsection name="Managed Reloading">
+ <p>
+ <code>ManagedReloadingStrategy</code> is an alternative to automatic
+ reloading. It allows to hot-reload properties on a running application
+ but only when requested by admin. The <code>refresh()</code> method
+ will force a reload of the configuration source.
+ </p>
+ <p>
+ A typical use of this feature is to setup ManagedReloadingStrategy as
+ a JMX MBean. The following code sample uses Springframework
+ MBeanExporter to expose the ManagedReloadingStrategy to the JMX
+ console :
+<source>
+<![CDATA[
+<!-- A file based configuration bean -->
+<bean id="configuration" class="(...).PropertiesConfiguration">
+ <constructor-arg type="java.net.URL" value="file:${user.home}/custom.properties"/>
+ <property name="reloadingStrategy" ref="reloadingStrategy"/>
+</bean>
+
+<!-- The managed reloading strategy for the configuration bean -->
+<bean id="reloadingStrategy" class="...ManagedReloadingStrategy"/>
+
+<!-- The MBeanExporter that exposes reloadingStrategy to the JMX console -->
+<bean id="mbeanMetadataExporter" class="org.springframework.jmx.export.MBeanExporter">
+ <property name="server" ref="mbeanServer"/>
+ <property name="beans">
+ <map>
+ <entry key="myApp:bean=configuration" value-ref="reloadingStrategy"/>
+ </map>
+ </property>
+</bean>
+]]>
+</source>
+ With this configuration, the JMX console will expose the
+ "myApp:bean=configuration" MBean and it's refresh operation.
+ </p>
+ </subsection>
+ </section>
+
+</body>
+
+</document>
diff --git a/src/site/xdoc/userguide/howto_filesystems.xml b/src/site/xdoc/userguide/howto_filesystems.xml
new file mode 100644
index 0000000..aba2651
--- /dev/null
+++ b/src/site/xdoc/userguide/howto_filesystems.xml
@@ -0,0 +1,131 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<document>
+
+ <properties>
+ <title>File Systems</title>
+ <author email="rgoers at apache.org">Ralph Goers</author>
+ </properties>
+
+<body>
+ <section name="File Systems">
+ <p>
+ In its default mode of operation Commons Configuration supports retrieving and storing
+ configuration files either on a local file system or via http. However, Commons
+ Configuration provides support for allowing other File System adapters. All file
+ access is accomplished through the <code>FileSystem</code> interface so accessing files
+ using other mechanisms is possible.
+ </p>
+ <p>
+ Commons Configuration also provides a second FileSystem which allows retrieval using
+ <a href="http://commons.apache.org/vfs">Apache Commons VFS</a>. As of this writing
+ Commons VFS supports 18 protocols for manipulating files.
+ </p>
+ <subsection name="Configuration">
+ <p>
+ The FileSystem used by Commons Configuration can be set in one of several ways:
+ <ol>
+ <li>A system property named "org.apache.commons.configuration.filesystem" can be defined
+ with the full class name of the desired <code>FileSystem</code> implementation to set the
+ default <code>FileSystem</code>.</li>
+ <li><code>FileSystem.setDefaultFilesystem()</code> can be called to directly set the
+ default <code>FileSystem</code> implementation.</li>
+ <li><code>DefaultConfigurationBuilder.setFileSystem()</code> can be called to set the
+ FileSystem implementation. <code>DefaultConfiguratonBuilder</code> will use this for each
+ configuration it creates</li>
+ <li><code>DefaultConfigurationBuilder</code> can be configured with the <code>FileSystem</code>
+ to be used when creating each of the configurations.</li>
+ <li>Each Configuration referenced in <code>DefaultConfigurationBuilder's</code>
+ configuration can be configured with the <code>FileSystem</code> to use for that
+ configuration.</li>
+ <li>Call setFileSystem() directly on any Configuration that implements <code>FileSystemBased.</code>
+ Both <code>AbstractFileConfiguration</code> and <code>AbstractHierarchicalFileConfiguration</code>
+ implement <code>FileSystemBased</code></li>
+ </ol>
+ </p>
+ <p>
+ The example that follows shows how to add <code>FileSystem</code> configuration to
+ <code>DefaultConfigurationBuilder</code>.
+ </p>
+ <source><![CDATA[
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true">
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <fileSystem config-class="org.apache.commons.configuration.VFSFileSystem"/>
+ </header>
+ <override>
+ <xml fileName="settings.xml" config-name="xml">
+ <fileSystem config-class="org.apache.commons.configuration.DefaultFileSystem"/>
+ </xml>
+ </override>
+</configuration>
+]]></source>
+ </subsection>
+ <subsection name="File Options Provider">
+ <p>
+ Commons VFS allows options to the underlying file systems being used. Commons Configuration
+ allows applications to provide these by implementing the <code>FileOptionsProvider</code> interface
+ and registering the provider with the <code>FileSystem</code>. <code>FileOptionsProvider</code>
+ has a single method that must be implemented, <code>getOptions</code>, which returns a Map
+ containing the keys and values that the <code>FileSystem</code> might use. The getOptions
+ method is called as each configuration uses VFS to create a <code>FileOjbect</code> to
+ access the file. The map returned does not have to contain the same keys and/or values
+ each time it is called. For example, the value of the <code>currentUser</code> key can be
+ set to the id of the currently logged in user to allow a WebDAV save to record the userid
+ as a file attribute.
+ </p>
+ </subsection>
+ <subsection name="File Reloading Strategy">
+ <p>
+ The <code><a href="../apidocs/org/apache/commons/configuration/reloading/VFSFileChangedReloadingStrategy.html">VFSFileChangedReloadingStrategy</a></code>
+ can be used to cause Configurations accessed via the <code>VFSFileSystem</code> to be
+ monitored and reloaded when the files are modified. The example below shows how
+ <code>DefaultConfigurationBuilder</code> can be configured to use
+ <code>VFSFilChangedReloadingStrategy</code>.
+ In the example below both test.properties and settings.xml would be checked for changes
+ once per minute.
+ </p>
+ <source><![CDATA[
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true">
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <fileSystem config-class="org.apache.commons.configuration.VFSFileSystem"/>
+ </header>
+ <override>
+ <properties fileName="test.properties" throwExceptionOnMissing="true">
+ <reloadingStrategy refreshDelay="60000"
+ config-class="org.apache.commons.configuration.reloading.VFSFileChangedReloadingStrategy"/>
+ </properties>
+ <xml fileName="settings.xml" config-name="xml">
+ <reloadingStrategy refreshDelay="60000"
+ config-class="org.apache.commons.configuration.reloading.VFSFileChangedReloadingStrategy"/>
+ </xml>
+ </override>
+</configuration>
+]]></source>
+ </subsection>
+ </section>
+
+</body>
+
+</document>
diff --git a/src/site/xdoc/userguide/howto_multitenant.xml b/src/site/xdoc/userguide/howto_multitenant.xml
new file mode 100644
index 0000000..857daba
--- /dev/null
+++ b/src/site/xdoc/userguide/howto_multitenant.xml
@@ -0,0 +1,171 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<document>
+
+ <properties>
+ <title>Mutli-tenant Configurations</title>
+ <author email="rgoers at apache.org">Ralph Goers</author>
+ </properties>
+
+ <body>
+ <section name="Multi-tenant Configurations">
+ <p>
+ In a multi-tenant environment a single instance of the application
+ while run on behalf of many clients. Typically, this will require
+ that each client have its own unique configuration. The simplest
+ approach to enable an application to be multi-tenant is for it
+ to not really be aware of it at all. This requires that the
+ configuration framework take on some of the responsility for
+ making the application work correctly.
+ </p>
+ <p>
+ One approach to enable this support in a web application might be
+ to use a Servlet Filter and then use the Log4j or SLF4J MDC to
+ save the attributes needed to identify the client. These attributes
+ can then be used to identify the specific client configuration to
+ be used. The classes described below use this technique to allow
+ configurations to transparently provide the configuration appropriate
+ for the clients.
+ </p>
+
+ <subsection name="MultiFileHierarchicalConfiguration">
+ <p>
+ The constructor for this class accepts a pattern. The pattern can
+ contain keys that will be resolved using the ConfigurationInterpolator
+ on each call to a method in the class. The configuration file will then
+ be located using the resolved pattern and a new XMLConfiguration
+ will be created and cached for subsequent requests. The ExpressionEngine,
+ ReloadingStrategy and listeners will be propogated to each of the
+ created configurations.
+ </p>
+ <p>
+ When used in a combined configuration it is often acceptable for a file
+ matching a particular pattern to be missing so, by default, most exceptions
+ encountered when loading files are ignored. To change this behavior
+ call setIgnoreException(false) or configure the attribute to false in
+ DefaultConfigurationBuilder's configuration file. If schema validation
+ is enabled validation exceptions will always cause a failure.
+ </p>
+ </subsection>
+ <subsection name="DynamicCombinedConfiguration">
+ <p>
+ The CombinedConfiguration class allows multiple configuration files to be
+ merged together. However, it will not merge a MultiFileHierarchicalConfiguration
+ properly since the underlying configuration file will be different depending
+ on the resolution of the location pattern. DynamicCombinedConfiguration
+ solves this by creating a new CombinedConfiguration for each pattern.
+ </p>
+ </subsection>
+ <subsection name="Sample Configuration">
+ <p>
+ This sample configuration illustrates how to use DynamicCombinedConfiguration
+ in combination with MultiFileHierarchicalConfiguration to create a multi-tenant
+ configuration.
+ </p>
+ <source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true"
+ config-class="org.apache.commons.configuration.DynamicCombinedConfiguration"
+ keyPattern="$${sys:Id}">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <providers>
+ <provider config-tag="multifile"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$FileConfigurationProvider"
+ configurationClass="org.apache.commons.configuration.MultiFileHierarchicalConfiguration"/>
+ </providers>
+ </header>
+ <override>
+ <multifile filePattern="/opt/configs/$$${sys:Id}/config.xml" config-name="clientConfig"/>
+ <xml fileName="/opt/configs/default/config.xml" config-name="defaultConfig"/>
+ </override>
+</configuration>
+]]></source>
+ <p>
+ Note how the variables have multiple '$'. This is how variables are escaped and
+ is necessary because the variables will be interpolated multiple times. Each
+ attempt will remove the leading '$'. When there is only a single '$' in front
+ of the '{' the interpolator will then resolve the variable. The first extra '$'
+ is necessary because DefaultConfigurationBuilder will interpolate any variables
+ in the configuration. In the case of the multifile configuration item two
+ leading '$' characters are necessary before the variable because it will be
+ interpolated by both DefaultConfigurationBuilder and DynamicCombinedConfiguration
+ before MultiFileHierarchicalConfiguration gets the chance to evaluate it.
+ Although in this example one would not expect system properties to change
+ at runtime, other types of lookups such as the MDCStrLookup provided with
+ SLF4J require that the variables be evaluated as the configuration is being
+ accessed instead of when the configuration file is processed to behave as desired.
+ </p>
+ </subsection>
+ <subsection name="PatternSubtreeConfigurationWrapper">
+ <p>
+ Applications are often composed of many components each of which need their
+ own configuration. This can be accomodated by having a configuration file
+ per component, but this can make things hard to manage when there are many
+ clients and many components. A second approach is to combine them into
+ a single configuration file. However, this either means the subcomponent
+ has to be aware of the surrounding configuration and navigate past it or the
+ application must be provided just the portion of the configuration it
+ can process. PatternSubtreeConfigurationWrapper can be used for this purpose.
+ </p>
+ <p>
+ Normal practice when using dependency injection frameworks is to have the
+ attributes needed to make components work correctly injected into them.
+ When working with Commons Configuration this works very well. Components
+ simply need to have a HierarchicalConfiguration attribute along with
+ a corresponding setter and getter. The injection framework can then be
+ used to provide the component with the correct configuration using
+ PatternSubtreeConfigurationWrapper as shown in the next example.
+ </p>
+ <p>
+ <source><![CDATA[
+ <bean id="configurationBuilder"
+ class="org.apache.commons.configuration.DefaultConfigurationBuilder">
+ <property name="fileName">
+ <value>configuration.xml</value>
+ </property>
+ </bean>
+ <bean id="applicationConfig" factory-bean="configurationBuilder"
+ factory-method="getConfiguration">
+ </bean>
+ <bean id="subcomponentConfig"
+ class="org.apache.commons.configuration.PatternSubtreeConfigurationWrapper"
+ autowire='autodetect'>
+ <constructor-arg index="0">
+ <ref bean="applicationConfig"/>
+ </constructor-arg>
+ <constructor-arg index="1" value="/Components/MyComponent"
+ </bean>
+ <bean id='MyComponent' class='org.test.MyComponent' autowire='autodetect'>
+ <property name="configuration">
+ <ref bean="subcomponentConfig"/>
+ </property>
+ </bean>
+]]></source>
+ </p>
+ </subsection>
+ </section>
+
+ </body>
+
+</document>
diff --git a/src/site/xdoc/userguide/howto_properties.xml b/src/site/xdoc/userguide/howto_properties.xml
new file mode 100644
index 0000000..4d1a245
--- /dev/null
+++ b/src/site/xdoc/userguide/howto_properties.xml
@@ -0,0 +1,445 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<document>
+ <properties>
+ <title>Properties files</title>
+ <author email="smanux at lfjr.net">Emmanuel Bourg</author>
+ <author email="oheger at apache.org">Oliver Heger</author>
+ </properties>
+
+ <body>
+
+ <section name="Properties files">
+ <p>
+ Properties files are a popular mean of configuring applications. Of course Commons Configuration
+ supports this format and enhances significantly the basic <code>java.util.Properties</code> class.
+ This section introduces the features of the
+ <code><a href="../apidocs/org/apache/commons/configuration/PropertiesConfiguration.html">PropertiesConfiguration</a></code> class.
+ Note that <code>PropertiesConfiguration</code> is a very typical example
+ for an implementation of the <code>Configuration</code> interface and
+ many of the features described in this section (e.g. list handling or
+ interpolation) are supported by other configuration classes as well.
+ This is because most configuration implementations that ship with
+ Commons Configuration are derived from the common base class
+ <code><a href="../apidocs/org/apache/commons/configuration/AbstractConfiguration.html">AbstractConfiguration</a></code>,
+ which implements these features.
+ </p>
+
+ <subsection name="Using PropertiesConfiguration">
+ <p>
+ Let's start with a simple properties file named
+ <code>usergui.properties</code> with the following content:
+ </p>
+ <source><![CDATA[
+# Properties definining the GUI
+colors.background = #FFFFFF
+colors.foreground = #000080
+
+window.width = 500
+window.height = 300
+]]></source>
+
+ <p>
+ To load this file, you'll write:
+ </p>
+<source>
+Configuration config = new PropertiesConfiguration("usergui.properties");
+</source>
+ <p>
+ If you do not specify an absolute path, the file will be searched automatically
+ in the following locations:
+ <ul>
+ <li>in the current directory</li>
+ <li>in the user home directory</li>
+ <li>in the classpath</li>
+ </ul>
+ </p>
+ <p>
+ Instead of using a constructor that takes a file name you can also
+ invoke one of the <code>load()</code> methods. There are several
+ overloaded variants that allow you to load properties from various
+ sources. More information about loading properties files (and file-based
+ configurations in general) can be found in the section about
+ <a href="howto_filebased.html">File-based Configurations</a>.
+ </p>
+ <p>
+ After the properties file was loaded you can access its content through
+ the methods of the <code>Configuration</code> interface, e.g.
+ </p>
+<source>
+String backColor = config.getString("colors.background");
+Dimension size = new Dimension(config.getInt("window.width"),
+ config.getInt("window.height"));
+</source>
+ </subsection>
+
+ <subsection name="Includes">
+ <p>
+ If a property is named "<code>include</code>" and the value of that property is the
+ name of a file on the disk, that file will be included into the configuration. Here is
+ an example:
+ </p>
+<source>
+# usergui.properties
+
+include = colors.properties
+include = sizes.properties
+</source>
+
+<source>
+# colors.properties
+
+colors.background = #FFFFFF
+</source>
+
+ </subsection>
+
+ <subsection name="Lists and arrays">
+ <p>
+ As was already pointed out in the section
+ <a href="howto_basicfeatures.html#List_handling">List handling</a>
+ of <em>Basic features</em>, Commons Configuration has the ability to
+ return easily a list of values. For example a properties file can
+ contain a list of comma separated values:
+ </p>
+<source>
+# chart colors
+colors.pie = #FF0000, #00FF00, #0000FF
+</source>
+ <p>
+ You don't have to split the value manually, you can retrieve an array
+ or a <code>java.util.List</code> directly with:
+ </p>
+<source>
+String[] colors = config.getStringArray("colors.pie");
+List<Object> colorList = config.getList("colors.pie");
+</source>
+ <p>
+ Alternatively, you can specify a list of values in your properties file by using
+ the same key on several lines:
+ </p>
+<source>
+# chart colors
+colors.pie = #FF0000;
+colors.pie = #00FF00;
+colors.pie = #0000FF;
+</source>
+ <p>
+ All of the features related to list handling described for
+ <code>AbstractConfiguration</code> also apply to properties files,
+ including changing the list delimiter or disabling list handling at
+ all.
+ </p>
+ </subsection>
+
+ <subsection name="Saving">
+ <p>
+ To save your configuration, just call the <code>save()</code> method:
+ </p>
+<source>
+PropertiesConfiguration config = new PropertiesConfiguration("usergui.properties");
+config.setProperty("colors.background", "#000000);
+config.save();
+</source>
+ <p>
+ You can also save a copy of the configuration to another file:
+ </p>
+<source>
+PropertiesConfiguration config = new PropertiesConfiguration("usergui.properties");
+config.setProperty("colors.background", "#000000);
+config.save("usergui.backup.properties);
+</source>
+ <p>
+ More information about saving properties files (and file-based
+ configurations in general) can be found in the section about
+ <a href="howto_filebased.html">File-based Configurations</a>.
+ </p>
+ </subsection>
+
+ <subsection name="Special Characters and Escaping">
+ <p>
+ If you need a special character in a property like a line feed, a tabulation or
+ an unicode character, you can specify it with the same escaped notation used for
+ Java Strings. The list separator ("," by default), can also be escaped:
+ </p>
+ <source><![CDATA[
+key = This \n string \t contains \, escaped \\ characters \u0020
+]]></source>
+ <p>
+ When dealing with lists of elements that contain backslash characters
+ (e.g. file paths on Windows systems) escaping rules can become pretty
+ complex. The first thing to keep in mind is that in order to get a
+ single backslash, you have to write two:
+ </p>
+ <source><![CDATA[
+config.dir = C:\\Temp\\
+ ]]></source>
+ <p>
+ This issue is not specific to Commons Configuration, but is related to
+ the standard format for properties files. Refer to the Javadocs of the
+ <code>load()</code> method of <code>java.util.Properties</code> for more
+ information. Now if you want to define a list with file paths, you may
+ be tempted to write the following:
+ </p>
+ <source><![CDATA[
+# Wrong way to define a list of directories
+config.dirs = C:\\Temp\\,D:\\data\\
+ ]]></source>
+ <p>
+ As the comment indicates, this will not work. The trailing backslash of
+ the first directory is interpreted as escape character for the list
+ delimiter. So instead of a list with two elements only a single value
+ of the property is defined - clearly not what was desired. To get a
+ correct list the trailing backslash has to be escaped. This is achieved
+ by duplicating it (yes, in a properties file that means that we now need
+ 4 backslashes):
+ </p>
+ <source><![CDATA[
+# Correct way to define a list of directories
+config.dirs = C:\\Temp\\\\,D:\\data\\
+ ]]></source>
+ <p>
+ So a sequence of 4 backslashes in the value of a property is interpreted
+ as an escaped backslash and eventually results in a single backslash.
+ This creates another problem when a properties file should refer to the
+ names of network shares. Typically these names start with two
+ backslashes, so the obvious way to define such a property is as follows:
+ </p>
+ <source><![CDATA[
+# Wrong way to define a list of network shares
+config.dirs = \\\\share1,\\\\share2
+ ]]></source>
+ <p>
+ Unfortunately, this will not work because the shares contain the reserved
+ sequence of 4 backslashes. So when reading the value of the
+ <em>config.dirs</em> property a list with two elements is returned
+ starting only with a single backslash. To fix the problem the sequence
+ for escaping a backslash has to be duplicated - we are now at 8
+ backslashes:
+ </p>
+ <source><![CDATA[
+# Correct way to define a list of network shares
+config.dirs = \\\\\\\\share1,\\\\\\\\share2
+ ]]></source>
+ <p>
+ As becomes obvious, escape sequences can become pretty complex and
+ unreadable. In such situations it is recommended to use the alternative
+ way of defining a list: just use the same key multiple times. In this
+ case no additional escaping of backslashes (beyond the usual duplicating
+ required by properties files) is needed because there is no list
+ delimter character involved. Using this syntax the list of network
+ shares looks like the following:
+ </p>
+ <source><![CDATA[
+# Straightforward way to define a list of network shares
+config.dirs = \\\\share1
+config.dirs = \\\\share2
+ ]]></source>
+ </subsection>
+
+ <subsection name="Layout Objects">
+ <p>
+ Each <code>PropertiesConfiguration</code> object is associated with a
+ <em>Layout object</em>, an instance of the class
+ <code><a href="../apidocs/org/apache/commons/configuration/PropertiesConfigurationLayout.html">
+ PropertiesConfigurationLayout</a></code>. This layout object is
+ responsible for preserving most of the structure of loaded configuration
+ files. This means that things like comments or blank lines in a saved
+ properties file will closely resemble the original properties file
+ (the algorithm is not 100 percent perfect, but for most use cases it
+ should be sufficient).
+ </p>
+ <p>
+ Normally a developer does not have to deal with these layout objects.
+ However, there are some methods that might be of interest if enhanced
+ control over the output of properties files is needed. The following
+ list describes these methods (note that corresponding get methods are
+ of course also provided):
+ <ul>
+ <li><code>setComment()</code><br/>
+ With this method a comment can be set for a specified property. When
+ storing the configuration the comment is output before the property,
+ followed by a line break. The comment can span multiple lines; in this
+ case the newline character "\n" must be used as line
+ separator.</li>
+ <li><code>setHeaderComment()</code><br/>
+ With <code>setHeaderComment()</code> a global comment can be set for the
+ properties file. This comment is written at the very start of the file,
+ followed by an empty line.</li>
+ <li><code>setBlancLinesBefore()</code><br/>
+ This methods allows defining the number of empty lines to be written
+ before the specified property. It can be used, for instance, to
+ devide the properties file into multiple logical sections.</li>
+ <li><code>setSingleLine()</code><br/>
+ If a property has multiple values, with <code>setSingleLine()</code> it
+ can be specified that all these values should be written into a single
+ line separated by the default list separator. It is also possible to
+ write multiple definitions for this property (i.e. multiple lines of the
+ form <code>property = value1</code>, <code>property = value2</code> etc.).
+ This is supported by <code>PropertiesConfiguration</code>, but will
+ probably not work when processing the properties file with other tools.
+ </li>
+ <li><code>setForceSingleLine()</code><br/>
+ This is similar to <code>setSingleLine()</code>, but sets a global
+ single line flag. If set to <b>true</b>, all properties with multiple
+ values are always written on a single line.</li>
+ <li><code>setGlobalSeparator()</code><br/>
+ Sometimes it may be necessary to define the properties separator, i.e.
+ the string that separates the property key from the value. This can be
+ done using <code>setGlobalSeparator()</code>. Here an arbitrary string
+ can be specified that will be used as separator. (Note: In order to
+ produce valid properties files only the characters <code>=</code> and
+ <code>:</code> should be used as separators (with or without leading or
+ trailing whitespace), but the method does not enforce this.</li>
+ <li><code>setSeparator()</code><br/>
+ This method is similar to <code>setGlobalSeparator()</code>, but
+ allows setting the property separator for a specific property.</li>
+ <li><code>setLineSeparator()</code><br/>
+ Using this method the line separator can be specified. Per default the
+ platform-specific line separator is used (e.g. <code>\n</code> on unix).
+ </li>
+ </ul>
+ The default settings of <code>PropertiesConfigurationLayout</code> are
+ chosen in a way that most of the original layout of a properties file
+ is retained. With the methods listed above specific layout restrictions
+ can be enforced.
+ </p>
+ </subsection>
+
+ <subsection name="Custom properties readers and writers">
+ <p>
+ There are situations when more control over the process of reading and
+ writing properties file is needed. For instance, an application might
+ have to deal with some legacy properties file in a specific format,
+ which is not supported out of the box by
+ <code>PropertiesConfiguration</code>, but must not be modified. In these
+ cases it is possible to inject a custom reader and writer for
+ properties files.
+ </p>
+ <p>
+ Per default properties files are read and written by the nested classes
+ <code>PropertiesReader</code> and <code>PropertiesWriter</code>
+ (defined within <code>PropertiesConfiguration</code>). These classes are
+ regular reader and writer classes (both are derived from typical base
+ classes of the <code>java.io</code> package) that provide some
+ additional methods making dealing with properties files more
+ convenient. Custom implementations of properties readers and writers
+ must extend these base classes.
+ </p>
+ <p>
+ For installing a custom properties reader or writer
+ <code>PropertiesConfiguration</code> provides the <code>IOFactory</code>
+ interface (which is also defined as a nested class). An object
+ implementing this interface is stored in each
+ <code>PropertiesConfiguration</code> instance. Whenever a properties
+ file is to be read or written (i.e. when one of the <code>load()</code>
+ or <code>save()</code> methods is called), the <code>IOFactory</code>
+ object is asked for creating the properties reader or writer to be
+ used.
+ </p>
+ <p>
+ The <code>IOFactory</code> interface is pretty simple; it defines one
+ method for creating a properties reader and another one for creating a
+ properties writer. A default implementation called
+ <code>DefaultIOFactory</code> is also available and is used by
+ <code>PropertiesConfiguration</code> when no specific
+ <code>IOFactory</code> is set. To make this discussion more concrete
+ we provide an example of how to inject a custom properties reader. The
+ use case is that we have to load a properties file that contains keys
+ with whitespace, which is not supported by
+ <code>PropertiesConfiguration</code> per default. A fragment from such
+ a properties file could look as follows:
+ </p>
+ <source><![CDATA[
+Background Color = #800080
+Foreground Color = #000080
+]]></source>
+ <p>
+ The first step is to create a custom properties reader implementation
+ that can deal with such properties. The class is derived from
+ <code>PropertiesConfiguration.PropertiesReader</code> and overrides the
+ <code>parseProperty()</code> method:
+ </p>
+ <source><![CDATA[
+public class WhitespacePropertiesReader extends PropertiesConfiguration.PropertiesReader
+{
+ public WhitespacePropertiesReader(Reader in, char delimiter)
+ {
+ super(in, delimiter);
+ }
+
+ /**
+ * Special algorithm for parsing properties keys with whitespace. This
+ * method is called for each non-comment line read from the properties
+ * file.
+ */
+ protected void parseProperty(String line)
+ {
+ // simply split the line at the first '=' character
+ // (this should be more robust in production code)
+ int pos = line.indexOf('=');
+ String key = line.substring(0, pos).trim();
+ String value = line.substring(pos + 1).trim();
+
+ // now store the key and the value of the property
+ initPropertyName(key);
+ initPropertyValue(value);
+ }
+}
+]]></source>
+ <p>
+ Notice the calls to the methods <code>initPropertyName()</code> and
+ <code>initPropertyValue()</code>. Here the results of the parsing
+ operation are stored. The next step is to provide a specialized
+ implementation of the <code>IOFactory</code> interface that returns
+ the new properties reader class. As we only want to replace the
+ properties reader (and use the standard writer), we can derive our
+ implementation from <code>DefaultIOFactory</code> and thus only have
+ to override the <code>createPropertiesReader()</code> method.
+ </p>
+ <source><![CDATA[
+public class WhitespaceIOFactory extends PropertiesConfiguration.DefaultIOFactory
+{
+ /**
+ * Return our special properties reader.
+ */
+ public PropertiesReader createPropertiesReader(Reader in, char delimiter)
+ {
+ return new WhitespacePropertiesReader(in, delimiter);
+ }
+}
+]]></source>
+ <p>
+ Finally an instance of our new <code>IOFactory</code> implementation
+ has to be created and passed to the <code>PropertiesConfiguration</code>
+ object. This must be done before the <code>load()</code> method is
+ called. So we cannot use one of the constructors that load the data.
+ The code for setting up the configuration object could look as follows:
+ </p>
+ <source><![CDATA[
+PropertiesConfiguration config = new PropertiesConfiguration();
+config.setIOFactory(new WhitespaceIOFactory());
+config.setFile(...); // set the file to be loaded
+config.load();
+]]></source>
+ </subsection>
+ </section>
+
+ </body>
+</document>
diff --git a/src/site/xdoc/userguide/howto_utilities.xml b/src/site/xdoc/userguide/howto_utilities.xml
new file mode 100644
index 0000000..df124b0
--- /dev/null
+++ b/src/site/xdoc/userguide/howto_utilities.xml
@@ -0,0 +1,279 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<document>
+
+ <properties>
+ <title>Utility classes and Tips and Tricks Howto</title>
+ <author email="oheger at apache.org">Oliver Heger</author>
+ </properties>
+
+<body>
+ <section name="Utility classes and Tips and Tricks">
+ <p>
+ In this section some utility classes will be introduced that can be used
+ to make handling of configuration objects easier. These classes already
+ provide solutions for some often occurring problems. We will list these
+ problems in no specific order and show how they can be solved with
+ classes provided by <em>Commons Configuration</em>.
+ </p>
+
+ <subsection name="Copy a configuration">
+ <p>
+ Often it is required to copy the data of one <code>Configuration</code>
+ object into another one. For this purpose the
+ <code><a href="../apidocs/org/apache/commons/configuration/AbstractConfiguration.html">
+ AbstractConfiguration</a></code> class (which serves as the base class for
+ most of the configuration implementations shipped with this library)
+ provides two methods implementing a basic copy operation:
+ <ul>
+ <li><code>append()</code> takes the configuration to be copied
+ as argument and adds all of its properties to the current configuration.</li>
+ <li><code>copy()</code> is very similar to <code>append()</code>. The
+ difference is that properties that already exist in the target
+ configuration are replaced by the properties of the source configuration.
+ </li>
+ </ul>
+ </p>
+ <p>
+ These methods work fine if the target configuration is not a hierarchical
+ configuration. If a hierarchical configuration is to be copied into
+ another one, the methods are not able to handle the hierarchical
+ structure; so the resulting configuration will contain all of the
+ properties of the source configuration, but the specific parent-child
+ relations will probably be lost. If a hierarchical configuration needs to
+ be copied, there are the following options:
+ <ul>
+ <li>The <code>clone()</code> method can be used to create a copy of a
+ hierarchical configuration. This also works for non-hierarchical
+ configurations. Most of the configuration implementations provided by
+ <em>Commons Configurations</em> support cloning. The
+ <code>cloneConfiguration()</code> method of
+ <code><a href="../apidocs/org/apache/commons/configuration/ConfigurationUtils.html">
+ ConfigurationUtils</a></code> can be used for creating a copy of an
+ arbitrary <code>Configuration</code> object. This method checks whether
+ the passed in configuration implements the <code>Cloneable</code>
+ interface and, if so, invokes its <code>clone()</code> method.</li>
+ <li>Most hierarchical configurations have a constructor, which takes
+ another hierarchical configuration as argument. This constructor
+ copies the content of the specified configuration into the newly created
+ object.</li>
+ </ul>
+ </p>
+ </subsection>
+
+ <subsection name="Converting a flat configuration into a hierarchical one">
+ <p>
+ <a href="howto_xml.html">Hierarchical configurations</a> provide some
+ enhanced features that are not available for "flat"
+ configurations. For instance they support more sophisticated query
+ facilities. Because of that it may be sometimes useful to transform an
+ ordinary configuration into a hierarchical one. The following code
+ fragment shows how this can be done:
+ </p>
+ <source><![CDATA[
+// Create a flat configuration
+PropertiesConfiguration flatConfig = new PropertiesConfiguration();
+flatConfig.load(...);
+HierarchicalConfiguration hc =
+ ConfigurationUtils.convertToHierarchical(flatConfig);
+]]></source>
+ <p>
+ The <code>convertToHierarchical()</code> method of
+ <code><a href="../apidocs/org/apache/commons/configuration/ConfigurationUtils.html">
+ ConfigurationUtils</a></code> checks whether the passed in object
+ is already a hierarchical configuration. If this is the case, it is
+ returned unchanged. Otherwise a new <code>HierarchicalConfiguration</code>
+ object is created, and the properties of the source configuration are
+ copied into it.
+ </p>
+ <p>
+ Sometimes a flat configuration contains keys with special characters
+ that are not compatible with the expression engine of a hierarchical
+ configuration. For instance, a properties configuration could have the
+ following property:
+ </p>
+ <source><![CDATA[
+test(xy)=true
+]]></source>
+ <p>
+ When processing this property during conversion the default expression
+ engine of the resulting hierarchical configuration will interpret the
+ brackets as an index marker and try to convert the string between the
+ brackets into a number. In this example this fails with a
+ <code>NumberFormatException</code>! The cause for this problem is that the
+ property key contains characters with a special meaning for the default
+ expression engine.
+ </p>
+ <p>
+ To solve this problem, it is possible to specify an alternative expression
+ engine that will be used for the conversion. For instance, if you know that
+ your property keys can contain brackets, you could use an instance of
+ <code>DefaultExpressionEngine</code> that is configured with a different
+ index marker. This could look as follows:
+ </p>
+ <source><![CDATA[
+DefaultExpressionEngine engineConvert = new DefaultExpressionEngine();
+engineConvert.setIndexStart("[");
+engineConvert.setIndexEnd("]");
+HierarchicalConfiguration hc =
+ ConfigurationUtils.convertToHierarchical(flatConfig, engineConvert);
+]]></source>
+ <p>
+ In this example an expression engine is constructed that uses square
+ brackets as index markers. Therefore normal brackets do not have a
+ special meaning and thus are no more problematic during conversion.
+ </p>
+ <p>
+ <em>Note:</em> When using a <a href="howto_combinedconfiguration.html">
+ CombinedConfiguration</a> flat configurations contained in the combined
+ configuration are also converted into hierarchical configurations using
+ the methods discussed here. The <code>CombinedConfiguration</code> class
+ defines the method <code>setConversionExpressionEngine()</code>, which
+ can be called to specify an expression engine to be used during this
+ conversion. The expression engine passed to this method will be
+ propagated to ConfigurationUtils.convertToHierarchical().
+ </p>
+ </subsection>
+
+ <subsection name="Converting between properties and configurations">
+ <p>
+ When working with the JDK the <code>java.util.Properties</code> class is
+ typically used for storing configuration data. If <em>Commons
+ Configuration</em> is to be integrated in such an application, there may
+ be the requirement of converting from <code>Properties</code> objects to
+ <code>Configuration</code> objects and vice versa. For this purpose an
+ utility class can be used:
+ <code><a href="../apidocs/org/apache/commons/configuration/ConfigurationConverter.html">
+ ConfigurationConverter</a></code>.
+ </p>
+ <p>
+ Usage of this class is pretty simple. It provides some static utility
+ methods that perform different conversions. Below you can see some
+ examples. In this fragment we assume that we have a method
+ <code>processConfiguration()</code> that is called from older parts of an
+ application that are not aware of the <em>Commons Configuration</em> API.
+ So they pass in a <code>Properties</code> object and expect one as
+ return value. Inside the method a temporary <code>Configuration</code>
+ object is created and used.
+ </p>
+ <source><![CDATA[
+/**
+ * Does some processing of properties.
+ * @param props the source properties
+ * @return the processed properties
+ */
+Properties processConfiguration(Properties props)
+{
+ // Create a configuration for the properties for easy access
+ Configuration config = ConfigurationConverter.getConfiguration(props);
+
+ // Now use the Configuration API for manipulating the configuration data
+ ...
+
+ // Return a Properties object with the results
+ return ConfigurationConverter.getProperties(config);
+}
+]]></source>
+ <p>
+ Please refer to the Javadocs of
+ <code><a href="../apidocs/org/apache/commons/configuration/ConfigurationConverter.html">
+ ConfigurationConverter</a></code> to learn more about the available
+ conversion methods and their limitations.
+ </p>
+ </subsection>
+
+ <subsection name="Interpolation of all variables">
+ <p>
+ Another issue with the integration of <em>Commons Configuration</em> with
+ native Java applications can be variables: Configuration implementations
+ are able to detect variables like <code>${myReference}</code> or
+ <code>${sys:java.version}</code> in the values of their properties and
+ substitute them by their current values (see the section
+ <a href="howto_basicfeatures.html#Variable_Interpolation">Variable
+ Interpolation</a> for more details). External components probably do not
+ know how to handle such placeholders when processing configuration files
+ written by <em>Commons Configuration</em>.
+ </p>
+ <p>
+ <code><a href="../apidocs/org/apache/commons/configuration/AbstractConfiguration.html">
+ AbstractConfiguration</a></code> provides the method
+ <code>interpolatedConfiguration()</code>. This method creates a clone of
+ the current configuration and then performs interpolation on all of its
+ properties. So the result of this method is a configuration object with
+ basically the same content as the original configuration, but with all
+ variables replaced by their actual values (as far as this was possible).
+ The following code fragment shows how a
+ <code><a href="../apidocs/org/apache/commons/configuration/PropertiesConfiguration.html">
+ PropertiesConfiguration</a></code> object can be saved in a way that the
+ resulting properties file does not contain any variables:
+ </p>
+ <source><![CDATA[
+// Load a properties file (which may contain variables)
+PropertiesConfiguration config = new PropertiesConfiguration("config.properties");
+
+// Perform interpolation on all variables
+PropertiesConfiguration extConfig =
+ (PropertiesConfiguration) config.interpolatedConfiguration();
+
+// Save the interpolated configuration (no more variables)
+extConfig.save("external_config.properties");
+]]></source>
+ </subsection>
+
+ <subsection name="Handling of runtime exceptions">
+ <p>
+ Section <a href="howto_events.html#Error_listeners">Error listeners</a>
+ introduces a way of dealing with runtime exceptions that can occur on
+ accessing configuration properties by registering an event listener. If
+ you do not want to provide a special error handler, but only need to
+ propagate the exception that caused the error event, you can make use of
+ a convenience method of the
+ <code><a href="../apidocs/org/apache/commons/configuration/ConfigurationUtils.html">
+ ConfigurationUtils</a></code> class: <code>enableRuntimeExceptions()</code>
+ registers a special error listener at the passed in configuration that
+ will throw a
+ <code><a href="../apidocs/org/apache/commons/configuration/ConfigurationRuntimeException.html">
+ ConfigurationRuntimeException</a></code> exception for each received
+ error event. The following code fragment shows an example of using this
+ method:
+ </p>
+ <source><![CDATA[
+JNDIConfiguration config = new JNDIConfiguration();
+ConfigurationUtils.enableRuntimeExceptions(config);
+
+// This may now throw a ConfigurationRuntimeException
+String value = config.getString("myKey");
+]]></source>
+ <p>
+ <code>enableRuntimeExceptions()</code> can be called for all
+ <code>Configuration</code> implementations that are derived from
+ <code><a href="../apidocs/org/apache/commons/configuration/event/EventSource.html">
+ EventSource</a></code> (which is the case for almost all configuration
+ classes provided by this library). Of course the affected implementation
+ must support the mechanism of error events, otherwise the registered
+ listener will not be triggered. In
+ <a href="howto_events.html#Error_listeners">Error listeners</a> more
+ information can be found.
+ </p>
+ </subsection>
+
+ </section>
+</body>
+
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/userguide/howto_xml.xml b/src/site/xdoc/userguide/howto_xml.xml
new file mode 100644
index 0000000..67b6b15
--- /dev/null
+++ b/src/site/xdoc/userguide/howto_xml.xml
@@ -0,0 +1,1163 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<document>
+
+ <properties>
+ <title>Hierarchical configurations and XML Howto</title>
+ <author email="oheger at apache.org">Oliver Heger</author>
+ </properties>
+
+<body>
+ <section name="Using Hierarchical Configurations">
+ <p>
+ This section explains how to use hierarchical
+ and structured XML datasets.
+ </p>
+ </section>
+
+ <section name="Hierarchical properties">
+ <p>
+ Many sources of configuration data have a hierarchical or tree-like
+ nature. They can represent data that is structured in many ways.
+ Such configuration sources are represented by classes derived from
+ <a href="../apidocs/org/apache/commons/configuration/HierarchicalConfiguration.html">
+ <code>HierarchicalConfiguration</code></a>.
+ </p>
+ <p>
+ Prominent examples of hierarchical configuration sources are XML
+ documents. They can be read and written using the
+ <a href="../apidocs/org/apache/commons/configuration/XMLConfiguration.html">
+ <code>XMLConfiguration</code></a> class. This section explains how
+ to deal with such structured data and demonstrates the enhanced query
+ facilities supported by <code>HierarchicalConfiguration</code>. We
+ use XML documents as examples for structured configuration sources,
+ but the information provided here (especially the rules for accessing
+ properties) applies to other hierarchical configurations as well.
+ Examples for other hierarchical configuration classes are
+ <ul>
+ <li><a href="../apidocs/org/apache/commons/configuration/CombinedConfiguration.html">
+ <code>CombinedConfiguration</code></a></li>
+ <li><a href="../apidocs/org/apache/commons/configuration/HierarchicalINIConfiguration.html">
+ <code>HierarchicalINIConfiguration</code></a></li>
+ <li><a href="../apidocs/org/apache/commons/configuration/plist/PropertyListConfiguration.html">
+ <code>PropertyListConfiguration</code></a></li>
+ </ul>
+ </p>
+ <subsection name="Accessing properties in hierarchical configurations">
+ <p>
+ We will start with a simple XML document to show some basics
+ about accessing properties. The following file named
+ <code>gui.xml</code> is used as example document:
+ </p>
+ <source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<gui-definition>
+ <colors>
+ <background>#808080</background>
+ <text>#000000</text>
+ <header>#008000</header>
+ <link normal="#000080" visited="#800080"/>
+ <default>${colors.header}</default>
+ </colors>
+ <rowsPerPage>15</rowsPerPage>
+ <buttons>
+ <name>OK,Cancel,Help</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+</gui-definition>
+]]></source>
+ <p>
+ (As becomes obvious, this tutorial does not bother with good
+ design of XML documents, the example file should rather
+ demonstrate the different ways of accessing properties.)
+ To access the data stored in this document it must be loaded
+ by <code>XMLConfiguration</code>. Like other
+ <a href="howto_filebased.html">file based</a>
+ configuration classes <code>XMLConfiguration</code> supports
+ many ways of specifying the file to process. One way is to
+ pass the file name to the constructor as shown in the following
+ code fragment:
+ </p>
+ <source><![CDATA[
+try
+{
+ XMLConfiguration config = new XMLConfiguration("tables.xml");
+ // do something with config
+}
+catch(ConfigurationException cex)
+{
+ // something went wrong, e.g. the file was not found
+}
+]]></source>
+ <p>
+ If no exception was thrown, the properties defined in the
+ XML document are now available in the configuration object.
+ Other hierarchical configuration classes that operate on files
+ have corresponding constructors and methods for loading their data.
+ The following fragment shows how the properties can be accessed:
+ </p>
+ <source><![CDATA[
+String backColor = config.getString("colors.background");
+String textColor = config.getString("colors.text");
+String linkNormal = config.getString("colors.link[@normal]");
+String defColor = config.getString("colors.default");
+int rowsPerPage = config.getInt("rowsPerPage");
+List<Object> buttons = config.getList("buttons.name");
+]]></source>
+ <p>
+ This listing demonstrates some important points about constructing
+ keys for accessing properties in hierarchical configuration sources and about
+ features of <code>HierarchicalConfiguration</code> in general:
+ <ul>
+ <li>
+ Nested elements are accessed using a dot notation. In
+ the example document there is an element
+ <code><text></code> in the body of the
+ <code><color></code> element. The corresponding
+ key is <code>color.text</code>.
+ </li>
+ <li>
+ The root element is ignored when constructing keys. In
+ the example you do not write
+ <code>gui-definition.color.text</code>, but only
+ <code>color.text</code>.
+ </li>
+ <li>
+ Attributes of XML elements are accessed in a XPath like
+ notation.
+ </li>
+ <li>
+ Interpolation can be used as in <code>PropertiesConfiguration</code>.
+ Here the <code><default></code> element in the
+ <code>colors</code> section refers to another color.
+ </li>
+ <li>
+ Lists of properties can be defined in a short form using
+ the delimiter character (which is the comma by default).
+ In this example the <code>buttons.name</code> property
+ has the three values <em>OK</em>, <em>Cancel</em>, and
+ <em>Help</em>, so it is queried using the <code>getList()</code>
+ method. This works in attributes, too. Using the static
+ <code>setDefaultDelimiter()</code> method of
+ <code>AbstractConfiguration</code> you can globally
+ define a different delimiter character or -
+ by setting the delimiter to 0 - disabling this mechanism
+ completely. Placing a backslash before a delimiter
+ character will escape it. This is demonstrated in the
+ <code>pattern</code> attribute of the <code>numberFormat</code>
+ element.
+ </li>
+ </ul>
+ </p>
+ <p>
+ In the next section will show how data in a more complex XML
+ document can be processed.
+ </p>
+ </subsection>
+ <subsection name="Complex hierarchical structures">
+ <p>
+ Consider the following scenario: An application operates on
+ database tables and wants to load a definition of the database
+ schema from its configuration. A XML document provides this
+ information. It could look as follows:
+ </p>
+ <source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+
+<database>
+ <tables>
+ <table tableType="system">
+ <name>users</name>
+ <fields>
+ <field>
+ <name>uid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>uname</name>
+ <type>java.lang.String</type>
+ </field>
+ <field>
+ <name>firstName</name>
+ <type>java.lang.String</type>
+ </field>
+ <field>
+ <name>lastName</name>
+ <type>java.lang.String</type>
+ </field>
+ <field>
+ <name>email</name>
+ <type>java.lang.String</type>
+ </field>
+ </fields>
+ </table>
+ <table tableType="application">
+ <name>documents</name>
+ <fields>
+ <field>
+ <name>docid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>name</name>
+ <type>java.lang.String</type>
+ </field>
+ <field>
+ <name>creationDate</name>
+ <type>java.util.Date</type>
+ </field>
+ <field>
+ <name>authorID</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>version</name>
+ <type>int</type>
+ </field>
+ </fields>
+ </table>
+ </tables>
+</database>
+]]></source>
+ <p>
+ This XML is quite self explanatory; there is an arbitrary number
+ of table elements, each of it has a name and a list of fields.
+ A field in turn consists of a name and a data type. This
+ XML document (let's call it <code>tables.xml</code>) can be
+ loaded in exactly the same way as the simple document in the
+ section before.
+ </p>
+ <p>
+ When we now want to access some of the properties we face a
+ problem: the syntax for constructing configuration keys we
+ learned so far is not powerful enough to access all of the data
+ stored in the tables document.
+ </p>
+ <p>
+ Because the document contains a list of tables some properties
+ are defined more than once. E.g. the configuration key
+ <code>tables.table.name</code> refers to a <code>name</code>
+ element inside a <code>table</code> element inside a
+ <code>tables</code> element. This constellation happens to
+ occur twice in the tables document.
+ </p>
+ <p>
+ Multiple definitions of a property do not cause problems and are
+ supported by all classes of Configuration. If such a property
+ is queried using <code>getProperty()</code>, the method
+ recognizes that there are multiple values for that property and
+ returns a collection with all these values. So we could write
+ </p>
+ <source><![CDATA[
+Object prop = config.getProperty("tables.table.name");
+if(prop instanceof Collection)
+{
+ System.out.println("Number of tables: " + ((Collection<?>) prop).size());
+}
+]]></source>
+ <p>
+ An alternative to this code would be the <code>getList()</code>
+ method of <code>Configuration</code>. If a property is known to
+ have multiple values (as is the table name property in this example),
+ <code>getList()</code> allows retrieving all values at once.
+ <b>Note:</b> it is legal to call <code>getString()</code>
+ or one of the other getter methods on a property with multiple
+ values; it returns the first element of the list.
+ </p>
+ </subsection>
+ <subsection name="Accessing structured properties">
+ <p>
+ Okay, we can obtain a list with the names of all defined
+ tables. In the same way we can retrieve a list with the names
+ of all table fields: just pass the key
+ <code>tables.table.fields.field.name</code> to the
+ <code>getList()</code> method. In our example this list
+ would contain 10 elements, the names of all fields of all tables.
+ This is fine, but how do we know, which field belongs to
+ which table?
+ </p>
+ <p>
+ When working with such hierarchical structures the configuration keys
+ used to query properties can have an extended syntax. All components
+ of a key can be appended by a numerical value in parentheses that
+ determines the index of the affected property. So if we have two
+ <code>table</code> elements we can exactly specify, which one we
+ want to address by appending the corresponding index. This is
+ explained best by some examples:
+ </p>
+ <p>
+ We will now provide some configuration keys and show the results
+ of a <code>getProperty()</code> call with these keys as arguments.
+ <dl>
+ <dt><code>tables.table(0).name</code></dt>
+ <dd>
+ Returns the name of the first table (all indices are 0 based),
+ in this example the string <em>users</em>.
+ </dd>
+ <dt><code>tables.table(0)[@tableType]</code></dt>
+ <dd>
+ Returns the value of the tableType attribute of the first
+ table (<em>system</em>).
+ </dd>
+ <dt><code>tables.table(1).name</code></dt>
+ <dd>
+ Analogous to the first example returns the name of the
+ second table (<em>documents</em>).
+ </dd>
+ <dt><code>tables.table(2).name</code></dt>
+ <dd>
+ Here the name of a third table is queried, but because there
+ are only two tables result is <b>null</b>. The fact that a
+ <b>null</b> value is returned for invalid indices can be used
+ to find out how many values are defined for a certain property:
+ just increment the index in a loop as long as valid objects
+ are returned.
+ </dd>
+ <dt><code>tables.table(1).fields.field.name</code></dt>
+ <dd>
+ Returns a collection with the names of all fields that
+ belong to the second table. With such kind of keys it is
+ now possible to find out, which fields belong to which table.
+ </dd>
+ <dt><code>tables.table(1).fields.field(2).name</code></dt>
+ <dd>
+ The additional index after field selects a certain field.
+ This expression represents the name of the third field in
+ the second table (<em>creationDate</em>).
+ </dd>
+ <dt><code>tables.table.fields.field(0).type</code></dt>
+ <dd>
+ This key may be a bit unusual but nevertheless completely
+ valid. It selects the data types of the first fields in all
+ tables. So here a collection would be returned with the
+ values [<em>long, long</em>].
+ </dd>
+ </dl>
+ </p>
+ <p>
+ These examples should make the usage of indices quite clear.
+ Because each configuration key can contain an arbitrary number
+ of indices it is possible to navigate through complex structures of
+ hierarchical configurations; each property can be uniquely identified.
+ </p>
+ <p>
+ Sometimes dealing with long property keys may become inconvenient,
+ especially if always the same properties are accessed. For this
+ case <code>HierarchicalConfiguration</code> provides a short cut
+ with the <code>configurationAt()</code> method. This method can
+ be passed a key that selects exactly one node of the hierarchy
+ of nodes contained in a hierarchical configuration. Then a new
+ hierarchical configuration will be returned whose root node is
+ the selected node. So all property keys passed into that
+ configuration should be relative to the new root node. For
+ instance, if we are only interested in information about the
+ first database table, we could do something like that:
+ </p>
+ <source><![CDATA[
+HierarchicalConfiguration sub = config.configurationAt("tables.table(0)");
+String tableName = sub.getString("name"); // only need to provide relative path
+List<Object> fieldNames = sub.getList("fields.field.name");
+]]></source>
+ <p>
+ For dealing with complex list-like structures there is another
+ short cut. Often it will be necessary to iterate over all items
+ in the list and access their (sub) properties. A good example are
+ the fields of the tables in our demo configuration. When you want
+ to process all fields of a table (e.g. for constructing a
+ <code>CREATE TABLE</code> statement), you will need all information
+ stored for them in the configuration. An option would be to use
+ the <code>getList()</code> method to fetch the required data one
+ by one:
+ </p>
+ <source><![CDATA[
+List<Object> fieldNames = config.getList("tables.table(0).fields.field.name");
+List<Object> fieldTypes = config.getList("tables.table(0).fields.field.type");
+List<Object> ... // further calls for other data that might be stored in the config
+]]></source>
+ <p>
+ But this is not very readable and will fail if not all field
+ elements contain the same set of data (for instance the
+ <code>type</code> property may be optional, then the list for
+ the types can contain less elements than the other lists). A
+ solution to these problems is the <code>configurationsAt()</code>
+ method, a close relative to the <code>configurationAt()</code>
+ method covered above. This method evaluates the passed in key and
+ collects all configuration nodes that match this criterion. Then
+ for each node a <code>HierarchicalConfiguration</code> object is
+ created with this node as root node. A list with these configuration
+ objects is returned. As the following example shows this comes in
+ very handy when processing list-like structures:
+ </p>
+ <source><![CDATA[
+List<HierarchicalConfiguration> fields =
+ config.configurationsAt("tables.table(0).fields.field");
+for(HierarchicalConfiguration sub : fields)
+{
+ // sub contains all data about a single field
+ String fieldName = sub.getString("name");
+ String fieldType = sub.getString("type");
+ ...
+]]></source>
+ <p>
+ The configurations returned by the <code>configurationAt()</code> and
+ <code>configurationsAt()</code> method are in fact instances of the
+ <a href="../apidocs/org/apache/commons/configuration/SubnodeConfiguration.html">
+ <code>SubnodeConfiguration</code></a> class. The API documentation of
+ this class contains more information about its features and
+ limitations.
+ </p>
+ </subsection>
+ <subsection name="Adding new properties">
+ <p>
+ So far we have learned how to use indices to avoid ambiguities when
+ querying properties. The same problem occurs when adding new
+ properties to a structured configuration. As an example let's
+ assume we want to add a new field to the second table. New properties
+ can be added to a configuration using the <code>addProperty()</code>
+ method. Of course, we have to exactly specify where in the tree like structure new
+ data is to be inserted. A statement like
+ </p>
+ <source><![CDATA[
+// Warning: This might cause trouble!
+config.addProperty("tables.table.fields.field.name", "size");
+]]></source>
+ <p>
+ would not be sufficient because it does not contain all needed
+ information. How is such a statement processed by the
+ <code>addProperty()</code> method?
+ </p>
+ <p>
+ <code>addProperty()</code> splits the provided key into its
+ single parts and navigates through the properties tree along the
+ corresponding element names. In this example it will start at the
+ root element and then find the <code>tables</code> element. The
+ next key part to be processed is <code>table</code>, but here a
+ problem occurs: the configuration contains two <code>table</code>
+ properties below the <code>tables</code> element. To get rid off
+ this ambiguity an index can be specified at this position in the
+ key that makes clear, which of the two properties should be
+ followed. <code>tables.table(1).fields.field.name</code> e.g.
+ would select the second <code>table</code> property. If an index
+ is missing, <code>addProperty()</code> always follows the last
+ available element. In our example this would be the second
+ <code>table</code>, too.
+ </p>
+ <p>
+ The following parts of the key are processed in exactly the same
+ manner. Under the selected <code>table</code> property there is
+ exactly one <code>fields</code> property, so this step is not
+ problematic at all. In the next step the <code>field</code> part
+ has to be processed. At the actual position in the properties tree
+ there are multiple <code>field</code> (sub) properties. So we here
+ have the same situation as for the <code>table</code> part.
+ Because no explicit index is defined the last <code>field</code>
+ property is selected. The last part of the key passed to
+ <code>addProperty()</code> (<code>name</code> in this example)
+ will always be added as new property at the position that has
+ been reached in the former processing steps. So in our example
+ the last <code>field</code> property of the second table would
+ be given a new <code>name</code> sub property and the resulting
+ structure would look like the following listing:
+ </p>
+ <source><![CDATA[
+ ...
+ <table tableType="application">
+ <name>documents</name>
+ <fields>
+ <field>
+ <name>docid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>name</name>
+ <type>java.lang.String</type>
+ </field>
+ <field>
+ <name>creationDate</name>
+ <type>java.util.Date</type>
+ </field>
+ <field>
+ <name>authorID</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>version</name>
+ <name>size</name> <== Newly added property
+ <type>int</type>
+ </field>
+ </fields>
+ </table>
+ </tables>
+</database>
+]]></source>
+ <p>
+ This result is obviously not what was desired, but it demonstrates
+ how <code>addProperty()</code> works: the method follows an
+ existing branch in the properties tree and adds new leaves to it.
+ (If the passed in key does not match a branch in the existing tree,
+ a new branch will be added. E.g. if we pass the key
+ <code>tables.table.data.first.test</code>, the existing tree can be
+ navigated until the <code>data</code> part of the key. From here a
+ new branch is started with the remaining parts <code>data</code>,
+ <code>first</code> and <code>test</code>.)
+ </p>
+ <p>
+ If we want a different behavior, we must explicitely tell
+ <code>addProperty()</code> what to do. In our example with the
+ new field our intension was to create a new branch for the
+ <code>field</code> part in the key, so that a new <code>field</code>
+ property is added to the structure rather than adding sub properties
+ to the last existing <code>field</code> property. This can be
+ achieved by specifying the special index <code>(-1)</code> at the
+ corresponding position in the key as shown below:
+ </p>
+ <source><![CDATA[
+config.addProperty("tables.table(1).fields.field(-1).name", "size");
+config.addProperty("tables.table(1).fields.field.type", "int");
+]]></source>
+ <p>
+ The first line in this fragment specifies that a new branch is
+ to be created for the <code>field</code> property (index -1).
+ In the second line no index is specified for the field, so the
+ last one is used - which happens to be the field that has just
+ been created. So these two statements add a fully defined field
+ to the second table. This is the default pattern for adding new
+ properties or whole hierarchies of properties: first create a new
+ branch in the properties tree and then populate its sub properties.
+ As an additional example let's add a complete new table definition
+ to our example configuration:
+ </p>
+ <source><![CDATA[
+// Add a new table element and define the name
+config.addProperty("tables.table(-1).name", "versions");
+
+// Add a new field to the new table
+// (an index for the table is not necessary because the latest is used)
+config.addProperty("tables.table.fields.field(-1).name", "id");
+config.addProperty("tables.table.fields.field.type", "int");
+
+// Add another field to the new table
+config.addProperty("tables.table.fields.field(-1).name", "date");
+config.addProperty("tables.table.fields.field.type", "java.sql.Date");
+...
+]]></source>
+ <p>
+ For more information about adding properties to a hierarchical
+ configuration also have a look at the javadocs for
+ <code>HierarchicalConfiguration</code>.
+ </p>
+ </subsection>
+ <subsection name="Escaping special characters">
+ <p>
+ Some characters in property keys or values require a special
+ treatment.
+ </p>
+ <p>
+ Per default the dot character is used as delimiter by most
+ configuration classes (we will learn how to change this for
+ hierarchical configurations in a later section). In some
+ configuration formats however, dots can be contained in the
+ names of properties. For instance, in XML the dot is a legal
+ character that can occur in any tag. The same is true for the names
+ of properties in windows ini files. So the following XML
+ document is completely valid:
+ </p>
+ <source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+
+<configuration>
+ <test.value>42</test.value>
+ <test.complex>
+ <test.sub.element>many dots</test.sub.element>
+ </test.complex>
+</configuration>
+]]></source>
+ <p>
+ This XML document can be loaded by <code>XMLConfiguration</code>
+ without trouble, but when we want to access certain properties
+ we face a problem: The configuration claims that it does not
+ store any values for the properties with the keys
+ <code>test.value</code> or <code>test.complex.test.sub.element</code>!
+ </p>
+ <p>
+ Of course, it is the dot character contained in the property
+ names, which causes this problem. A dot is always interpreted
+ as a delimiter between elements. So given the property key
+ <code>test.value</code> the configuration would look for an
+ element named <code>test</code> and then for a sub element
+ with the name <code>value</code>. To change this behavior it is
+ possible to escape a dot character, thus telling the configuration
+ that it is really part of an element name. This is simply done
+ by duplicating the dot. So the following statements will return
+ the desired property values:
+ </p>
+ <source><![CDATA[
+int testVal = config.getInt("test..value");
+String complex = config.getString("test..complex.test..sub..element");
+]]></source>
+ <p>
+ Note the duplicated dots whereever the dot does not act as
+ delimiter. This way it is possible to access properties containing
+ dots in arbitrary combination. However, as you can see, the
+ escaping can be confusing sometimes. So if you have a choice,
+ you should avoid dots in the tag names of your XML configuration
+ files or other configuration sources.
+ </p>
+ <p>
+ Another source of problems is related to list delimiter characters
+ in the values of properties. Like other configuration classes
+ <code>XMLConfiguration</code> implements
+ <a href="howto_basicfeatures.html#List_handling">list handling</a>.
+ This means that the values of XML elements and attributes are
+ checked whether they contain a list delimiter character. If this
+ is the case, the value is split, and a list property is created.
+ Per default this feature is enabled. Have a look at the
+ following example:
+ </p>
+ <source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+
+<configuration>
+ <pi>3,1415</pi>
+</configuration>
+]]></source>
+ <p>
+ Here we use the comma as delimiter for fraction digits (as is
+ standard for some languages). However, the configuration will
+ interpret the comma as list delimiter character and assign the
+ property <em>pi</em> the two values 3 and 1415. This was not
+ desired.
+ </p>
+ <p>
+ XML has a natural way of defining list properties by simply
+ repeating elements. So defining multiple values of a property in
+ a single element or attribute is a rather untypical use case.
+ Unfortunately, early versions of Commons Configuration had list
+ delimiter splitting enabled per default. Later it became obvious
+ that this feature can cause serious problems related to the
+ interpretation of property values and the escaping of delimiter
+ characters. For reasons of backwards compatibility we have to
+ stick to this approach in the 1.x series though.
+ </p>
+ <p>
+ In the next major release the handling of lists will propably be
+ reworked. Therefore it is recommended not to use this feature.
+ You are save if you disable it immediately after the creation of
+ an <code>XMLConfiguration</code> object (and before a file is
+ loaded). This can be achieved as follows:
+ </p>
+ <source><![CDATA[
+XMLConfiguration config = new XMLConfiguration();
+config.setDelimiterParsingDisabled(true);
+config.setAttributeSplittingDisabled(true);
+config.load("config.xml");
+]]></source>
+ </subsection>
+ </section>
+
+ <section name="Expression engines">
+ <p>
+ In the previous chapters we saw many examples about how properties
+ in a <code>XMLConfiguration</code> object (or more general in a
+ <code>HierarchicalConfiguration</code> object, because this is the
+ base class, which implements this functionality) can be queried or
+ modified using a special syntax for the property keys. Well, this
+ was not the full truth. Actually, property keys are not processed
+ by the configuration object itself, but are delegated to a helper
+ object, a so called <em>Expression engine</em>.
+ </p>
+ <p>
+ The separation of the task of interpreting property keys into a
+ helper object is a typical application of the <em>Strategy</em>
+ design pattern. In this case it also has the advantage that it
+ becomes possible to plug in different expression engines into a
+ <code>HierarchicalConfiguration</code> object. So by providing
+ different implementations of the
+ <a href="../apidocs/org/apache/commons/configuration/tree/ExpressionEngine.html">
+ <code>ExpressionEngine</code></a>
+ interface hierarchical configurations can support alternative
+ expression languages for accessing their data.
+ </p>
+ <p>
+ Before we discuss the available expression engines that ship
+ with Commons Configuration, it should be explained how an
+ expression engine can be associated with a configuration object.
+ <a href="../apidocs/org/apache/commons/configuration/HierarchicalConfiguration.html">
+ <code>HierarchicalConfiguration</code></a> and all derived classes
+ provide a <code>setExpressionEngine()</code> method, which expects
+ an implementation of the <code>ExpressionEngine</code> interface as
+ argument. After this method was called, the configuration object will
+ use the passed expression engine, which means that all property keys
+ passed to methods like <code>getProperty()</code>,
+ <code>getString()</code>, or <code>addProperty()</code> must
+ conform to the syntax supported by this engine. Property keys
+ returned by the <code>getKeys()</code> method will follow this
+ syntax, too.
+ </p>
+ <p>
+ In addition to instance specific expression engines that change the
+ behavior of single configuration objects it is also possible to set
+ a global expression engine. This engine is shared between all
+ hierarchical configuration objects, for which no specific expression
+ engine was set. The global expression engine can be set using the
+ static <code>setDefaultExpressionEngine()</code> method of
+ <code>HierarchicalConfiguration</code>. By invoking this method with
+ a custom expression engine the syntax of all hierarchical configuration
+ objects can be altered at once.
+ </p>
+
+ <subsection name="The default expression engine">
+ <p>
+ The syntax described so far for property keys of hierarchical
+ configurations is implemented by a specific implementation of the
+ <a href="../apidocs/org/apache/commons/configuration/tree/ExpressionEngine.html">
+ <code>ExpressionEngine</code></a> interface called
+ <a href="../apidocs/org/apache/commons/configuration/tree/DefaultExpressionEngine.html">
+ <code>DefaultExpressionEngine</code></a>. An instance of this class
+ is installed as the global expression engine in
+ <code>HierarchicalConfiguration</code>. So all newly created
+ instances of this class will make use of this engine (which is
+ the reason that our examples above worked).
+ </p>
+ <p>
+ After reading the examples of property keys provided so far in
+ this document you should have a sound understanding regarding
+ the features and the syntax supported by the
+ <code>DefaultExpressionEngine</code> class. But it can do a
+ little bit more for you: it defines a bunch of properties,
+ which can be used to customize most tokens that can appear in a
+ valid property key. You prefer curly brackets over paranthesis
+ as index markers? You find the duplicated dot as escaped
+ property delimiter counter-intuitive? Well, simply go ahead and
+ change it! The following example shows how the syntax of a
+ <code>DefaultExpressionEngine</code> object is modified. Then
+ this object is set as the global expression engine, so that from
+ now on all hierarchical configuration objects will take up this
+ new syntax:
+ </p>
+ <source><![CDATA[
+DefaultExpressionEngine engine = new DefaultExpressionEngine();
+
+// Use a slash as property delimiter
+engine.setPropertyDelimiter("/");
+// Indices should be provided in curly brackets
+engine.setIndexStart("{");
+engine.setIndexEnd("}");
+// For attributes use simply a @
+engine.setAttributeStart("@");
+engine.setAttributeEnd(null);
+// A Backslash is used for escaping property delimiters
+engine.setEscapedDelimiter("\\/");
+
+// Now install this engine as the global engine
+HierarchicalConfiguration.setDefaultExpressionEngine(engine);
+
+// Access properties using the new syntax
+HierarchicalConfiguration config = ...
+String tableName = config.getString("tables/table{0}/name");
+String tableType = config.getString("tables/table{0}@type");
+ ]]></source>
+ <p>
+ <em>Tip:</em> Sometimes when processing an XML document you
+ don't want to distinguish between attributes and "normal"
+ child nodes. You can achieve this by setting the
+ <code>AttributeEnd</code> property to <b>null</b> and the
+ <code>AttributeStart</code> property to the same value as the
+ <code>PropertyDelimiter</code> property. Then the syntax for
+ accessing attributes is the same as the syntax for other
+ properties:
+ </p>
+ <source><![CDATA[
+DefaultExpressionEngine engine = new DefaultExpressionEngine();
+engine.setAttributeEnd(null);
+engine.setAttributeStart(engine.getPropertyDelimiter());
+...
+Object value = config.getProperty("tables.table(0).name");
+// name can either be a child node of table or an attribute
+ ]]></source>
+ </subsection>
+
+ <subsection name="The XPATH expression engine">
+ <p>
+ The expression language provided by the <code>DefaultExpressionEngine</code>
+ class is powerful enough to address all properties in a
+ hierarchical configuration, but it is not always convenient to
+ use. Especially if list structures are involved, it is often
+ necessary to iterate through the whole list to find a certain
+ element.
+ </p>
+ <p>
+ Think about our example configuration that stores information about
+ database tables. A use case could be to load all fields that belong
+ to the "users" table. If you knew the index of this
+ table, you could simply build a property key like
+ <code>tables.table(<index>).fields.field.name</code>,
+ but how do you find out the correct index? When using the
+ default expression engine, the only solution to this problem is
+ to iterate over all tables until you find the "users"
+ table.
+ </p>
+ <p>
+ Life would be much easier if an expression language could be used,
+ which would directly support queries of such kind. In the XML
+ world, the XPATH syntax has grown popular as a powerful means
+ of querying structured data. In XPATH a query that selects all
+ field names of the "users" table would look something
+ like <code>tables/table[@name='users']/fields/name</code> (here
+ we assume that the table's name is modelled as an attribute).
+ This is not only much simpler than an iteration over all tables,
+ but also much more readable: it is quite obvious, which fields
+ are selected by this query.
+ </p>
+ <p>
+ Given the power of XPATH it is no wonder that we got many
+ user requests to add XPATH support to Commons Configuration.
+ Well, here is it!
+ </p>
+ <p>
+ For enabling XPATH syntax for property keys you need the
+ <a href="../apidocs/org/apache/commons/configuration/tree/xpath/XPathExpressionEngine.html">
+ <code>XPathExpressionEngine</code></a> class. This class
+ implements the <code>ExpressionEngine</code> interface and can
+ be plugged into a <code>HierarchicalConfiguration</code> object
+ using the <code>setExpressionEngine()</code> method. It is also
+ possible to set an instance of this class as the global
+ expression engine, so that all hierarchical configuration
+ objects make use of XPATH syntax. The following code fragment
+ shows how XPATH support can be enabled for a configuration
+ object:
+ </p>
+ <source><![CDATA[
+HierarchicalConfiguration config = ...
+config.setExpressionEngine(new XPathExpressionEngine());
+
+// Now we can use XPATH queries:
+List<Object> fields = config.getList("tables/table[1]/fields/name");
+ ]]></source>
+ <p>
+ XPATH expressions are not only used for selecting properties
+ (i.e. for the several getter methods), but also for adding new
+ properties. For this purpose the keys passed into the
+ <code>addProperty()</code> method must conform to a special
+ syntax. They consist of two parts: the first part is an
+ arbitrary XPATH expression that selects the node where the new
+ property is to be added to, the second part defines the new
+ element to be added. Both parts are separated by whitespace.
+ </p>
+ <p>
+ Okay, let's make an example. Say, we want to add a <code>type</code>
+ property under the first table (as a sibling to the <code>name</code>
+ element). Then the first part of our key will have to select
+ the first table element, the second part will simply be
+ <code>type</code>, i.e. the name of the new property:
+ </p>
+ <source><![CDATA[
+config.addProperty("tables/table[1] type", "system");
+ ]]></source>
+ <p>
+ (Note that indices in XPATH are 1-based, while in the default
+ expression language they are 0-based.) In this example the part
+ <code>tables/table[1]</code> selects the target element of the
+ add operation. This element must exist and must be unique, otherwise an exception
+ will be thrown. <code>type</code> is the name of the new element
+ that will be added. If instead of a normal element an attribute
+ should be added, the example becomes
+ </p>
+ <source><![CDATA[
+config.addProperty("tables/table[1] @type", "system");
+ ]]></source>
+ <p>
+ It is possible to add complete paths at once. Then the single
+ elements in the new path are separated by "/"
+ characters. The following example shows how data about a new
+ table can be added to the configuration. Here we use full paths:
+ </p>
+ <source><![CDATA[
+// Add new table "tasks" with name element and type attribute
+config.addProperty("tables table/name", "tasks");
+// last() selects the last element of this name,
+// which is the newest table element
+config.addProperty("tables/table[last()] @type", "system");
+
+// Now add fields
+config.addProperty("tables/table[last()] fields/field/name", "taskid");
+config.addProperty("tables/table[last()]/fields/field[last()] type", "int");
+config.addProperty("tables/table[last()]/fields field/name", "name");
+config.addProperty("tables/table[last()]/fields field/name", "startDate");
+...
+ ]]></source>
+ <p>
+ The first line of this example adds the path <code>table/name</code>
+ to the <code>tables</code> element, i.e. a new <code>table</code>
+ element will be created and added as last child to the
+ <code>tables</code> element. Then a new <code>name</code> element
+ is added as child to the new <code>table</code> element. To this
+ element the value "tasks" is assigned. The next line
+ adds a <code>type</code> attribute to the new table element. To
+ obtain the correct <code>table</code> element, to which the
+ attribute must be added, the XPATH function <code>last()</code>
+ is used; this function selects the last element with a given
+ name, which in this case is the new <code>table</code> element.
+ The following lines all use the same approach to construct a new
+ element hierarchy: At first complete new branches are added
+ (<code>fields/field/name</code>), then to the newly created
+ elements further children are added.
+ </p>
+ <p>
+ There is one gotcha with these keys described so far: they do
+ not work with the <code>setProperty()</code> method! This is
+ because <code>setProperty()</code> has to check whether the
+ passed in key already exists; therefore it needs a key which can
+ be interpreted by query methods. If you want to use
+ <code>setProperty()</code>, you can pass in regular keys (i.e.
+ without a whitespace separator). The method then tries to figure
+ out which part of the key already exists in the configuration
+ and adds new nodes as necessary. In principle such regular keys
+ can also be used with <code>addProperty()</code>. However, they
+ do not contain sufficient information to decide where new nodes
+ should be added.
+ </p>
+ <p>
+ To make this clearer let's go back to the example with the
+ tables. Consider that there is a configuration which already
+ contains information about some database tables. In order to add
+ a new table element in the configuration
+ <code>addProperty()</code> could be used as follows:
+ </p>
+ <source><![CDATA[
+config.addProperty("tables/table/name", "documents");
+ ]]></source>
+ <p>
+ In the configuration a <code><tables></code> element
+ already exists, also <code><table></code> and
+ <code><name></code> elements. How should the expression
+ engine know where new node structures are to be added? The
+ solution to this problem is to provide this information in the
+ key by stating:
+ </p>
+ <source><![CDATA[
+config.addProperty("tables table/name", "documents");
+ ]]></source>
+ <p>
+ Now it is clear that new nodes should be added as children of
+ the <code><tables></code> element. More information about
+ keys and how they play together with <code>addProperty()</code>
+ and <code>setProperty()</code> can be found in the Javadocs for
+ <a href="../apidocs/org/apache/commons/configuration/tree/xpath/XPathExpressionEngine.html">
+ <code>XPathExpressionEngine</code></a>.
+ </p>
+ <p>
+ <em>Note:</em> XPATH support is implemented through
+ <a href="http://commons.apache.org/jxpath">Commons JXPath</a>.
+ So when making use of this feature, be sure you include the
+ commons-jxpath jar in your classpath.
+ </p>
+ <p>
+ In this tutorial we don't want to describe XPATH syntax and
+ expressions in detail. Please refer to corresponding documentation.
+ It is important to mention that by embedding Commons JXPath the
+ full extent of the XPATH 1.0 standard can be used for constructing
+ property keys.
+ </p>
+ </subsection>
+ </section>
+
+ <section name="Validation of XML configuration files">
+ <p>
+ XML parsers provide support for validation of XML documents to ensure that they
+ conform to a certain DTD or XML Schema. This feature can be useful for
+ configuration files, too. <code>XMLConfiguration</code> allows this feature
+ to be enabled when files are loaded.
+ </p>
+ <subsection name="Validation using a DTD">
+ <p>
+ The easiest way to turn on validation is to simply set the
+ <code>validating</code> property to true as shown in the
+ following example:
+ </p>
+ <source><![CDATA[
+XMLConfiguration config = new XMLConfiguration();
+config.setFileName("myconfig.xml");
+config.setValidating(true);
+
+// This will throw a ConfigurationException if the XML document does not
+// conform to its DTD.
+config.load();
+]]></source>
+ <p>
+ Setting the <code>validating</code> flag to true will cause
+ <code>XMLConfiguration</code> to use a validating XML parser. At this parser
+ a custom <code>ErrorHandler</code> will be registered, which throws
+ exceptions on simple and fatal parsing errors.
+ </p>
+ </subsection>
+ <subsection name="Validation using a Schema">
+ <p>
+ XML Parsers also provide support for validating XML documents using an
+ XML Schema. XMLConfiguration provides a simple mechanism for enabling
+ this by setting the <code>schemaValidation</code> flag to true. This
+ will also set the <code>validating</code> flag to true so both do not
+ need to be set. The XML Parser will then use the schema defined in the
+ XML document to validate it. Enabling schema validation will also
+ enable the parser's namespace support.
+ </p>
+ <p>
+ <source><![CDATA[
+XMLConfiguration config = new XMLConfiguration();
+config.setFileName("myconfig.xml");
+config.setSchemaValidation(true);
+
+// This will throw a ConfigurationException if the XML document does not
+// conform to its Schema.
+config.load();
+]]></source>
+ </p>
+ </subsection>
+ <subsection name="Default Entity Resolution">
+ <p>
+ There is also some support for dealing with DTD files. Often the
+ DTD of an XML document is stored locally so that it can be quickly
+ accessed. However the <code>DOCTYPE</code> declaration of the document
+ points to a location on the web as in the following example:
+ </p>
+ <source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!DOCTYPE web-app
+ PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
+ "http://java.sun.com/j2ee/dtds/web-app_2.2.dtd">
+]]></source>
+ <p>
+ When working with XML documents directly you would use an
+ <code>EntityResolver</code> in such a case. The task of such an
+ entity resolver is to point the XML parser to the location of the
+ file referred to by the declaration. So in our example the entity
+ resolver would load the DTD file from a local cache instead of
+ retrieving it from the internet.
+ </p>
+ <p>
+ <code>XMLConfiguration</code> provides a simple default implementation of
+ an <code>EntityResolver</code>. This implementation is initialized
+ by calling the <code>registerEntityId()</code> method with the
+ public IDs of the entities to be retrieved and their corresponding
+ local URLs. This method has to be called before the configuration
+ is loaded. To continue our example, consider that the DTD file for
+ our example document is stored on the class path. We can register it
+ at <code>XMLConfiguration</code> using the following code:
+ </p>
+ <source><![CDATA[
+XMLConfiguration config = new XMLConfiguration();
+// load the URL to the DTD file from class path
+URL dtdURL = getClass().getResource("web-app_2.2.dtd");
+// register it at the configuration
+config.registerEntityId("-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN",
+ dtdURL);
+config.setValidating(true); // enable validation
+config.setFileName("web.xml");
+config.load();
+]]></source>
+ <p>
+ This basically tells the XML configuration to use the specified
+ URL when it encounters the given public ID. Note that the call to
+ <code>registerEntityId()</code> has to be performed before the
+ configuration is loaded. So you cannot use one of the constructors
+ that directly load the configuration.
+ </p>
+ </subsection>
+ <subsection name="Enhanced Entity Resolution">
+ <p>
+ While the default entity resolver can be used under certain circumstances,
+ it does not work well when using the DefaultConfigurationBuilder.
+ Furthermore, in many circumstances the programmatic nature of
+ registering entities will tie the application tightly to the
+ XML content. In addition, because it only works with the public id it
+ cannot support XML documents using an XML Schema.
+ </p>
+ <p>
+ <a href="http://xml.apache.org/commons/components/resolver/resolver-article.html#s.whats.wrong">XML
+ Entity and URI Resolvers</a> describes using a set of catalog files to
+ resolve enitities. Commons Configuration provides support for
+ this Catalog Resolver through its own CatalogResolver class.
+ </p>
+ <source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<Employees xmlns="http://commons.apache.org/employee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://commons.apache.org/employee http://commons.apache.org/sample.xsd">
+ <Employee>
+ <SSN>555121211</SSN>
+ <Name>John Doe</Name>
+ <DateOfBirth>1975-05-15</DateOfBirth>
+ <EmployeeType>Exempt</EmployeeType>
+ <Salary>100000</Salary>
+ </Employee>
+</Employees>]]></source>
+ <p>
+ The XML sample above is an XML document using a default namespace of
+ http://commons.apache.org/employee. The schemaLocation allows a set
+ of namespaces and hints to the location of their corresponding
+ schemas. When processing the document the parser will pass the hint,
+ in this case http://commons.apache.org/sample.xsd, to the entity resolver
+ as the system id. More information on using schema locations can be found
+ at <a href="http://www.w3.org/TR/xmlschema-0/#schemaLocation">schemaLocation</a>.
+ </p>
+ <p>
+ The example that follows shows how to use the CatalogResolver when
+ processing an XMLConfiguration. It should be noted that by using the
+ setEntityResolver method any EntityResolver may be used, not just those
+ provided by Commons Configuration.
+ </p>
+ <source><![CDATA[
+CatalogResolver resolver = new CatalogResolver();
+resolver.setCatalogFiles("local/catalog.xml","http://test.org/catalogs/catalog1.xml");
+XMLConfiguration config = new XMLConfiguration();
+config.setEntityResolver(resolver);
+config.setSchemaValidation(true); // enable schema validation
+config.setFileName("config.xml");
+config.load();
+]]></source>
+ </subsection>
+ <subsection name="Extending Validation and Entity Resolution">
+ <p>
+ The mechanisms provided with Commons Configuration will hopefully be
+ sufficient in most cases, however there will certainly be circumstances
+ where they are not. XMLConfiguration provides two extension mechanisms
+ that should provide applications with all the flexibility they may
+ need. The first, registering a custom Entity Resolver has already been
+ discussed in the preceeding section. The second is that XMLConfiguration
+ provides a generic way of setting up the XML parser to use: A preconfigured
+ <code>DocumentBuilder</code> object can be passed to the
+ <code>setDocumentBuilder()</code> method.
+ </p>
+ <p>
+ So an application can create a <code>DocumentBuilder</code> object
+ and initialize it according to its special needs. Then this
+ object must be passed to the <code>XMLConfiguration</code> instance
+ before invocation of the <code>load()</code> method. When loading
+ a configuration file, the passed in <code>DocumentBuilder</code> will
+ be used instead of the default one. <em>Note:</em> If a custom
+ <code>DocumentBuilder</code> is used, the default implementation of
+ the <code>EntityResolver</code> interface is disabled. This means
+ that the <code>registerEntityId()</code> method has no effect in
+ this mode.
+ </p>
+ </subsection>
+ </section>
+</body>
+
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/userguide/overview.xml b/src/site/xdoc/userguide/overview.xml
new file mode 100644
index 0000000..a6e4d1e
--- /dev/null
+++ b/src/site/xdoc/userguide/overview.xml
@@ -0,0 +1,213 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<document>
+ <properties>
+ <title>Configuration Overview</title>
+ <author email="epugh at upstate.com">Eric Pugh</author>
+ <author email="smanux at lfjr.net">Emmanuel Bourg</author>
+ </properties>
+ <body>
+
+ <section name="Using Configuration">
+ <p>
+ Commons Configuration allows you to access configuration properties from
+ a variety of different sources. No matter if they are stored in a properties file,
+ a XML document, or a JNDI tree, they can all be accessed in the same way
+ through the generic <code><a href="../apidocs/org/apache/commons/configuration/Configuration.html">Configuration</a></code>
+ interface.
+ </p>
+ <p>
+ Another strength of Commons Configuration is its ability to mix configurations
+ from heterogeneous sources and treat them like a single logic configuration.
+ This section will introduce you to the different configurations
+ available and will show you how to combine them.
+ </p>
+
+ <subsection name="Configuration Sources">
+ <p>
+ Currently there are quite a number of different sources of Configuration objects. But,
+ by just using a Configuration object versus a specific type like XMLConfiguration or
+ JNDIConfiguration, you are sheltered from the mechanics of actually retrieving the
+ configuration values. These various sources include:
+ <ul>
+ <li>
+ <strong>PropertiesConfiguration</strong>
+ Loads configuration values from a properties file.
+ </li>
+ <li>
+ <strong>XMLConfiguration</strong>
+ Takes values from an XML document.
+ </li>
+ <li>
+ <strong>INIConfiguration</strong>
+ Loads the values from a .ini file as used by Windows.
+ </li>
+ <li>
+ <strong>PropertyListConfiguration</strong>
+ Loads values from an OpenStep .plist file. XMLPropertyListConfiguration is also
+ available to read the XML variant used by Mac OS X.
+ </li>
+ <li>
+ <strong>JNDIConfiguration</strong>
+ Using a key in the JNDI tree, can retrieve values as configuration properties.
+ </li>
+ <li>
+ <strong>BaseConfiguration</strong>
+ An in-memory method of populating a Configuration object.
+ </li>
+ <li>
+ <strong>HierarchicalConfiguration</strong>
+ An in-memory Configuration object that is able to deal with complex
+ structured data.
+ </li>
+ <li>
+ <strong>SystemConfiguration</strong>
+ A configuration using the system properties
+ </li>
+ <li>
+ <strong>ConfigurationConverter</strong>
+ Takes a java.util.Properties or an org.apache.commons.collections.ExtendedProperties
+ and converts it to a Configuration object.
+ </li>
+ </ul>
+
+ </p>
+ </subsection>
+
+ <subsection name="Mixing Configuration Sources">
+ <p>
+ Often you want to provide a base set of configuration values, but allow the user to easily
+ override them for their specific environment. Well one way is to hard code the default
+ values into your code, and have then provide a property file that overrides this. However,
+ this is a very rigid way of doing things. Instead, with the <code>CompositeConfiguration</code>
+ you can provide many different ways of setting up a configuration. You can either do it
+ manually:
+ </p>
+
+<source>
+CompositeConfiguration config = new CompositeConfiguration();
+config.addConfiguration(new SystemConfiguration());
+config.addConfiguration(new PropertiesConfiguration("application.properties"));
+</source>
+
+ <p>or via the <code>ConfigurationFactory</code> class:</p>
+
+<source>
+ConfigurationFactory factory = new ConfigurationFactory("config.xml");
+Configuration config = factory.getConfiguration();
+</source>
+
+ <p>
+ The <code>config.xml</code> file used in the example above is a configuration descriptor,
+ it specifies the Configuration objects to load. Here is an example of descriptor:
+ </p>
+
+<source><![CDATA[
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+
+<configuration>
+ <system/>
+ <properties fileName="application.properties"/>
+</configuration>
+]]></source>
+
+ <p>
+ What this says is that we are loading up all system properties, as well as the properties
+ file <code>application.properties</code>. The order of precedence is first to last. So in
+ the above example, if a property is not found in the system properties, it'll be searched
+ in the properties file. This allows you to set up default values in a properties file, and
+ override them with the system properties.
+ </p>
+ </subsection>
+
+ <subsection name="The Configuration interface">
+ <p>
+ All the classes in this package that represent different kinds of configuration
+ sources share a single interface:
+ <code><a href="../apidocs/org/apache/commons/configuration/Configuration.html">Configuration</a></code>.
+ This interface allows you to access and manipulate configuration properties
+ in a generic way.
+ </p>
+ <p>
+ A major part of the methods defined in the <code>Configuration</code>
+ interface deals with retrieving properties of different data types. All
+ these methods take a key as an argument that points to the desired
+ property. This is a string value whose exact meaning depends on the
+ concrete <code>Configuration</code> implementation used. They try to
+ find the property specified by the passed in key and convert it to their
+ target type; this converted value will be returned. There are also
+ overloaded variants of all methods that allow to specify a default value,
+ which will be returned if the property cannot be found. The following
+ data types are supported:
+ <ul>
+ <li>BigDecimal</li>
+ <li>BigInteger</li>
+ <li>boolean</li>
+ <li>byte</li>
+ <li>double</li>
+ <li>float</li>
+ <li>int</li>
+ <li>long</li>
+ <li>short</li>
+ <li>String</li>
+ </ul>
+ The names of these methods start with <code>get</code> followed by their
+ data type. The <code>getString()</code> method for instance will return
+ String values, <code>getInt()</code> will operate on integers.
+ </p>
+ <p>
+ Properties can have multiple values, so it is also possible to query a
+ list containing all of the available values. This is done using the
+ <code>getList()</code> method.
+ </p>
+ <p>
+ For manipulating properties or their values the following methods can
+ be used:
+ <dl>
+ <dt><code>addProperty()</code></dt>
+ <dd>Adds a new property to the configuration. If this property already
+ exists, another value is added to it (so it becomes a multi-valued
+ property).</dd>
+ <dt><code>clearProperty()</code></dt>
+ <dd>Removes the specified property from the configuration.</dd>
+ <dt><code>setProperty()</code></dt>
+ <dd>Overwrites the value of the specified property. This is the same
+ as removing the property and then calling <code>addProperty()</code>
+ with the new property value.</dd>
+ <dt><code>clear()</code></dt>
+ <dd>Wipes out the whole configuration</dd>
+ </dl>
+ </p>
+ </subsection>
+
+ <subsection name="Threading issues">
+ <p>
+ The most concrete implementations of the <code>Configuration</code>
+ interface that are shipped with this library are not thread-safe.
+ They can be accessed concurrently in a read-only manner. However if one
+ thread modifies a configuration object, manual synchronization has to be
+ performed to ensure correctness of data. Notes about the thread
+ safety of conrete implementation classes can be found in the Javadocs
+ for these classes.
+ </p>
+ </subsection>
+ </section>
+
+ </body>
+</document>
diff --git a/src/site/xdoc/userguide/user_guide.xml b/src/site/xdoc/userguide/user_guide.xml
new file mode 100644
index 0000000..811efbe
--- /dev/null
+++ b/src/site/xdoc/userguide/user_guide.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- $Id: user_guide.xml 1156502 2011-08-11 06:23:25Z oheger $ -->
+
+<document>
+
+ <properties>
+ <title>Commons Configuration User's Guide</title>
+ </properties>
+
+<body>
+
+ <section name="About this document">
+ <p>
+ This document describes the features of the Commons Configuration
+ component starting with the very basics and up to the more advanced
+ topics. If you read it in a linear way, you should get a sound
+ understanding of the provided classes and the possibilities they
+ offer. But you can also skip sections and jump directly to the topics
+ you are most interested in.
+ </p>
+ </section>
+
+ <section name="Table of contents">
+ <ul>
+ <li><a href="overview.html#Using_Configuration">Using Configuration</a></li>
+ <ul>
+ <li><a href="overview.html#Configuration_Sources">Configuration Sources</a></li>
+ <li><a href="overview.html#Mixing_Configuration_Sources">Mixing Configuration Sources</a></li>
+ <li><a href="overview.html#The_Configuration_interface">The Configuration interface</a></li>
+ <li><a href="overview.html#Threading_issues">Threading issues</a></li>
+ </ul>
+ <li><a href="howto_basicfeatures.html#Basic_features_and_AbstractConfiguration">Basic features and AbstractConfiguration</a></li>
+ <ul>
+ <li><a href="howto_basicfeatures.html#Handling_of_missing_properties">Handling of missing properties</a></li>
+ <li><a href="howto_basicfeatures.html#List_handling">List handling</a></li>
+ <li><a href="howto_basicfeatures.html#Variable_Interpolation">Variable Interpolation</a></li>
+ <li><a href="howto_basicfeatures.html#Customizing_interpolation">Customizing interpolation</a></li>
+ </ul>
+ <li><a href="howto_properties.html#Properties_files">Properties files</a></li>
+ <ul>
+ <li><a href="howto_properties.html#Using_PropertiesConfiguration">Using PropertiesConfiguration</a></li>
+ <li><a href="howto_properties.html#Includes">Includes</a></li>
+ <li><a href="howto_properties.html#Lists_and_arrays">Lists and arrays</a></li>
+ <li><a href="howto_properties.html#Saving">Saving</a></li>
+ <li><a href="howto_properties.html#Special_Characters_and_Escaping">Special Characters and Escaping</a></li>
+ <li><a href="howto_properties.html#Layout_Objects">Layout Objects</a></li>
+ <li><a href="howto_properties.html#Custom_properties_readers_and_writers">Custom properties readers and writers</a></li>
+ </ul>
+ <li><a href="howto_filebased.html#File-based_Configurations">File-based Configurations</a></li>
+ <ul>
+ <li><a href="howto_filebased.html#Specifying_the_file">Specifying the file</a></li>
+ <li><a href="howto_filebased.html#Loading">Loading</a></li>
+ <li><a href="howto_filebased.html#Saving">Saving</a></li>
+ <li><a href="howto_filebased.html#Automatic_Saving">Automatic Saving</a></li>
+ <li><a href="howto_filebased.html#Automatic_Reloading">Automatic Reloading</a></li>
+ </ul>
+ <li><a href="howto_xml.html#Hierarchical_properties">Hierarchical properties</a></li>
+ <ul>
+ <li><a href="howto_xml.html#Accessing_properties_in_hierarchical_configurations">Accessing properties in hierarchical configurations</a></li>
+ <li><a href="howto_xml.html#Complex_hierarchical_structures">Complex hierarchical structures</a></li>
+ <li><a href="howto_xml.html#Accessing_structured_properties">Accessing structured properties</a></li>
+ <li><a href="howto_xml.html#Adding_new_properties">Adding new properties</a></li>
+ <li><a href="howto_xml.html#Escaping_special_characters">Escaping dot characters in property names</a></li>
+ <li><a href="howto_xml.html#Expression_engines">Expression engines</a></li>
+ <ul>
+ <li><a href="howto_xml.html#The_default_expression_engine">The default expression engine</a></li>
+ <li><a href="howto_xml.html#The_XPATH_expression_engine">The XPATH expression engine</a></li>
+ </ul>
+ <li><a href="howto_xml.html#Validation_of_XML_configuration_files">Validation of XML configuration files</a></li>
+ </ul>
+ <li><a href="howto_compositeconfiguration.html#Composite_Configuration_Details">Composite Configuration Details</a></li>
+ <ul>
+ <li><a href="howto_compositeconfiguration.html#Setting_Up_Defaults">Setting Up Defaults</a></li>
+ <li><a href="howto_compositeconfiguration.html#Saving_Changes">Saving Changes</a></li>
+ </ul>
+ <li><a href="howto_combinedconfiguration.html#Combined_Configuration">Combined Configuration</a></li>
+ <ul>
+ <li><a href="howto_combinedconfiguration.html#How_it_works">How it works</a></li>
+ <li><a href="howto_combinedconfiguration.html#Node_combiners">Node combiners</a></li>
+ <li><a href="howto_combinedconfiguration.html#Constructing_a_CombinedConfiguration">Constructing a CombinedConfiguration</a></li>
+ <li><a href="howto_combinedconfiguration.html#Dealing_with_changes">Dealing with changes</a></li>
+ </ul>
+ <li><a href="howto_beans.html#Declaring_and_Creating_Beans">Declaring and Creating Beans</a></li>
+ <ul>
+ <li><a href="howto_beans.html#Basic_Concepts">Basic Concepts</a></li>
+ <li><a href="howto_beans.html#An_Example">An Example</a></li>
+ <li><a href="howto_beans.html#Extending_the_Basic_Mechanism">Extending the Basic Mechanism</a></li>
+ </ul>
+ <li><a href="howto_configurationbuilder.html#Using_DefaultConfigurationBuilder">Using DefaultConfigurationBuilder</a></li>
+ <ul>
+ <li><a href="howto_configurationbuilder.html#The_configuration_definition_file">The configuration definition file</a></li>
+ <li><a href="howto_configurationbuilder.html#Setting_up_a_DefaultConfigurationBuilder">Setting up a ConfigurationFactory</a></li>
+ <li><a href="howto_configurationbuilder.html#Overriding_properties">Overriding properties</a></li>
+ <li><a href="howto_configurationbuilder.html#Optional_configuration_sources">Optional configuration sources</a></li>
+ <li><a href="howto_configurationbuilder.html#Union_configuration">Union configuration</a></li>
+ <li><a href="howto_configurationbuilder.html#Configuration_definition_file_reference">Configuration definition file reference</a></li>
+ <li><a href="howto_configurationbuilder.html#An_example">An example</a></li>
+ <li><a href="howto_configurationbuilder.html#Extending_the_configuration_definition_file_format">Extending the configuration definition file format</a></li>
+ </ul>
+ <li><a href="howto_multitenant.html#Multi-tenant Configurations">Multi-tenant Configurations</a></li>
+ <ul>
+ <li><a href="howto_multitenant.html#MultiFileHierarchicalConfiguration">MultiFileHierarchicalConfiguration</a></li>
+ <li><a href="howto_multitenant.html#DynamicCombinedConfiguration">DynamicCombinedConfiguration</a></li>
+ <li><a href="howto_multitenant.html#Sample Configuration">Sample Configuration</a></li>
+ <li><a href="howto_multitenant.html#PatternSubtreeConfigurationWrapper">PatternSubtreeConfigurationWrapper</a></li>
+ </ul>
+ <li><a href="howto_events.html#Configuration_Events">Configuration Events</a></li>
+ <ul>
+ <li><a href="howto_events.html#Configuration_listeners">Configuration listeners</a></li>
+ <li><a href="howto_events.html#An_example">An example</a></li>
+ <li><a href="howto_events.html#Error_listeners">Error listeners</a></li>
+ </ul>
+ <li><a href="howto_utilities.html#Utility_classes_and_Tips_and_Tricks">Utility classes and Tips and Tricks</a></li>
+ <ul>
+ <li><a href="howto_utilities.html#Copy_a_configuration">Copy a configuration</a></li>
+ <li><a href="howto_utilities.html#Converting_a_flat_configuration_into_a_hierarchical_one">Converting a flat configuration into a hierarchical one</a></li>
+ <li><a href="howto_utilities.html#Converting_between_properties_and_configurations">Converting between properties and configurations</a></li>
+ <li><a href="howto_utilities.html#Interpolation_of_all_variables">Interpolation of all variables</a></li>
+ <li><a href="howto_utilities.html#Handling_of_runtime_exceptions">Handling of runtime exceptions</a></li>
+ </ul>
+ <li><a href="howto_filesystems.html#File_Systems">File Systems</a></li>
+ <ul>
+ <li><a href="howto_filesystems.html#File_Systems#Configuration">Configuration</a></li>
+ <li><a href="howto_filesystems.html#File_Systems#File_Options_Provider">File Options Provider</a></li>
+ <li><a href="howto_filesystems.html#File_Systems#File_Reloading_Strategy">File Reloading Strategy</a></li>
+ </ul>
+ </ul>
+ </section>
+
+</body>
+
+</document>
\ No newline at end of file
diff --git a/src/test/java/org/apache/commons/configuration/BaseNonStringProperties.java b/src/test/java/org/apache/commons/configuration/BaseNonStringProperties.java
new file mode 100644
index 0000000..522cee8
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/BaseNonStringProperties.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/**
+ * Test if non-string properties are handled correctly.
+ *
+ * @version $Id: BaseNonStringProperties.java 1302002 2012-03-17 20:43:46Z sebb $
+ */
+public abstract class BaseNonStringProperties
+{
+
+ protected NonStringTestHolder nonStringTestHolder = new NonStringTestHolder();
+
+ protected Configuration conf;
+
+ @Test
+ public void testBoolean() throws Exception
+ {
+ nonStringTestHolder.testBoolean();
+ }
+
+ @Test
+ public void testBooleanDefaultValue() throws Exception
+ {
+ nonStringTestHolder.testBooleanDefaultValue();
+ }
+
+ @Test
+ public void testBooleanArrayValue() throws Exception
+ {
+ boolean booleanValue = conf.getBoolean("test.boolean");
+ assertTrue(booleanValue);
+ assertEquals(2, conf.getList("test.boolean.array").size());
+ }
+
+ @Test
+ public void testByte() throws Exception
+ {
+ nonStringTestHolder.testByte();
+ }
+
+ @Test
+ public void testByteArrayValue() throws Exception
+ {
+ byte testValue = 10;
+ byte byteValue = conf.getByte("test.byte");
+ assertEquals(testValue, byteValue);
+ assertEquals(2, conf.getList("test.byte.array").size());
+ }
+
+ @Test
+ public void testDouble() throws Exception
+ {
+ nonStringTestHolder.testDouble();
+ }
+
+ @Test
+ public void testDoubleDefaultValue() throws Exception
+ {
+ nonStringTestHolder.testDoubleDefaultValue();
+ }
+
+ @Test
+ public void testDoubleArrayValue() throws Exception
+ {
+ double testValue = 10.25;
+ double doubleValue = conf.getDouble("test.double");
+ assertEquals(testValue, doubleValue, 0.01);
+ assertEquals(2, conf.getList("test.double.array").size());
+ }
+
+ @Test
+ public void testFloat() throws Exception
+ {
+ nonStringTestHolder.testFloat();
+ }
+
+ @Test
+ public void testFloatDefaultValue() throws Exception
+ {
+ nonStringTestHolder.testFloatDefaultValue();
+
+ }
+
+ @Test
+ public void testFloatArrayValue() throws Exception
+ {
+ float testValue = (float) 20.25;
+ float floatValue = conf.getFloat("test.float");
+ assertEquals(testValue, floatValue, 0.01);
+ assertEquals(2, conf.getList("test.float.array").size());
+ }
+
+ @Test
+ public void testInteger() throws Exception
+ {
+ nonStringTestHolder.testInteger();
+ }
+
+ @Test
+ public void testIntegerDefaultValue() throws Exception
+ {
+ nonStringTestHolder.testIntegerDefaultValue();
+ }
+
+ @Test
+ public void testIntegerArrayValue() throws Exception
+ {
+ int intValue = conf.getInt("test.integer");
+ assertEquals(10, intValue);
+ assertEquals(2, conf.getList("test.integer.array").size());
+ }
+
+ @Test
+ public void testLong() throws Exception
+ {
+ nonStringTestHolder.testLong();
+ }
+
+ @Test
+ public void testLongDefaultValue() throws Exception
+ {
+ nonStringTestHolder.testLongDefaultValue();
+ }
+
+ @Test
+ public void testLongArrayValue() throws Exception
+ {
+ long longValue = conf.getLong("test.long");
+ assertEquals(1000000, longValue);
+ assertEquals(2, conf.getList("test.long.array").size());
+ }
+
+ @Test
+ public void testShort() throws Exception
+ {
+ nonStringTestHolder.testShort();
+ }
+
+ @Test
+ public void testShortDefaultValue() throws Exception
+ {
+ nonStringTestHolder.testShortDefaultValue();
+ }
+
+ @Test
+ public void testShortArrayValue() throws Exception
+ {
+ short shortValue = conf.getShort("test.short");
+ assertEquals(1, shortValue);
+ assertEquals(2, conf.getList("test.short.array").size());
+ }
+
+ @Test
+ public void testListMissing() throws Exception
+ {
+ nonStringTestHolder.testListMissing();
+ }
+
+ @Test
+ public void testSubset() throws Exception
+ {
+ nonStringTestHolder.testSubset();
+ }
+
+ @Test
+ public void testIsEmpty() throws Exception
+ {
+ nonStringTestHolder.testIsEmpty();
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/ConfigurationAssert.java b/src/test/java/org/apache/commons/configuration/ConfigurationAssert.java
new file mode 100644
index 0000000..d15a6b5
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/ConfigurationAssert.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Iterator;
+
+import junit.framework.Assert;
+
+/**
+ * Assertions on configurations for the unit tests. This class also provides
+ * access to test files.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: ConfigurationAssert.java 1221893 2011-12-21 21:34:38Z oheger $
+ */
+public class ConfigurationAssert
+{
+ /** Constant for the name of the directory with the test files. */
+ public static final String TEST_DIR_NAME = "target/test-classes";
+
+ /** Constant for the name of the directory with the output files. */
+ public static final String OUT_DIR_NAME = "target";
+
+ /** The directory with the test files. */
+ public static final File TEST_DIR = new File(TEST_DIR_NAME);
+
+ /** The directory with the output files. */
+ public static final File OUT_DIR = new File(OUT_DIR_NAME);
+
+ /**
+ * Checks the content of a configuration.
+ *
+ * @param expected the expected properties
+ * @param actual the configuration to check
+ */
+ public static void assertEquals(Configuration expected, Configuration actual)
+ {
+ // check that the actual configuration contains all the properties of the expected configuration
+ for (Iterator<String> it = expected.getKeys(); it.hasNext();)
+ {
+ String key = it.next();
+ Assert.assertTrue("The actual configuration doesn't contain the expected key '" + key + "'", actual.containsKey(key));
+ Assert.assertEquals("Value of the '" + key + "' property", expected.getProperty(key), actual.getProperty(key));
+ }
+
+ // check that the actual configuration has no extra properties
+ for (Iterator<String> it = actual.getKeys(); it.hasNext();)
+ {
+ String key = it.next();
+ Assert.assertTrue("The actual configuration contains an extra key '" + key + "'", expected.containsKey(key));
+ }
+ }
+
+ /**
+ * Returns a {@code File} object for the specified test file.
+ *
+ * @param name the name of the test file
+ * @return a {@code File} object pointing to that test file
+ */
+ public static File getTestFile(String name)
+ {
+ return new File(TEST_DIR, name);
+ }
+
+ /**
+ * Returns a {@code File} object for the specified out file.
+ *
+ * @param name the name of the out file
+ * @return a {@code File} object pointing to that out file
+ */
+ public static File getOutFile(String name)
+ {
+ return new File(OUT_DIR, name);
+ }
+
+ /**
+ * Returns a URL pointing to the specified test file. If the URL cannot be
+ * constructed, a runtime exception is thrown.
+ *
+ * @param name the name of the test file
+ * @return the corresponding URL
+ */
+ public static URL getTestURL(String name)
+ {
+ return urlFromFile(getTestFile(name));
+ }
+
+ /**
+ * Returns a URL pointing to the specified output file. If the URL cannot be
+ * constructed, a runtime exception is thrown.
+ *
+ * @param name the name of the output file
+ * @return the corresponding URL
+ */
+ public static URL getOutURL(String name)
+ {
+ return urlFromFile(getOutFile(name));
+ }
+
+ /**
+ * Helper method for converting a file to a URL.
+ *
+ * @param file the file
+ * @return the corresponding URL
+ * @throws ConfigurationRuntimeException if the URL cannot be constructed
+ */
+ private static URL urlFromFile(File file)
+ {
+ try
+ {
+ return file.toURI().toURL();
+ }
+ catch (MalformedURLException mex)
+ {
+ throw new ConfigurationRuntimeException(mex);
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/ConfigurationErrorListenerImpl.java b/src/test/java/org/apache/commons/configuration/ConfigurationErrorListenerImpl.java
new file mode 100644
index 0000000..588779e
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/ConfigurationErrorListenerImpl.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.commons.configuration.event.ConfigurationErrorEvent;
+import org.apache.commons.configuration.event.ConfigurationErrorListener;
+
+/**
+ * An implementation of the {@code ConfigurationErrorListener} interface
+ * that can be used in unit tests. This implementation just records received
+ * events and allows to test whether expected errors occurred.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationErrorListenerImpl.java 1222446 2011-12-22 20:57:32Z oheger $
+ */
+public class ConfigurationErrorListenerImpl implements
+ ConfigurationErrorListener
+{
+ /** Stores the last received error event. */
+ private ConfigurationErrorEvent event;
+
+ /** Stores the number of calls to configurationError(). */
+ private int errorCount;
+
+ /**
+ * An error event is received. Updates the internal counter and stores the
+ * event.
+ *
+ * @param event the error event
+ */
+ public void configurationError(ConfigurationErrorEvent event)
+ {
+ this.event = event;
+ errorCount++;
+ }
+
+ /**
+ * Returns the last received error event.
+ *
+ * @return the last error event (may be <b>null</b>)
+ */
+ public ConfigurationErrorEvent getLastEvent()
+ {
+ return event;
+ }
+
+ /**
+ * Returns the number of received error events.
+ *
+ * @return the number of error events
+ */
+ public int getErrorCount()
+ {
+ return errorCount;
+ }
+
+ /**
+ * Checks whether no error event was received.
+ */
+ public void verify()
+ {
+ assertEquals("Error events received", 0, errorCount);
+ }
+
+ /**
+ * Checks whether an expected error event was received. This is a
+ * convenience method for checking whether exactly one event of a certain
+ * type was received.
+ *
+ * @param type the type of the event
+ * @param propName the name of the property
+ * @param propValue the value of the property
+ */
+ public void verify(int type, String propName, Object propValue)
+ {
+ assertEquals("Wrong number of error events", 1, errorCount);
+ assertEquals("Wrong event type", type, event.getType());
+ assertTrue("Wrong property name", (propName == null) ? event
+ .getPropertyName() == null : propName.equals(event
+ .getPropertyName()));
+ assertTrue("Wrong property value", (propValue == null) ? event
+ .getPropertyValue() == null : propValue.equals(event
+ .getPropertyValue()));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/DatabaseConfigurationTestHelper.java b/src/test/java/org/apache/commons/configuration/DatabaseConfigurationTestHelper.java
new file mode 100644
index 0000000..dc8c820
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/DatabaseConfigurationTestHelper.java
@@ -0,0 +1,230 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.sql.Connection;
+
+import javax.sql.DataSource;
+
+import org.apache.commons.configuration.test.HsqlDB;
+import org.apache.commons.dbcp.BasicDataSource;
+import org.dbunit.database.DatabaseConnection;
+import org.dbunit.database.IDatabaseConnection;
+import org.dbunit.dataset.IDataSet;
+import org.dbunit.dataset.xml.XmlDataSet;
+import org.dbunit.operation.DatabaseOperation;
+
+/**
+ * A helper class for performing tests for {@link DatabaseConfiguration}. This
+ * class maintains an in-process database that stores configuration data and can
+ * be accessed from a {@link DatabaseConfiguration} instance. Constants for
+ * table and column names and database connection settings are provided, too.
+ *
+ * @version $Id: DatabaseConfigurationTestHelper.java 1222447 2011-12-22 20:59:44Z oheger $
+ */
+public class DatabaseConfigurationTestHelper
+{
+ /** Constant for the JDBC driver class. */
+ public final String DATABASE_DRIVER = "org.hsqldb.jdbcDriver";
+
+ /** Constant for the connection URL. */
+ public final String DATABASE_URL = "jdbc:hsqldb:mem:testdb";
+
+ /** Constant for the DB user name. */
+ public final String DATABASE_USERNAME = "sa";
+
+ /** Constant for the DB password. */
+ public final String DATABASE_PASSWORD = "";
+
+ /** Constant for the configuration table. */
+ public static final String TABLE = "configuration";
+
+ /** Constant for the multi configuration table. */
+ public static final String TABLE_MULTI = "configurations";
+
+ /** Constant for the column with the keys. */
+ public static final String COL_KEY = "key";
+
+ /** Constant for the column with the values. */
+ public static final String COL_VALUE = "value";
+
+ /** Constant for the column with the configuration name. */
+ public static final String COL_NAME = "name";
+
+ /** Constant for the name of the test configuration. */
+ public static final String CONFIG_NAME = "test";
+
+ /** Stores the in-process database. */
+ private HsqlDB hsqlDB;
+
+ /** The data source. */
+ private DataSource datasource;
+
+ /**
+ * The auto-commit mode for the connections created by the managed data
+ * source.
+ */
+ private boolean autoCommit = true;
+
+ /**
+ * Returns the auto-commit mode of the connections created by the managed
+ * data source.
+ *
+ * @return the auto-commit mode
+ */
+ public boolean isAutoCommit()
+ {
+ return autoCommit;
+ }
+
+ /**
+ * Sets the auto-commit mode of the connections created by the managed data
+ * source.
+ *
+ * @param autoCommit the auto-commit mode
+ */
+ public void setAutoCommit(boolean autoCommit)
+ {
+ this.autoCommit = autoCommit;
+ }
+
+ /**
+ * Initializes this helper object. This method can be called from a
+ * {@code setUp()} method of a unit test class. It creates the database
+ * instance if necessary.
+ *
+ * @throws Exception if an error occurs
+ */
+ public void setUp() throws Exception
+ {
+ File script = ConfigurationAssert.getTestFile("testdb.script");
+ hsqlDB = new HsqlDB(DATABASE_URL, DATABASE_DRIVER, script.getAbsolutePath());
+ }
+
+ /**
+ * Frees the resources used by this helper class. This method can be called
+ * by a {@code tearDown()} method of a unit test class.
+ *
+ * @throws Exception if an error occurs
+ */
+ public void tearDown() throws Exception
+ {
+ if (datasource != null)
+ {
+ datasource.getConnection().close();
+ }
+ hsqlDB.close();
+ }
+
+ /**
+ * Creates a database configuration with default values.
+ *
+ * @return the configuration
+ */
+ public DatabaseConfiguration setUpConfig()
+ {
+ return new DatabaseConfiguration(getDatasource(), TABLE, COL_KEY,
+ COL_VALUE, !isAutoCommit());
+ }
+
+ /**
+ * Creates a database configuration that supports multiple configurations in
+ * a table with default values.
+ *
+ * @return the configuration
+ */
+ public DatabaseConfiguration setUpMultiConfig()
+ {
+ return setUpMultiConfig(CONFIG_NAME);
+ }
+
+ /**
+ * Creates a database configuration that supports multiple configurations in
+ * a table and sets the specified configuration name.
+ *
+ * @param configName the name of the configuration
+ * @return the configuration
+ */
+ public DatabaseConfiguration setUpMultiConfig(String configName)
+ {
+ return new DatabaseConfiguration(getDatasource(), TABLE_MULTI,
+ COL_NAME, COL_KEY, COL_VALUE, configName, !isAutoCommit());
+ }
+
+ /**
+ * Returns the {@code DataSource} managed by this class. The data
+ * source is created on first access.
+ *
+ * @return the {@code DataSource}
+ */
+ public DataSource getDatasource()
+ {
+ if (datasource == null)
+ {
+ try
+ {
+ datasource = setUpDataSource();
+ }
+ catch (Exception ex)
+ {
+ throw new ConfigurationRuntimeException(
+ "Could not create data source", ex);
+ }
+ }
+ return datasource;
+ }
+
+ /**
+ * Creates the internal data source. This method also initializes the
+ * database.
+ *
+ * @return the data source
+ * @throws Exception if an error occurs
+ */
+ private DataSource setUpDataSource() throws Exception
+ {
+ BasicDataSource ds = new BasicDataSource();
+ ds.setDriverClassName(DATABASE_DRIVER);
+ ds.setUrl(DATABASE_URL);
+ ds.setUsername(DATABASE_USERNAME);
+ ds.setPassword(DATABASE_PASSWORD);
+ ds.setDefaultAutoCommit(isAutoCommit());
+
+ // prepare the database
+ Connection conn = ds.getConnection();
+ IDatabaseConnection connection = new DatabaseConnection(conn);
+ IDataSet dataSet = new XmlDataSet(new FileInputStream(
+ ConfigurationAssert.getTestFile("dataset.xml")));
+
+ try
+ {
+ DatabaseOperation.CLEAN_INSERT.execute(connection, dataSet);
+ }
+ finally
+ {
+ if (!isAutoCommit())
+ {
+ conn.commit();
+ }
+ connection.close();
+ }
+
+ return ds;
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/FileURLStreamHandler.java b/src/test/java/org/apache/commons/configuration/FileURLStreamHandler.java
new file mode 100644
index 0000000..cbb36e5
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/FileURLStreamHandler.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+/**
+ * A custom URLStreamHandler to test loading and saving configurations to non
+ * standard URLs. This handler acts like a file handler with write support.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: FileURLStreamHandler.java 1222450 2011-12-22 21:01:33Z oheger $
+ */
+public class FileURLStreamHandler extends URLStreamHandler
+{
+ @Override
+ protected URLConnection openConnection(URL u) throws IOException
+ {
+ final File file = new File(u.getFile());
+
+ return new URLConnection(u) {
+
+ @Override
+ public void connect() throws IOException
+ {
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException
+ {
+ return new FileInputStream(file);
+ }
+
+ @Override
+ public OutputStream getOutputStream() throws IOException
+ {
+ return new FileOutputStream(file);
+ }
+ };
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/InterpolationTestHelper.java b/src/test/java/org/apache/commons/configuration/InterpolationTestHelper.java
new file mode 100644
index 0000000..1b87ef9
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/InterpolationTestHelper.java
@@ -0,0 +1,257 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.awt.event.KeyEvent;
+import java.util.List;
+
+import org.apache.commons.configuration.interpol.ConfigurationInterpolator;
+import org.apache.commons.lang.text.StrLookup;
+
+/**
+ * A helper class that defines a bunch of tests related to variable
+ * interpolation. It can be used for running these tests on different
+ * configuration implementations.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: InterpolationTestHelper.java 1222452 2011-12-22 21:06:17Z oheger $
+ */
+public class InterpolationTestHelper
+{
+ /**
+ * Tests basic interpolation facilities of the specified configuration.
+ *
+ * @param config the configuration to test
+ */
+ public static void testInterpolation(Configuration config)
+ {
+ config.setProperty("applicationRoot", "/home/applicationRoot");
+ config.setProperty("db", "${applicationRoot}/db/hypersonic");
+ String unInterpolatedValue = "${applicationRoot2}/db/hypersonic";
+ config.setProperty("dbFailedInterpolate", unInterpolatedValue);
+ String dbProp = "/home/applicationRoot/db/hypersonic";
+
+ assertEquals("Checking interpolated variable", dbProp, config
+ .getString("db"));
+ assertEquals("lookup fails, leave variable as is", config
+ .getString("dbFailedInterpolate"), unInterpolatedValue);
+
+ config.setProperty("arrayInt", "${applicationRoot}/1");
+ String[] arrayInt = config.getStringArray("arrayInt");
+ assertEquals("check first entry was interpolated",
+ "/home/applicationRoot/1", arrayInt[0]);
+
+ config.addProperty("path", "/temp,C:\\Temp,/usr/local/tmp");
+ config.setProperty("path.current", "${path}");
+ assertEquals("Interpolation with multi-valued property",
+ "/temp", config.getString("path.current"));
+ }
+
+ /**
+ * Tests an interpolation over multiple levels (i.e. the replacement of a
+ * variable is another variable and so on).
+ *
+ * @param config the configuration to test
+ */
+ public static void testMultipleInterpolation(Configuration config)
+ {
+ config.setProperty("test.base-level", "/base-level");
+ config
+ .setProperty("test.first-level",
+ "${test.base-level}/first-level");
+ config.setProperty("test.second-level",
+ "${test.first-level}/second-level");
+ config.setProperty("test.third-level",
+ "${test.second-level}/third-level");
+
+ String expectedValue = "/base-level/first-level/second-level/third-level";
+
+ assertEquals(config.getString("test.third-level"),
+ expectedValue);
+ }
+
+ /**
+ * Tests an invalid interpolation that results in an infinite loop. This
+ * loop should be detected and an exception should be thrown.
+ *
+ * @param config the configuration to test
+ */
+ public static void testInterpolationLoop(Configuration config)
+ {
+ config.setProperty("test.a", "${test.b}");
+ config.setProperty("test.b", "${test.a}");
+
+ try
+ {
+ config.getString("test.a");
+ fail("IllegalStateException should have been thrown for looped property references");
+ }
+ catch (IllegalStateException e)
+ {
+ // ok
+ }
+
+ }
+
+ /**
+ * Tests interpolation when a subset configuration is involved.
+ *
+ * @param config the configuration to test
+ */
+ public static void testInterpolationSubset(Configuration config)
+ {
+ config.addProperty("test.a", new Integer(42));
+ config.addProperty("test.b", "${test.a}");
+ assertEquals("Wrong interpolated value", 42, config
+ .getInt("test.b"));
+ Configuration subset = config.subset("test");
+ assertEquals("Wrong string property", "42", subset
+ .getString("b"));
+ assertEquals("Wrong int property", 42, subset.getInt("b"));
+ }
+
+ /**
+ * Tests interpolation when the referred property is not found.
+ *
+ * @param config the configuration to test
+ */
+ public static void testInterpolationUnknownProperty(Configuration config)
+ {
+ config.addProperty("test.interpol", "${unknown.property}");
+ assertEquals("Wrong interpolated unknown property",
+ "${unknown.property}", config.getString("test.interpol"));
+ }
+
+ /**
+ * Tests interpolation of system properties.
+ *
+ * @param config the configuration to test
+ */
+ public static void testInterpolationSystemProperties(Configuration config)
+ {
+ String[] sysProperties =
+ { "java.version", "java.vendor", "os.name", "java.class.path" };
+ for (int i = 0; i < sysProperties.length; i++)
+ {
+ config.addProperty("prop" + i, "${sys:" + sysProperties[i] + "}");
+ }
+
+ for (int i = 0; i < sysProperties.length; i++)
+ {
+ assertEquals("Wrong value for system property "
+ + sysProperties[i], System.getProperty(sysProperties[i]),
+ config.getString("prop" + i));
+ }
+ }
+
+ /**
+ * Tests interpolation of constant values.
+ *
+ * @param config the configuration to test
+ */
+ public static void testInterpolationConstants(Configuration config)
+ {
+ config.addProperty("key.code",
+ "${const:java.awt.event.KeyEvent.VK_CANCEL}");
+ assertEquals("Wrong value of constant variable",
+ KeyEvent.VK_CANCEL, config.getInt("key.code"));
+ assertEquals("Wrong value when fetching constant from cache",
+ KeyEvent.VK_CANCEL, config.getInt("key.code"));
+ }
+
+ /**
+ * Tests whether a variable can be escaped, so that it won't be
+ * interpolated.
+ *
+ * @param config the configuration to test
+ */
+ public static void testInterpolationEscaped(Configuration config)
+ {
+ config.addProperty("var", "x");
+ config.addProperty("escVar", "Use the variable $${${var}}.");
+ assertEquals("Wrong escaped variable", "Use the variable ${x}.",
+ config.getString("escVar"));
+ }
+
+ /**
+ * Tests accessing and manipulating the interpolator object.
+ *
+ * @param config the configuration to test
+ */
+ public static void testGetInterpolator(AbstractConfiguration config)
+ {
+ config.addProperty("var", "${echo:testVar}");
+ ConfigurationInterpolator interpol = config.getInterpolator();
+ interpol.registerLookup("echo", new StrLookup()
+ {
+ @Override
+ public String lookup(String varName)
+ {
+ return "Value of variable " + varName;
+ }
+ });
+ assertEquals("Wrong value of echo variable",
+ "Value of variable testVar", config.getString("var"));
+ }
+
+ /**
+ * Tests obtaining a configuration with all variables replaced by their
+ * actual values.
+ *
+ * @param config the configuration to test
+ * @return the interpolated configuration
+ */
+ public static Configuration testInterpolatedConfiguration(
+ AbstractConfiguration config)
+ {
+ config.setProperty("applicationRoot", "/home/applicationRoot");
+ config.setProperty("db", "${applicationRoot}/db/hypersonic");
+ config.setProperty("inttest.interpol", "${unknown.property}");
+ config.setProperty("intkey.code",
+ "${const:java.awt.event.KeyEvent.VK_CANCEL}");
+ config.setProperty("inttest.sysprop", "${sys:java.version}");
+ config.setProperty("inttest.numvalue", "3\\,1415");
+ config.setProperty("inttest.value", "${inttest.numvalue}");
+ config.setProperty("inttest.list", "${db}");
+ config.addProperty("inttest.list", "${inttest.value}");
+
+ Configuration c = config.interpolatedConfiguration();
+ assertEquals("Property not replaced",
+ "/home/applicationRoot/db/hypersonic", c.getProperty("db"));
+ assertEquals("Const variable not replaced", KeyEvent.VK_CANCEL,
+ c.getInt("intkey.code"));
+ assertEquals("Sys property not replaced", System
+ .getProperty("java.version"), c.getProperty("inttest.sysprop"));
+ assertEquals("Delimiter value not replaced", "3,1415", c
+ .getProperty("inttest.value"));
+ List<?> lst = (List<?>) c.getProperty("inttest.list");
+ assertEquals("Wrong number of list elements", 2, lst.size());
+ assertEquals("List element 0 not replaced",
+ "/home/applicationRoot/db/hypersonic", lst.get(0));
+ assertEquals("List element 1 not replaced", "3,1415", lst
+ .get(1));
+ assertEquals("Unresolvable variable not found",
+ "${unknown.property}", c.getProperty("inttest.interpol"));
+
+ return c;
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/Logging.java b/src/test/java/org/apache/commons/configuration/Logging.java
new file mode 100644
index 0000000..22f853d
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/Logging.java
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import org.apache.commons.logging.impl.Log4JLogger;
+import org.apache.log4j.Priority;
+import org.apache.log4j.Level;
+import org.apache.log4j.Appender;
+import org.apache.log4j.PatternLayout;
+import org.apache.log4j.ConsoleAppender;
+
+/**
+ * Configures logging for tests.
+ *
+ * When running with Maven do -Dmaven.surefire.debug="LogLevel=level" to set the
+ * Log Level to the desired value.
+ */
+public class Logging extends Log4JLogger
+{
+ /**
+ * The fully qualified name of the Log4JLogger class.
+ */
+ private static final String FQCN = Logging.class.getName();
+
+ private static Priority traceLevel;
+
+ static
+ {
+ // Releases of log4j1.2 >= 1.2.12 have Priority.TRACE available, earlier
+ // versions do not. If TRACE is not available, then we have to map
+ // calls to Log.trace(...) onto the DEBUG level.
+
+ try
+ {
+ traceLevel = (Priority) Level.class.getDeclaredField("TRACE").get(null);
+ }
+ catch (Exception ex)
+ {
+ // ok, trace not available
+ traceLevel = Priority.DEBUG;
+ }
+
+ String level = System.getProperty("LogLevel");
+ if (level != null)
+ {
+ org.apache.log4j.Logger log = org.apache.log4j.Logger.getRootLogger();
+ log.setLevel(Level.toLevel(level));
+ Appender appender = new ConsoleAppender(new PatternLayout("%p %l - %m%n"), ConsoleAppender.SYSTEM_OUT);
+ log.addAppender(appender);
+ }
+ }
+
+ public Logging()
+ {
+ super();
+ }
+
+
+ /**
+ * Base constructor.
+ */
+ public Logging(String name)
+ {
+ super(name);
+ }
+
+ /**
+ * For use with a log4j factory.
+ */
+ public Logging(org.apache.log4j.Logger logger)
+ {
+ super(logger);
+ }
+
+ // ---------------------------------------------------------
+ // Implementation
+ //
+ // Note that in the methods below the Priority class is used to define
+ // levels even though the Level class is supported in 1.2. This is done
+ // so that at compile time the call definitely resolves to a call to
+ // a method that takes a Priority rather than one that takes a Level.
+ //
+ // The Category class (and hence its subclass Logging) in version 1.2 only
+ // has methods that take Priority objects. The Category class (and hence
+ // Logging class) in version 1.3 has methods that take both Priority and
+ // Level objects. This means that if we use Level here, and compile
+ // against log4j 1.3 then calls would be bound to the versions of
+ // methods taking Level objects and then would fail to run against
+ // version 1.2 of log4j.
+ // ---------------------------------------------------------
+
+
+ /**
+ * Logs a message with <code>org.apache.log4j.Priority.TRACE</code>.
+ * When using a log4j version that does not support the <code>TRACE</code>
+ * level, the message will be logged at the <code>DEBUG</code> level.
+ *
+ * @param message to log
+ * @see org.apache.commons.logging.Log#trace(Object)
+ */
+ @Override
+ public void trace(Object message)
+ {
+ getLogger().log(FQCN, traceLevel, message, null);
+ }
+
+
+ /**
+ * Logs a message with <code>org.apache.log4j.Priority.TRACE</code>.
+ * When using a log4j version that does not support the <code>TRACE</code>
+ * level, the message will be logged at the <code>DEBUG</code> level.
+ *
+ * @param message to log
+ * @param t log this cause
+ * @see org.apache.commons.logging.Log#trace(Object, Throwable)
+ */
+ @Override
+ public void trace(Object message, Throwable t)
+ {
+ getLogger().log(FQCN, traceLevel, message, t);
+ }
+
+
+ /**
+ * Logs a message with <code>org.apache.log4j.Priority.DEBUG</code>.
+ *
+ * @param message to log
+ * @see org.apache.commons.logging.Log#debug(Object)
+ */
+ @Override
+ public void debug(Object message)
+ {
+ getLogger().log(FQCN, Priority.DEBUG, message, null);
+ }
+
+ /**
+ * Logs a message with <code>org.apache.log4j.Priority.DEBUG</code>.
+ *
+ * @param message to log
+ * @param t log this cause
+ * @see org.apache.commons.logging.Log#debug(Object, Throwable)
+ */
+ @Override
+ public void debug(Object message, Throwable t)
+ {
+ getLogger().log(FQCN, Priority.DEBUG, message, t);
+ }
+
+
+ /**
+ * Logs a message with <code>org.apache.log4j.Priority.INFO</code>.
+ *
+ * @param message to log
+ * @see org.apache.commons.logging.Log#info(Object)
+ */
+ @Override
+ public void info(Object message)
+ {
+ getLogger().log(FQCN, Priority.INFO, message, null);
+ }
+
+
+ /**
+ * Logs a message with <code>org.apache.log4j.Priority.INFO</code>.
+ *
+ * @param message to log
+ * @param t log this cause
+ * @see org.apache.commons.logging.Log#info(Object, Throwable)
+ */
+ @Override
+ public void info(Object message, Throwable t)
+ {
+ getLogger().log(FQCN, Priority.INFO, message, t);
+ }
+
+
+ /**
+ * Logs a message with <code>org.apache.log4j.Priority.WARN</code>.
+ *
+ * @param message to log
+ * @see org.apache.commons.logging.Log#warn(Object)
+ */
+ @Override
+ public void warn(Object message)
+ {
+ getLogger().log(FQCN, Priority.WARN, message, null);
+ }
+
+
+ /**
+ * Logs a message with <code>org.apache.log4j.Priority.WARN</code>.
+ *
+ * @param message to log
+ * @param t log this cause
+ * @see org.apache.commons.logging.Log#warn(Object, Throwable)
+ */
+ @Override
+ public void warn(Object message, Throwable t)
+ {
+ getLogger().log(FQCN, Priority.WARN, message, t);
+ }
+
+
+ /**
+ * Logs a message with <code>org.apache.log4j.Priority.ERROR</code>.
+ *
+ * @param message to log
+ * @see org.apache.commons.logging.Log#error(Object)
+ */
+ @Override
+ public void error(Object message)
+ {
+ getLogger().log(FQCN, Priority.ERROR, message, null);
+ }
+
+
+ /**
+ * Logs a message with <code>org.apache.log4j.Priority.ERROR</code>.
+ *
+ * @param message to log
+ * @param t log this cause
+ * @see org.apache.commons.logging.Log#error(Object, Throwable)
+ */
+ @Override
+ public void error(Object message, Throwable t)
+ {
+ getLogger().log(FQCN, Priority.ERROR, message, t);
+ }
+
+
+ /**
+ * Logs a message with <code>org.apache.log4j.Priority.FATAL</code>.
+ *
+ * @param message to log
+ * @see org.apache.commons.logging.Log#fatal(Object)
+ */
+ @Override
+ public void fatal(Object message)
+ {
+ getLogger().log(FQCN, Priority.FATAL, message, null);
+ }
+
+
+ /**
+ * Logs a message with <code>org.apache.log4j.Priority.FATAL</code>.
+ *
+ * @param message to log
+ * @param t log this cause
+ * @see org.apache.commons.logging.Log#fatal(Object, Throwable)
+ */
+ @Override
+ public void fatal(Object message, Throwable t)
+ {
+ getLogger().log(FQCN, Priority.FATAL, message, t);
+ }
+
+}
diff --git a/src/test/java/org/apache/commons/configuration/MockInitialContextFactory.java b/src/test/java/org/apache/commons/configuration/MockInitialContextFactory.java
new file mode 100644
index 0000000..6553db0
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/MockInitialContextFactory.java
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.util.Hashtable;
+
+import javax.naming.Context;
+import javax.naming.NameClassPair;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.spi.InitialContextFactory;
+
+import com.mockobjects.dynamic.C;
+import com.mockobjects.dynamic.Mock;
+
+/**
+ * A mock implementation of the {@code InitialContextFactory} interface.
+ * This implementation will return a mock context that contains some test data.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: MockInitialContextFactory.java 1222455 2011-12-22 21:10:10Z oheger $
+ */
+public class MockInitialContextFactory implements InitialContextFactory
+{
+ /**
+ * Constant for the use cycles environment property. If this property is
+ * present in the environment, a cyclic context will be created.
+ */
+ public static final String PROP_CYCLES = "useCycles";
+
+ /** Constant for the lookup method. */
+ private static final String METHOD_LOOKUP = "lookup";
+
+ /** Constant for the list method. */
+ private static final String METHOD_LIST = "list";
+
+ /** Constant for the close method.*/
+ private static final String METHOD_CLOSE = "close";
+
+ /** Constant for the name of the missing property. */
+ private static final String MISSING_PROP = "/missing";
+
+ /** Constant for the name of the prefix. */
+ private static final String PREFIX = "test/";
+
+ /** An array with the names of the supported properties. */
+ private static final String[] PROP_NAMES =
+ { "key", "key2", "short", "boolean", "byte", "double", "float", "integer",
+ "long", "onlyinjndi" };
+
+ /** An array with the values of the supported properties. */
+ private static final String[] PROP_VALUES =
+ { "jndivalue", "jndivalue2", "1", "true", "10", "10.25", "20.25", "10",
+ "1000000", "true" };
+
+ /** An array with properties that are requested, but are not in the context. */
+ private static final String[] MISSING_NAMES =
+ { "missing/list", "test/imaginarykey", "foo/bar" };
+
+ /**
+ * Creates a {@code Context} object that is backed by a mock object.
+ * The mock context can be queried for the values of certain test
+ * properties. It also supports listing the contained (sub) properties.
+ *
+ * @param env the environment
+ * @return the context mock
+ */
+ public Context getInitialContext(@SuppressWarnings("rawtypes") Hashtable env) throws NamingException
+ {
+ boolean useCycles = env.containsKey(PROP_CYCLES);
+
+ Mock mockTopCtx = createCtxMock(PREFIX);
+ Mock mockCycleCtx = createCtxMock("");
+ Mock mockPrfxCtx = createCtxMock("");
+ Mock mockBaseCtx = new Mock(Context.class);
+ mockBaseCtx.matchAndReturn(METHOD_LOOKUP, C.eq(""), mockTopCtx.proxy());
+ mockBaseCtx.matchAndReturn(METHOD_LOOKUP, C.eq("test"), mockPrfxCtx
+ .proxy());
+ mockTopCtx.matchAndReturn(METHOD_LOOKUP, C.eq("test"), mockPrfxCtx
+ .proxy());
+ mockPrfxCtx.matchAndReturn(METHOD_LIST, C.eq(""), createEnumMock(
+ mockPrfxCtx, PROP_NAMES, PROP_VALUES).proxy());
+
+ if (useCycles)
+ {
+ mockTopCtx.matchAndReturn(METHOD_LOOKUP, C.eq("cycle"),
+ mockCycleCtx.proxy());
+ mockTopCtx.matchAndReturn(METHOD_LIST, C.eq(""), createEnumMock(
+ mockTopCtx, new String[]
+ { "test", "cycle" }, new Object[]
+ { mockPrfxCtx.proxy(), mockCycleCtx.proxy() }).proxy());
+ Mock mockEnum = createEnumMock(mockCycleCtx, PROP_NAMES,
+ PROP_VALUES, false);
+ addEnumPair(mockEnum, "cycleCtx", mockCycleCtx.proxy());
+ closeEnum(mockEnum);
+ mockCycleCtx
+ .matchAndReturn(METHOD_LIST, C.eq(""), mockEnum.proxy());
+ mockCycleCtx.matchAndReturn(METHOD_LOOKUP, C.eq("cycleCtx"),
+ mockCycleCtx.proxy());
+ }
+ else
+ {
+ mockTopCtx.matchAndReturn(METHOD_LIST, C.eq(""), createEnumMock(
+ mockTopCtx, new String[]
+ { "test" }, new Object[]
+ { mockPrfxCtx.proxy() }).proxy());
+ }
+ return (Context) mockBaseCtx.proxy();
+ }
+
+ /**
+ * Creates a mock for a Context with the specified prefix.
+ *
+ * @param prefix the prefix
+ * @return the mock for the context
+ */
+ private Mock createCtxMock(String prefix)
+ {
+ Mock mockCtx = new Mock(Context.class);
+ for (int i = 0; i < PROP_NAMES.length; i++)
+ {
+ bind(mockCtx, prefix + PROP_NAMES[i], PROP_VALUES[i]);
+ String errProp = (prefix.length() > 0) ? PROP_NAMES[i] : PREFIX
+ + PROP_NAMES[i];
+ bindError(mockCtx, errProp);
+ }
+ for (int i = 0; i < MISSING_NAMES.length; i++)
+ {
+ bindError(mockCtx, MISSING_NAMES[i]);
+ }
+ mockCtx.matchAndReturn("hashCode", System.identityHashCode(mockCtx.proxy()));
+
+ return mockCtx;
+ }
+
+ /**
+ * Binds a property value to the mock context.
+ *
+ * @param mockCtx the context
+ * @param name the name of the property
+ * @param value the value of the property
+ */
+ private void bind(Mock mockCtx, String name, String value)
+ {
+ mockCtx.matchAndReturn(METHOD_LOOKUP, C.eq(name), value);
+ bindError(mockCtx, name + MISSING_PROP);
+ }
+
+ /**
+ * Configures the mock to expect a call for a non existing property.
+ *
+ * @param mockCtx the mock
+ * @param name the name of the property
+ */
+ private void bindError(Mock mockCtx, String name)
+ {
+ mockCtx.matchAndThrow(METHOD_LOOKUP, C.eq(name),
+ new NameNotFoundException("unknown property"));
+ }
+
+ /**
+ * Creates and initializes a mock for a naming enumeration.
+ *
+ * @param mockCtx the mock representing the context
+ * @param names the names contained in the iteration
+ * @param values the corresponding values
+ * @param close a flag whether the enumeration should expect to be closed
+ * @return the mock for the enumeration
+ */
+ private Mock createEnumMock(Mock mockCtx, String[] names, Object[] values,
+ boolean close)
+ {
+ Mock mockEnum = new Mock(NamingEnumeration.class);
+ for (int i = 0; i < names.length; i++)
+ {
+ addEnumPair(mockEnum, names[i], values[i]);
+ }
+ if (close)
+ {
+ closeEnum(mockEnum);
+ }
+ return mockEnum;
+ }
+
+ /**
+ * Creates and initializes a mock for a naming enumeration that expects to
+ * be closed. This is a shortcut of createEnumMock(mockCtx, names, values,
+ * true);
+ *
+ * @param mockCtx the mock representing the context
+ * @param names the names contained in the iteration
+ * @param values the corresponding values
+ * @return the mock for the enumeration
+ */
+ private Mock createEnumMock(Mock mockCtx, String[] names, Object[] values)
+ {
+ return createEnumMock(mockCtx, names, values, true);
+ }
+
+ /**
+ * Adds a new name-and-value pair to an enum mock.
+ *
+ * @param mockEnum the enum mock
+ * @param name the name
+ * @param value the value
+ */
+ private void addEnumPair(Mock mockEnum, String name, Object value)
+ {
+ NameClassPair ncp = new NameClassPair(name, value.getClass().getName());
+ mockEnum.expectAndReturn("hasMore", true);
+ mockEnum.expectAndReturn("next", ncp);
+ }
+
+ /**
+ * Closes an enumeration mock.
+ *
+ * @param mockEnum the mock
+ */
+ private void closeEnum(Mock mockEnum)
+ {
+ mockEnum.expectAndReturn("hasMore", false);
+ mockEnum.expect(METHOD_CLOSE);
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/NonCloneableConfiguration.java b/src/test/java/org/apache/commons/configuration/NonCloneableConfiguration.java
new file mode 100644
index 0000000..9e0c83b
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/NonCloneableConfiguration.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import java.util.Iterator;
+
+/**
+ * A specialized configuration implementation that does not support cloning.
+ * This class is only used in some test cases for testing implementations of
+ * clone() methods. It does not make much sense otherwise; all methods are just
+ * dummies.
+ *
+ * @version $Id: NonCloneableConfiguration.java 1222456 2011-12-22 21:11:39Z oheger $
+ */
+public class NonCloneableConfiguration extends AbstractConfiguration
+{
+ /**
+ * Dummy implementation of this method.
+ */
+ @Override
+ protected void addPropertyDirect(String key, Object value)
+ {
+ }
+
+ /**
+ * Dummy implementation of this method.
+ */
+ public boolean isEmpty()
+ {
+ return true;
+ }
+
+ /**
+ * Dummy implementation of this method.
+ */
+ public boolean containsKey(String key)
+ {
+ return false;
+ }
+
+ /**
+ * Dummy implementation of this method.
+ */
+ public Iterator<String> getKeys()
+ {
+ return null;
+ }
+
+ /**
+ * Dummy implementation of this method.
+ */
+ public Object getProperty(String key)
+ {
+ return null;
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/NonStringTestHolder.java b/src/test/java/org/apache/commons/configuration/NonStringTestHolder.java
new file mode 100644
index 0000000..6862b0f
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/NonStringTestHolder.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.util.Iterator;
+import java.util.List;
+
+import junit.framework.Assert;
+
+/**
+ * Pulling out the calls to do the tests so both JUnit and Cactus tests
+ * can share.
+ *
+ * @version $Id: NonStringTestHolder.java 1302002 2012-03-17 20:43:46Z sebb $
+ */
+public class NonStringTestHolder
+{
+ private Configuration configuration;
+
+ public void setConfiguration(Configuration configuration)
+ {
+ this.configuration = configuration;
+ }
+
+ public void testBoolean() throws Exception
+ {
+ boolean booleanValue = configuration.getBoolean("test.boolean");
+ Assert.assertTrue(booleanValue);
+ Assert.assertEquals(1, configuration.getList("test.boolean").size());
+ }
+
+ public void testBooleanDefaultValue() throws Exception
+ {
+ boolean booleanValue = configuration.getBoolean("test.boolean.missing", true);
+ Assert.assertTrue(booleanValue);
+
+ Boolean booleanObject = configuration.getBoolean("test.boolean.missing", new Boolean(true));
+ Assert.assertEquals(new Boolean(true), booleanObject);
+ }
+
+ public void testByte() throws Exception
+ {
+ byte testValue = 10;
+ byte byteValue = configuration.getByte("test.byte");
+ Assert.assertEquals(testValue, byteValue);
+ Assert.assertEquals(1, configuration.getList("test.byte").size());
+ }
+
+ public void testDouble() throws Exception
+ {
+ double testValue = 10.25;
+ double doubleValue = configuration.getDouble("test.double");
+ Assert.assertEquals(testValue, doubleValue, 0.01);
+ Assert.assertEquals(1, configuration.getList("test.double").size());
+ }
+
+ public void testDoubleDefaultValue() throws Exception
+ {
+ double testValue = 10.25;
+ double doubleValue = configuration.getDouble("test.double.missing", 10.25);
+
+ Assert.assertEquals(testValue, doubleValue, 0.01);
+ }
+
+ public void testFloat() throws Exception
+ {
+ float testValue = (float) 20.25;
+ float floatValue = configuration.getFloat("test.float");
+ Assert.assertEquals(testValue, floatValue, 0.01);
+ Assert.assertEquals(1, configuration.getList("test.float").size());
+ }
+
+ public void testFloatDefaultValue() throws Exception
+ {
+ float testValue = (float) 20.25;
+ float floatValue = configuration.getFloat("test.float.missing", testValue);
+ Assert.assertEquals(testValue, floatValue, 0.01);
+ }
+
+ public void testInteger() throws Exception
+ {
+ int intValue = configuration.getInt("test.integer");
+ Assert.assertEquals(10, intValue);
+ Assert.assertEquals(1, configuration.getList("test.integer").size());
+ }
+
+ public void testIntegerDefaultValue() throws Exception
+ {
+ int intValue = configuration.getInt("test.integer.missing", 10);
+ Assert.assertEquals(10, intValue);
+ }
+
+ public void testLong() throws Exception
+ {
+ long longValue = configuration.getLong("test.long");
+ Assert.assertEquals(1000000, longValue);
+ Assert.assertEquals(1, configuration.getList("test.long").size());
+ }
+ public void testLongDefaultValue() throws Exception
+ {
+ long longValue = configuration.getLong("test.long.missing", 1000000);
+ Assert.assertEquals(1000000, longValue);
+ }
+
+ public void testShort() throws Exception
+ {
+ short shortValue = configuration.getShort("test.short");
+ Assert.assertEquals(1, shortValue);
+ Assert.assertEquals(1, configuration.getList("test.short").size());
+ }
+
+ public void testShortDefaultValue() throws Exception
+ {
+ short shortValue = configuration.getShort("test.short.missing", (short) 1);
+ Assert.assertEquals(1, shortValue);
+ }
+
+ public void testListMissing() throws Exception
+ {
+ List<?> list = configuration.getList("missing.list");
+ Assert.assertTrue("'missing.list' is not empty", list.isEmpty());
+ }
+
+ public void testSubset() throws Exception
+ {
+ Configuration subset = configuration.subset("test");
+
+ // search the "short" key in the subset using the key iterator
+ boolean foundKeyValue = false;
+ Iterator<String> it = subset.getKeys();
+ while (it.hasNext() && !foundKeyValue)
+ {
+ String key = it.next();
+ foundKeyValue = "short".equals(key);
+ }
+
+ Assert.assertTrue("'short' key not found in the subset key iterator", foundKeyValue);
+ }
+
+ public void testIsEmpty() throws Exception
+ {
+ Assert.assertTrue("Configuration should not be empty", !configuration.isEmpty());
+ }
+
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestAbstractConfiguration.java b/src/test/java/org/apache/commons/configuration/TestAbstractConfiguration.java
new file mode 100644
index 0000000..d18d1d7
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestAbstractConfiguration.java
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import junitx.framework.ListAssert;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.Test;
+
+/**
+ * Abstract TestCase for implementations of {@link AbstractConfiguration}.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: TestAbstractConfiguration.java 1222465 2011-12-22 21:32:56Z oheger $
+ */
+public abstract class TestAbstractConfiguration
+{
+ /**
+ * Return an abstract configuration with the following data:<br>
+ * <pre>
+ * key1 = value1
+ * key2 = value2
+ * list = value1, value2
+ * listesc = value1\\,value2
+ * </pre>
+ */
+ protected abstract AbstractConfiguration getConfiguration();
+
+ /**
+ * Return an empty configuration.
+ */
+ protected abstract AbstractConfiguration getEmptyConfiguration();
+
+ @Test
+ public void testGetProperty()
+ {
+ Configuration config = getConfiguration();
+ assertEquals("key1", "value1", config.getProperty("key1"));
+ assertEquals("key2", "value2", config.getProperty("key2"));
+ assertNull("key3", config.getProperty("key3"));
+ }
+
+ @Test
+ public void testList()
+ {
+ Configuration config = getConfiguration();
+
+ List<?> list = config.getList("list");
+ assertNotNull("list not found", config.getProperty("list"));
+ assertEquals("list size", 2, list.size());
+ assertTrue("'value1' is not in the list", list.contains("value1"));
+ assertTrue("'value2' is not in the list", list.contains("value2"));
+ }
+
+ /**
+ * Tests whether the escape character for list delimiters is recocknized and
+ * removed.
+ */
+ @Test
+ public void testListEscaped()
+ {
+ assertEquals("Wrong value for escaped list", "value1,value2",
+ getConfiguration().getString("listesc"));
+ }
+
+ @Test
+ public void testAddPropertyDirect()
+ {
+ AbstractConfiguration config = getConfiguration();
+ config.addPropertyDirect("key3", "value3");
+ assertEquals("key3", "value3", config.getProperty("key3"));
+
+ config.addPropertyDirect("key3", "value4");
+ config.addPropertyDirect("key3", "value5");
+ List<Object> list = config.getList("key3");
+ assertNotNull("no list found for the 'key3' property", list);
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add("value3");
+ expected.add("value4");
+ expected.add("value5");
+
+ ListAssert.assertEquals("values for the 'key3' property", expected, list);
+ }
+
+ @Test
+ public void testIsEmpty()
+ {
+ Configuration config = getConfiguration();
+ assertFalse("the configuration is empty", config.isEmpty());
+ assertTrue("the configuration is not empty", getEmptyConfiguration().isEmpty());
+ }
+
+ @Test
+ public void testContainsKey()
+ {
+ Configuration config = getConfiguration();
+ assertTrue("key1 not found", config.containsKey("key1"));
+ assertFalse("key3 found", config.containsKey("key3"));
+ }
+
+ @Test
+ public void testClearProperty()
+ {
+ Configuration config = getConfiguration();
+ config.clearProperty("key2");
+ assertFalse("key2 not cleared", config.containsKey("key2"));
+ }
+
+ @Test
+ public void testGetKeys()
+ {
+ Configuration config = getConfiguration();
+ Iterator<String> keys = config.getKeys();
+
+ List<String> expectedKeys = new ArrayList<String>();
+ expectedKeys.add("key1");
+ expectedKeys.add("key2");
+ expectedKeys.add("list");
+ expectedKeys.add("listesc");
+
+ assertNotNull("null iterator", keys);
+ assertTrue("empty iterator", keys.hasNext());
+
+ List<String> actualKeys = new ArrayList<String>();
+ while (keys.hasNext())
+ {
+ actualKeys.add(keys.next());
+ }
+
+ ListAssert.assertEquals("keys", expectedKeys, actualKeys);
+ }
+
+ /**
+ * Tests accessing the configuration's logger.
+ */
+ @Test
+ public void testSetLogger()
+ {
+ AbstractConfiguration config = getEmptyConfiguration();
+ assertNotNull("Default logger is null", config.getLogger());
+ Log log = LogFactory.getLog(config.getClass());
+ config.setLogger(log);
+ assertSame("Logger was not set", log, config.getLogger());
+ }
+
+ /**
+ * Tests the exception message triggered by the conversion to BigInteger.
+ * This test is related to CONFIGURATION-357.
+ */
+ @Test
+ public void testGetBigIntegerConversion()
+ {
+ Configuration config = getConfiguration();
+ try
+ {
+ config.getBigInteger("key1");
+ fail("No conversion exception thrown!");
+ }
+ catch (ConversionException cex)
+ {
+ assertEquals("Wrong exception message",
+ "'key1' doesn't map to a BigInteger object", cex
+ .getMessage());
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestAbstractConfigurationBasicFeatures.java b/src/test/java/org/apache/commons/configuration/TestAbstractConfigurationBasicFeatures.java
new file mode 100644
index 0000000..35da02a
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestAbstractConfigurationBasicFeatures.java
@@ -0,0 +1,529 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.junit.Test;
+
+/**
+ * A test class for some of the basic functionality implemented by
+ * AbstractConfiguration.
+ *
+ * @version $Id: TestAbstractConfigurationBasicFeatures.java 1222823 2011-12-23 20:03:10Z oheger $
+ */
+public class TestAbstractConfigurationBasicFeatures
+{
+ /** Constant for the prefix of test keys.*/
+ private static final String KEY_PREFIX = "key";
+
+ /** Constant for the number of properties in tests for copy operations.*/
+ private static final int PROP_COUNT = 12;
+
+ /**
+ * Tests the clear() implementation of AbstractConfiguration if the iterator
+ * returned by getKeys() does not support the remove() operation.
+ */
+ @Test
+ public void testClearIteratorNoRemove()
+ {
+ AbstractConfiguration config = new TestConfigurationImpl(
+ new BaseConfiguration())
+ {
+ // return an iterator that does not support remove operations
+ @Override
+ public Iterator<String> getKeys()
+ {
+ Collection<String> keyCol = new ArrayList<String>();
+ CollectionUtils.addAll(keyCol, getUnderlyingConfiguration()
+ .getKeys());
+ String[] keys = keyCol.toArray(new String[keyCol.size()]);
+ return Arrays.asList(keys).iterator();
+ }
+ };
+ for (int i = 0; i < 20; i++)
+ {
+ config.addProperty("key" + i, "value" + i);
+ }
+ config.clear();
+ assertTrue("Configuration not empty", config.isEmpty());
+ }
+
+ /**
+ * Tests escaping the variable marker, so that no interpolation will be
+ * performed.
+ */
+ @Test
+ public void testInterpolateEscape()
+ {
+ AbstractConfiguration config = new TestConfigurationImpl(
+ new PropertiesConfiguration());
+ config
+ .addProperty(
+ "mypath",
+ "$${DB2UNIVERSAL_JDBC_DRIVER_PATH}/db2jcc.jar\\,$${DB2UNIVERSAL_JDBC_DRIVER_PATH}/db2jcc_license_cu.jar");
+ assertEquals(
+ "Wrong interpolated value",
+ "${DB2UNIVERSAL_JDBC_DRIVER_PATH}/db2jcc.jar,${DB2UNIVERSAL_JDBC_DRIVER_PATH}/db2jcc_license_cu.jar",
+ config.getString("mypath"));
+ }
+
+ /**
+ * Tests adding list properties. The single elements of the list should be
+ * added.
+ */
+ @Test
+ public void testAddPropertyList()
+ {
+ checkAddListProperty(new TestConfigurationImpl(
+ new PropertiesConfiguration()));
+ }
+
+ /**
+ * Tests adding list properties when delimiter parsing is disabled.
+ */
+ @Test
+ public void testAddPropertyListNoDelimiterParsing()
+ {
+ AbstractConfiguration config = new TestConfigurationImpl(
+ new PropertiesConfiguration());
+ config.setDelimiterParsingDisabled(true);
+ checkAddListProperty(config);
+ }
+
+ /**
+ * Helper method for adding properties with multiple values.
+ *
+ * @param config the configuration to be used for testing
+ */
+ private void checkAddListProperty(AbstractConfiguration config)
+ {
+ config.addProperty("test", "value1");
+ Object[] lstValues1 = new Object[]
+ { "value2", "value3" };
+ Object[] lstValues2 = new Object[]
+ { "value4", "value5", "value6" };
+ config.addProperty("test", lstValues1);
+ config.addProperty("test", Arrays.asList(lstValues2));
+ List<Object> lst = config.getList("test");
+ assertEquals("Wrong number of list elements", 6, lst.size());
+ for (int i = 0; i < lst.size(); i++)
+ {
+ assertEquals("Wrong list element at " + i, "value" + (i + 1), lst
+ .get(i));
+ }
+ }
+
+ /**
+ * Tests the copy() method.
+ */
+ @Test
+ public void testCopy()
+ {
+ AbstractConfiguration config = setUpDestConfig();
+ Configuration srcConfig = setUpSourceConfig();
+ config.copy(srcConfig);
+ for (int i = 0; i < PROP_COUNT; i++)
+ {
+ String key = KEY_PREFIX + i;
+ if (srcConfig.containsKey(key))
+ {
+ assertEquals("Value not replaced: " + key, srcConfig
+ .getProperty(key), config.getProperty(key));
+ }
+ else
+ {
+ assertEquals("Value modified: " + key, "value" + i, config
+ .getProperty(key));
+ }
+ }
+ }
+
+ /**
+ * Tests the copy() method when properties with multiple values and escaped
+ * list delimiters are involved.
+ */
+ @Test
+ public void testCopyWithLists()
+ {
+ Configuration srcConfig = setUpSourceConfig();
+ AbstractConfiguration config = setUpDestConfig();
+ config.copy(srcConfig);
+ checkListProperties(config);
+ }
+
+ /**
+ * Tests the events generated by a copy() operation.
+ */
+ @Test
+ public void testCopyEvents()
+ {
+ AbstractConfiguration config = setUpDestConfig();
+ Configuration srcConfig = setUpSourceConfig();
+ CollectingConfigurationListener l = new CollectingConfigurationListener();
+ config.addConfigurationListener(l);
+ config.copy(srcConfig);
+ checkCopyEvents(l, srcConfig, AbstractConfiguration.EVENT_SET_PROPERTY);
+ }
+
+ /**
+ * Tests copying a null configuration. This should be a noop.
+ */
+ @Test
+ public void testCopyNull()
+ {
+ AbstractConfiguration config = setUpDestConfig();
+ config.copy(null);
+ ConfigurationAssert.assertEquals(setUpDestConfig(), config);
+ }
+
+ /**
+ * Tests the append() method.
+ */
+ @Test
+ public void testAppend()
+ {
+ AbstractConfiguration config = setUpDestConfig();
+ Configuration srcConfig = setUpSourceConfig();
+ config.append(srcConfig);
+ for (int i = 0; i < PROP_COUNT; i++)
+ {
+ String key = KEY_PREFIX + i;
+ if (srcConfig.containsKey(key))
+ {
+ List<Object> values = config.getList(key);
+ assertEquals("Value not added: " + key, 2, values.size());
+ assertEquals("Wrong value 1 for " + key, "value" + i, values
+ .get(0));
+ assertEquals("Wrong value 2 for " + key, "src" + i, values
+ .get(1));
+ }
+ else
+ {
+ assertEquals("Value modified: " + key, "value" + i, config
+ .getProperty(key));
+ }
+ }
+ }
+
+ /**
+ * Tests the append() method when properties with multiple values and
+ * escaped list delimiters are involved.
+ */
+ @Test
+ public void testAppendWithLists()
+ {
+ AbstractConfiguration config = setUpDestConfig();
+ config.append(setUpSourceConfig());
+ checkListProperties(config);
+ }
+
+ /**
+ * Tests the events generated by an append() operation.
+ */
+ @Test
+ public void testAppendEvents()
+ {
+ AbstractConfiguration config = setUpDestConfig();
+ Configuration srcConfig = setUpSourceConfig();
+ CollectingConfigurationListener l = new CollectingConfigurationListener();
+ config.addConfigurationListener(l);
+ config.append(srcConfig);
+ checkCopyEvents(l, srcConfig, AbstractConfiguration.EVENT_ADD_PROPERTY);
+ }
+
+ /**
+ * Tests appending a null configuration. This should be a noop.
+ */
+ @Test
+ public void testAppendNull()
+ {
+ AbstractConfiguration config = setUpDestConfig();
+ config.append(null);
+ ConfigurationAssert.assertEquals(setUpDestConfig(), config);
+ }
+
+ /**
+ * Tests whether environment variables can be interpolated.
+ */
+ @Test
+ public void testInterpolateEnvironmentVariables()
+ {
+ AbstractConfiguration config = new TestConfigurationImpl(
+ new PropertiesConfiguration());
+ EnvironmentConfiguration envConfig = new EnvironmentConfiguration();
+ Map<String, Object> env = new HashMap<String, Object>();
+ for (Iterator<String> it = envConfig.getKeys(); it.hasNext();)
+ {
+ String key = it.next();
+ String propKey = "envtest." + key;
+ env.put(propKey, envConfig.getString(key));
+ config.addProperty(propKey, "${env:" + key + "}");
+ }
+ assertFalse("No environment properties", env.isEmpty());
+ for (Map.Entry<String, Object> e : env.entrySet())
+ {
+ assertEquals("Wrong value for " + e.getKey(), e.getValue(), config
+ .getString(e.getKey()));
+ }
+ }
+
+ /**
+ * Tests getList() for single non-string values.
+ */
+ @Test
+ public void testGetListNonString()
+ {
+ checkGetListScalar(Integer.valueOf(42));
+ checkGetListScalar(Long.valueOf(42));
+ checkGetListScalar(Short.valueOf((short) 42));
+ checkGetListScalar(Byte.valueOf((byte) 42));
+ checkGetListScalar(Float.valueOf(42));
+ checkGetListScalar(Double.valueOf(42));
+ checkGetListScalar(Boolean.TRUE);
+}
+
+ /**
+ * Tests getStringArray() for single son-string values.
+ */
+ @Test
+ public void testGetStringArrayNonString()
+ {
+ checkGetStringArrayScalar(Integer.valueOf(42));
+ checkGetStringArrayScalar(Long.valueOf(42));
+ checkGetStringArrayScalar(Short.valueOf((short) 42));
+ checkGetStringArrayScalar(Byte.valueOf((byte) 42));
+ checkGetStringArrayScalar(Float.valueOf(42));
+ checkGetStringArrayScalar(Double.valueOf(42));
+ checkGetStringArrayScalar(Boolean.TRUE);
+ }
+
+ /**
+ * Helper method for checking getList() if the property value is a scalar.
+ * @param value the value of the property
+ */
+ private void checkGetListScalar(Object value)
+ {
+ BaseConfiguration config = new BaseConfiguration();
+ config.addProperty(KEY_PREFIX, value);
+ List<Object> lst = config.getList(KEY_PREFIX);
+ assertEquals("Wrong number of values", 1, lst.size());
+ assertEquals("Wrong value", value.toString(), lst.get(0));
+ }
+
+ /**
+ * Helper method for checking getStringArray() if the property value is a
+ * scalar.
+ *
+ * @param value the value of the property
+ */
+ private void checkGetStringArrayScalar(Object value)
+ {
+ BaseConfiguration config = new BaseConfiguration();
+ config.addProperty(KEY_PREFIX, value);
+ String[] array = config.getStringArray(KEY_PREFIX);
+ assertEquals("Weong number of elements", 1, array.length);
+ assertEquals("Wrong value", value.toString(), array[0]);
+ }
+
+ /**
+ * Tests whether interpolation works in variable names.
+ */
+ @Test
+ public void testNestedVariableInterpolation()
+ {
+ BaseConfiguration config = new BaseConfiguration();
+ config.getSubstitutor().setEnableSubstitutionInVariables(true);
+ config.addProperty("java.version", "1.4");
+ config.addProperty("jre-1.4", "C:\\java\\1.4");
+ config.addProperty("jre.path", "${jre-${java.version}}");
+ assertEquals("Wrong path", "C:\\java\\1.4",
+ config.getString("jre.path"));
+ }
+
+ /**
+ * Creates the source configuration for testing the copy() and append()
+ * methods. This configuration contains keys with an odd index and values
+ * starting with the prefix "src". There are also some list properties.
+ *
+ * @return the source configuration for copy operations
+ */
+ private Configuration setUpSourceConfig()
+ {
+ BaseConfiguration config = new BaseConfiguration();
+ for (int i = 1; i < PROP_COUNT; i += 2)
+ {
+ config.addProperty(KEY_PREFIX + i, "src" + i);
+ }
+ config.addProperty("list1", "1,2,3");
+ config.addProperty("list2", "3\\,1415,9\\,81");
+ return config;
+ }
+
+ /**
+ * Creates the destination configuration for testing the copy() and append()
+ * methods. This configuration contains keys with a running index and
+ * corresponding values starting with the prefix "value".
+ *
+ * @return the destination configuration for copy operations
+ */
+ private AbstractConfiguration setUpDestConfig()
+ {
+ AbstractConfiguration config = new TestConfigurationImpl(
+ new PropertiesConfiguration());
+ for (int i = 0; i < PROP_COUNT; i++)
+ {
+ config.addProperty(KEY_PREFIX + i, "value" + i);
+ }
+ return config;
+ }
+
+ /**
+ * Tests the values of list properties after a copy operation.
+ *
+ * @param config the configuration to test
+ */
+ private void checkListProperties(Configuration config)
+ {
+ List<Object> values = config.getList("list1");
+ assertEquals("Wrong number of elements in list 1", 3, values.size());
+ values = config.getList("list2");
+ assertEquals("Wrong number of elements in list 2", 2, values.size());
+ assertEquals("Wrong value 1", "3,1415", values.get(0));
+ assertEquals("Wrong value 2", "9,81", values.get(1));
+ }
+
+ /**
+ * Tests whether the correct events are received for a copy operation.
+ *
+ * @param l the event listener
+ * @param src the configuration that was copied
+ * @param eventType the expected event type
+ */
+ private void checkCopyEvents(CollectingConfigurationListener l,
+ Configuration src, int eventType)
+ {
+ Map<String, ConfigurationEvent> events = new HashMap<String, ConfigurationEvent>();
+ for (ConfigurationEvent e : l.events)
+ {
+ assertEquals("Wrong event type", eventType, e.getType());
+ assertTrue("Unknown property: " + e.getPropertyName(), src
+ .containsKey(e.getPropertyName()));
+ assertEquals("Wrong property value for " + e.getPropertyName(), e
+ .getPropertyValue(), src.getProperty(e.getPropertyName()));
+ if (!e.isBeforeUpdate())
+ {
+ assertTrue("After event without before event", events
+ .containsKey(e.getPropertyName()));
+ }
+ else
+ {
+ events.put(e.getPropertyName(), e);
+ }
+ }
+
+ for (Iterator<String> it = src.getKeys(); it.hasNext();)
+ {
+ String key = it.next();
+ assertTrue("No event received for key " + key, events
+ .containsKey(key));
+ }
+ }
+
+ /**
+ * A test configuration implementation. This implementation inherits
+ * directly from AbstractConfiguration. For implementing the required
+ * functionality another implementation of AbstractConfiguration is used;
+ * all methods that need to be implemented delegate to this wrapped
+ * configuration.
+ */
+ static class TestConfigurationImpl extends AbstractConfiguration
+ {
+ /** Stores the underlying configuration. */
+ private AbstractConfiguration config;
+
+ public AbstractConfiguration getUnderlyingConfiguration()
+ {
+ return config;
+ }
+
+ public TestConfigurationImpl(AbstractConfiguration wrappedConfig)
+ {
+ config = wrappedConfig;
+ }
+
+ @Override
+ protected void addPropertyDirect(String key, Object value)
+ {
+ config.addPropertyDirect(key, value);
+ }
+
+ public boolean containsKey(String key)
+ {
+ return config.containsKey(key);
+ }
+
+ public Iterator<String> getKeys()
+ {
+ return config.getKeys();
+ }
+
+ public Object getProperty(String key)
+ {
+ return config.getProperty(key);
+ }
+
+ public boolean isEmpty()
+ {
+ return config.isEmpty();
+ }
+
+ @Override
+ protected void clearPropertyDirect(String key)
+ {
+ config.clearPropertyDirect(key);
+ }
+ }
+
+ /**
+ * An event listener implementation that simply collects all received
+ * configuration events.
+ */
+ static class CollectingConfigurationListener implements
+ ConfigurationListener
+ {
+ List<ConfigurationEvent> events = new ArrayList<ConfigurationEvent>();
+
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ events.add(event);
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestBaseConfiguration.java b/src/test/java/org/apache/commons/configuration/TestBaseConfiguration.java
new file mode 100644
index 0000000..a0c363a
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestBaseConfiguration.java
@@ -0,0 +1,733 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import junitx.framework.ListAssert;
+
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests some basic functions of the BaseConfiguration class. Missing keys will
+ * throw Exceptions
+ *
+ * @version $Id: TestBaseConfiguration.java 1231721 2012-01-15 18:32:07Z oheger $
+ */
+public class TestBaseConfiguration
+{
+ /** Constant for the number key.*/
+ static final String KEY_NUMBER = "number";
+
+ protected BaseConfiguration config = null;
+
+ protected static Class<?> missingElementException = NoSuchElementException.class;
+ protected static Class<?> incompatibleElementException = ConversionException.class;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ config = new BaseConfiguration();
+ config.setThrowExceptionOnMissing(true);
+ }
+
+ @Test
+ public void testThrowExceptionOnMissing()
+ {
+ assertTrue("Throw Exception Property is not set!", config.isThrowExceptionOnMissing());
+ }
+
+ @Test
+ public void testGetProperty()
+ {
+ /* should be empty and return null */
+ assertEquals("This returns null", config.getProperty("foo"), null);
+
+ /* add a real value, and get it two different ways */
+ config.setProperty("number", "1");
+ assertEquals("This returns '1'", config.getProperty("number"), "1");
+ assertEquals("This returns '1'", config.getString("number"), "1");
+ }
+
+ @Test
+ public void testGetByte()
+ {
+ config.setProperty("number", "1");
+ byte oneB = 1;
+ byte twoB = 2;
+ assertEquals("This returns 1(byte)", oneB, config.getByte("number"));
+ assertEquals("This returns 1(byte)", oneB, config.getByte("number", twoB));
+ assertEquals("This returns 2(default byte)", twoB, config.getByte("numberNotInConfig", twoB));
+ assertEquals("This returns 1(Byte)", new Byte(oneB), config.getByte("number", new Byte("2")));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetByteUnknown()
+ {
+ config.getByte("numberNotInConfig");
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetByteIncompatibleType()
+ {
+ config.setProperty("test.empty", "");
+ config.getByte("test.empty");
+ }
+
+ @Test
+ public void testGetShort()
+ {
+ config.setProperty("numberS", "1");
+ short oneS = 1;
+ short twoS = 2;
+ assertEquals("This returns 1(short)", oneS, config.getShort("numberS"));
+ assertEquals("This returns 1(short)", oneS, config.getShort("numberS", twoS));
+ assertEquals("This returns 2(default short)", twoS, config.getShort("numberNotInConfig", twoS));
+ assertEquals("This returns 1(Short)", new Short(oneS), config.getShort("numberS", new Short("2")));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetShortUnknown()
+ {
+ config.getShort("numberNotInConfig");
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetShortIncompatibleType()
+ {
+ config.setProperty("test.empty", "");
+ config.getShort("test.empty");
+ }
+
+ @Test
+ public void testGetLong()
+ {
+ config.setProperty("numberL", "1");
+ long oneL = 1;
+ long twoL = 2;
+ assertEquals("This returns 1(long)", oneL, config.getLong("numberL"));
+ assertEquals("This returns 1(long)", oneL, config.getLong("numberL", twoL));
+ assertEquals("This returns 2(default long)", twoL, config.getLong("numberNotInConfig", twoL));
+ assertEquals("This returns 1(Long)", new Long(oneL), config.getLong("numberL", new Long("2")));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetLongUnknown()
+ {
+ config.getLong("numberNotInConfig");
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetLongIncompatibleTypes()
+ {
+ config.setProperty("test.empty", "");
+ config.getLong("test.empty");
+ }
+
+ @Test
+ public void testGetFloat()
+ {
+ config.setProperty("numberF", "1.0");
+ float oneF = 1;
+ float twoF = 2;
+ assertEquals("This returns 1(float)", oneF, config.getFloat("numberF"), 0);
+ assertEquals("This returns 1(float)", oneF, config.getFloat("numberF", twoF), 0);
+ assertEquals("This returns 2(default float)", twoF, config.getFloat("numberNotInConfig", twoF), 0);
+ assertEquals("This returns 1(Float)", new Float(oneF), config.getFloat("numberF", new Float("2")));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetFloatUnknown()
+ {
+ config.getFloat("numberNotInConfig");
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetFloatIncompatibleType()
+ {
+ config.setProperty("test.empty", "");
+ config.getFloat("test.empty");
+ }
+
+ @Test
+ public void testGetDouble()
+ {
+ config.setProperty("numberD", "1.0");
+ double oneD = 1;
+ double twoD = 2;
+ assertEquals("This returns 1(double)", oneD, config.getDouble("numberD"), 0);
+ assertEquals("This returns 1(double)", oneD, config.getDouble("numberD", twoD), 0);
+ assertEquals("This returns 2(default double)", twoD, config.getDouble("numberNotInConfig", twoD), 0);
+ assertEquals("This returns 1(Double)", new Double(oneD), config.getDouble("numberD", new Double("2")));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetDoubleUnknown()
+ {
+ config.getDouble("numberNotInConfig");
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetDoubleIncompatibleType()
+ {
+ config.setProperty("test.empty", "");
+ config.getDouble("test.empty");
+ }
+
+ @Test
+ public void testGetBigDecimal()
+ {
+ config.setProperty("numberBigD", "123.456");
+ BigDecimal number = new BigDecimal("123.456");
+ BigDecimal defaultValue = new BigDecimal("654.321");
+
+ assertEquals("Existing key", number, config.getBigDecimal("numberBigD"));
+ assertEquals("Existing key with default value", number, config.getBigDecimal("numberBigD", defaultValue));
+ assertEquals("Missing key with default value", defaultValue, config.getBigDecimal("numberNotInConfig", defaultValue));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetBigDecimalUnknown()
+ {
+ config.getBigDecimal("numberNotInConfig");
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetBigDecimalIncompatibleType()
+ {
+ config.setProperty("test.empty", "");
+ config.getBigDecimal("test.empty");
+ }
+
+ @Test
+ public void testGetBigInteger()
+ {
+ config.setProperty("numberBigI", "1234567890");
+ BigInteger number = new BigInteger("1234567890");
+ BigInteger defaultValue = new BigInteger("654321");
+
+ assertEquals("Existing key", number, config.getBigInteger("numberBigI"));
+ assertEquals("Existing key with default value", number, config.getBigInteger("numberBigI", defaultValue));
+ assertEquals("Missing key with default value", defaultValue, config.getBigInteger("numberNotInConfig", defaultValue));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetBigIntegerUnknown()
+ {
+ config.getBigInteger("numberNotInConfig");
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetBigIntegerIncompatibleType()
+ {
+ config.setProperty("test.empty", "");
+ config.getBigInteger("test.empty");
+ }
+
+ @Test
+ public void testGetString()
+ {
+ config.setProperty("testString", "The quick brown fox");
+ String string = "The quick brown fox";
+ String defaultValue = "jumps over the lazy dog";
+
+ assertEquals("Existing key", string, config.getString("testString"));
+ assertEquals("Existing key with default value", string, config.getString("testString", defaultValue));
+ assertEquals("Missing key with default value", defaultValue, config.getString("stringNotInConfig", defaultValue));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetStringUnknown()
+ {
+ config.getString("stringNotInConfig");
+ }
+
+ @Test
+ public void testGetBoolean()
+ {
+ config.setProperty("boolA", Boolean.TRUE);
+ boolean boolT = true, boolF = false;
+ assertEquals("This returns true", boolT, config.getBoolean("boolA"));
+ assertEquals("This returns true, not the default", boolT, config.getBoolean("boolA", boolF));
+ assertEquals("This returns false(default)", boolF, config.getBoolean("boolNotInConfig", boolF));
+ assertEquals("This returns true(Boolean)", new Boolean(boolT), config.getBoolean("boolA", new Boolean(boolF)));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetBooleanUnknown()
+ {
+ config.getBoolean("numberNotInConfig");
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetBooleanIncompatibleType()
+ {
+ config.setProperty("test.empty", "");
+ config.getBoolean("test.empty");
+ }
+
+ @Test
+ public void testGetList()
+ {
+ config.addProperty("number", "1");
+ config.addProperty("number", "2");
+ List<Object> list = config.getList("number");
+ assertNotNull("The list is null", list);
+ assertEquals("List size", 2, list.size());
+ assertTrue("The number 1 is missing from the list", list.contains("1"));
+ assertTrue("The number 2 is missing from the list", list.contains("2"));
+ }
+
+ /**
+ * Tests that the first scalar of a list is returned.
+ */
+ @Test
+ public void testGetStringForListValue()
+ {
+ config.addProperty("number", "1");
+ config.addProperty("number", "2");
+ assertEquals("Wrong result", "1", config.getString("number"));
+ }
+
+ @Test
+ public void testGetInterpolatedList()
+ {
+ config.addProperty("number", "1");
+ config.addProperty("array", "${number}");
+ config.addProperty("array", "${number}");
+
+ List<String> list = new ArrayList<String>();
+ list.add("1");
+ list.add("1");
+
+ ListAssert.assertEquals("'array' property", list, config.getList("array"));
+ }
+
+ @Test
+ public void testGetInterpolatedPrimitives()
+ {
+ config.addProperty("number", "1");
+ config.addProperty("value", "${number}");
+
+ config.addProperty("boolean", "true");
+ config.addProperty("booleanValue", "${boolean}");
+
+ // primitive types
+ assertEquals("boolean interpolation", true, config.getBoolean("booleanValue"));
+ assertEquals("byte interpolation", 1, config.getByte("value"));
+ assertEquals("short interpolation", 1, config.getShort("value"));
+ assertEquals("int interpolation", 1, config.getInt("value"));
+ assertEquals("long interpolation", 1, config.getLong("value"));
+ assertEquals("float interpolation", 1, config.getFloat("value"), 0);
+ assertEquals("double interpolation", 1, config.getDouble("value"), 0);
+
+ // primitive wrappers
+ assertEquals("Boolean interpolation", Boolean.TRUE, config.getBoolean("booleanValue", null));
+ assertEquals("Byte interpolation", new Byte("1"), config.getByte("value", null));
+ assertEquals("Short interpolation", new Short("1"), config.getShort("value", null));
+ assertEquals("Integer interpolation", new Integer("1"), config.getInteger("value", null));
+ assertEquals("Long interpolation", new Long("1"), config.getLong("value", null));
+ assertEquals("Float interpolation", new Float("1"), config.getFloat("value", null));
+ assertEquals("Double interpolation", new Double("1"), config.getDouble("value", null));
+
+ assertEquals("BigInteger interpolation", new BigInteger("1"), config.getBigInteger("value", null));
+ assertEquals("BigDecimal interpolation", new BigDecimal("1"), config.getBigDecimal("value", null));
+ }
+
+ @Test
+ public void testCommaSeparatedString()
+ {
+ String prop = "hey, that's a test";
+ config.setProperty("prop.string", prop);
+ List<Object> list = config.getList("prop.string");
+ assertEquals("Wrong number of list elements", 2, list.size());
+ assertEquals("Wrong element 1", "hey", list.get(0));
+ }
+
+ @Test
+ public void testCommaSeparatedStringEscaped()
+ {
+ String prop2 = "hey\\, that's a test";
+ config.setProperty("prop.string", prop2);
+ assertEquals("Wrong value", "hey, that's a test", config.getString("prop.string"));
+ }
+
+ @Test
+ public void testAddProperty() throws Exception
+ {
+ Collection<Object> props = new ArrayList<Object>();
+ props.add("one");
+ props.add("two,three,four");
+ props.add(new String[] { "5.1", "5.2", "5.3,5.4", "5.5" });
+ props.add("six");
+ config.addProperty("complex.property", props);
+
+ Object val = config.getProperty("complex.property");
+ assertTrue(val instanceof Collection);
+ Collection<?> col = (Collection<?>) val;
+ assertEquals(10, col.size());
+
+ props = new ArrayList<Object>();
+ props.add("quick");
+ props.add("brown");
+ props.add("fox,jumps");
+ Object[] data = new Object[] {
+ "The", props, "over,the", "lazy", "dog."
+ };
+ config.setProperty("complex.property", data);
+ val = config.getProperty("complex.property");
+ assertTrue(val instanceof Collection);
+ col = (Collection<?>) val;
+ Iterator<?> it = col.iterator();
+ StringTokenizer tok = new StringTokenizer("The quick brown fox jumps over the lazy dog.", " ");
+ while(tok.hasMoreTokens())
+ {
+ assertTrue(it.hasNext());
+ assertEquals(tok.nextToken(), it.next());
+ }
+ assertFalse(it.hasNext());
+
+ config.setProperty("complex.property", null);
+ assertFalse(config.containsKey("complex.property"));
+ }
+
+ @Test
+ public void testPropertyAccess()
+ {
+ config.clearProperty("prop.properties");
+ config.setProperty("prop.properties", "");
+ assertEquals(
+ "This returns an empty Properties object",
+ config.getProperties("prop.properties"),
+ new Properties());
+ config.clearProperty("prop.properties");
+ config.setProperty("prop.properties", "foo=bar, baz=moo, seal=clubber");
+
+ Properties p = new Properties();
+ p.setProperty("foo", "bar");
+ p.setProperty("baz", "moo");
+ p.setProperty("seal", "clubber");
+ assertEquals(
+ "This returns a filled in Properties object",
+ config.getProperties("prop.properties"),
+ p);
+ }
+
+ @Test
+ public void testSubset()
+ {
+ /*
+ * test subset : assure we don't reprocess the data elements
+ * when generating the subset
+ */
+
+ String prop = "hey, that's a test";
+ String prop2 = "hey\\, that's a test";
+ config.setProperty("prop.string", prop2);
+ config.setProperty("property.string", "hello");
+
+ Configuration subEprop = config.subset("prop");
+
+ assertEquals(
+ "Returns the full string",
+ prop,
+ subEprop.getString("string"));
+ assertEquals("Wrong list size", 1, subEprop.getList("string").size());
+
+ Iterator<String> it = subEprop.getKeys();
+ it.next();
+ assertFalse(it.hasNext());
+
+ subEprop = config.subset("prop.");
+ it = subEprop.getKeys();
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void testInterpolation()
+ {
+ InterpolationTestHelper.testInterpolation(config);
+ }
+
+ @Test
+ public void testMultipleInterpolation()
+ {
+ InterpolationTestHelper.testMultipleInterpolation(config);
+ }
+
+ @Test
+ public void testInterpolationLoop()
+ {
+ InterpolationTestHelper.testInterpolationLoop(config);
+ }
+
+ /**
+ * Tests interpolation when a subset configuration is involved.
+ */
+ @Test
+ public void testInterpolationSubset()
+ {
+ InterpolationTestHelper.testInterpolationSubset(config);
+ }
+
+ /**
+ * Tests interpolation when the referred property is not found.
+ */
+ @Test
+ public void testInterpolationUnknownProperty()
+ {
+ InterpolationTestHelper.testInterpolationUnknownProperty(config);
+ }
+
+ /**
+ * Tests interpolation of system properties.
+ */
+ @Test
+ public void testInterpolationSystemProperties()
+ {
+ InterpolationTestHelper.testInterpolationSystemProperties(config);
+ }
+
+ /**
+ * Tests interpolation of constant values.
+ */
+ @Test
+ public void testInterpolationConstants()
+ {
+ InterpolationTestHelper.testInterpolationConstants(config);
+ }
+
+ /**
+ * Tests whether a variable can be escaped, so that it won't be
+ * interpolated.
+ */
+ @Test
+ public void testInterpolationEscaped()
+ {
+ InterpolationTestHelper.testInterpolationEscaped(config);
+ }
+
+ /**
+ * Tests accessing and manipulating the interpolator object.
+ */
+ @Test
+ public void testGetInterpolator()
+ {
+ InterpolationTestHelper.testGetInterpolator(config);
+ }
+
+ /**
+ * Tests obtaining a configuration with all variables replaced by their
+ * actual values.
+ */
+ @Test
+ public void testInterpolatedConfiguration()
+ {
+ InterpolationTestHelper.testInterpolatedConfiguration(config);
+ }
+
+ @Test
+ public void testGetHexadecimalValue()
+ {
+ config.setProperty("number", "0xFF");
+ assertEquals("byte value", (byte) 0xFF, config.getByte("number"));
+
+ config.setProperty("number", "0xFFFF");
+ assertEquals("short value", (short) 0xFFFF, config.getShort("number"));
+
+ config.setProperty("number", "0xFFFFFFFF");
+ assertEquals("int value", 0xFFFFFFFF, config.getInt("number"));
+
+ config.setProperty("number", "0xFFFFFFFFFFFFFFFF");
+ assertEquals("long value", 0xFFFFFFFFFFFFFFFFL, config.getLong("number"));
+
+ assertEquals("long value", 0xFFFFFFFFFFFFFFFFL, config.getBigInteger("number").longValue());
+ }
+
+ @Test
+ public void testGetBinaryValue()
+ {
+ config.setProperty("number", "0b11111111");
+ assertEquals("byte value", (byte) 0xFF, config.getByte("number"));
+
+ config.setProperty("number", "0b1111111111111111");
+ assertEquals("short value", (short) 0xFFFF, config.getShort("number"));
+
+ config.setProperty("number", "0b11111111111111111111111111111111");
+ assertEquals("int value", 0xFFFFFFFF, config.getInt("number"));
+
+ config.setProperty("number", "0b1111111111111111111111111111111111111111111111111111111111111111");
+ assertEquals("long value", 0xFFFFFFFFFFFFFFFFL, config.getLong("number"));
+
+ assertEquals("long value", 0xFFFFFFFFFFFFFFFFL, config.getBigInteger("number").longValue());
+ }
+
+ @Test
+ public void testResolveContainerStore()
+ {
+ AbstractConfiguration config = new BaseConfiguration();
+
+ // array of objects
+ config.addPropertyDirect("array", new String[] { "foo", "bar" });
+
+ assertEquals("first element of the 'array' property", "foo", config.resolveContainerStore("array"));
+
+ // list of objects
+ List<Object> list = new ArrayList<Object>();
+ list.add("foo");
+ list.add("bar");
+ config.addPropertyDirect("list", list);
+
+ assertEquals("first element of the 'list' property", "foo", config.resolveContainerStore("list"));
+
+ // set of objects
+ Set<Object> set = new LinkedHashSet<Object>();
+ set.add("foo");
+ set.add("bar");
+ config.addPropertyDirect("set", set);
+
+ assertEquals("first element of the 'set' property", "foo", config.resolveContainerStore("set"));
+
+ // arrays of primitives
+ config.addPropertyDirect("array.boolean", new boolean[] { true, false });
+ assertEquals("first element of the 'array.boolean' property", true, config.getBoolean("array.boolean"));
+
+ config.addPropertyDirect("array.byte", new byte[] { 1, 2 });
+ assertEquals("first element of the 'array.byte' property", 1, config.getByte("array.byte"));
+
+ config.addPropertyDirect("array.short", new short[] { 1, 2 });
+ assertEquals("first element of the 'array.short' property", 1, config.getShort("array.short"));
+
+ config.addPropertyDirect("array.int", new int[] { 1, 2 });
+ assertEquals("first element of the 'array.int' property", 1, config.getInt("array.int"));
+
+ config.addPropertyDirect("array.long", new long[] { 1, 2 });
+ assertEquals("first element of the 'array.long' property", 1, config.getLong("array.long"));
+
+ config.addPropertyDirect("array.float", new float[] { 1, 2 });
+ assertEquals("first element of the 'array.float' property", 1, config.getFloat("array.float"), 0);
+
+ config.addPropertyDirect("array.double", new double[] { 1, 2 });
+ assertEquals("first element of the 'array.double' property", 1, config.getDouble("array.double"), 0);
+ }
+
+ /**
+ * Tests if conversion between number types is possible.
+ */
+ @Test
+ public void testNumberConversions()
+ {
+ config.setProperty(KEY_NUMBER, new Integer(42));
+ assertEquals("Wrong int returned", 42, config.getInt(KEY_NUMBER));
+ assertEquals("Wrong long returned", 42L, config.getLong(KEY_NUMBER));
+ assertEquals("Wrong byte returned", (byte) 42, config
+ .getByte(KEY_NUMBER));
+ assertEquals("Wrong float returned", 42.0f,
+ config.getFloat(KEY_NUMBER), 0.01f);
+ assertEquals("Wrong double returned", 42.0, config
+ .getDouble(KEY_NUMBER), 0.001);
+
+ assertEquals("Wrong Long returned", new Long(42L), config.getLong(
+ KEY_NUMBER, null));
+ assertEquals("Wrong BigInt returned", new BigInteger("42"), config
+ .getBigInteger(KEY_NUMBER));
+ assertEquals("Wrong DigDecimal returned", new BigDecimal("42"), config
+ .getBigDecimal(KEY_NUMBER));
+ }
+
+ /**
+ * Tests cloning a BaseConfiguration.
+ */
+ @Test
+ public void testClone()
+ {
+ for (int i = 0; i < 10; i++)
+ {
+ config.addProperty("key" + i, new Integer(i));
+ }
+ BaseConfiguration config2 = (BaseConfiguration) config.clone();
+
+ for (Iterator<String> it = config.getKeys(); it.hasNext();)
+ {
+ String key = it.next();
+ assertTrue("Key not found: " + key, config2.containsKey(key));
+ assertEquals("Wrong value for key " + key, config.getProperty(key),
+ config2.getProperty(key));
+ }
+ }
+
+ /**
+ * Tests whether a cloned configuration is decoupled from its original.
+ */
+ @Test
+ public void testCloneModify()
+ {
+ ConfigurationListener l = new ConfigurationListener()
+ {
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ // just a dummy
+ }
+ };
+ config.addConfigurationListener(l);
+ config.addProperty("original", Boolean.TRUE);
+ BaseConfiguration config2 = (BaseConfiguration) config.clone();
+
+ config2.addProperty("clone", Boolean.TRUE);
+ assertFalse("New key appears in original", config.containsKey("clone"));
+ config2.setProperty("original", Boolean.FALSE);
+ assertTrue("Wrong value of original property", config
+ .getBoolean("original"));
+
+ assertEquals("Event listener was copied", 0, config2
+ .getConfigurationListeners().size());
+ }
+
+ /**
+ * Tests the clone() method if a list property is involved.
+ */
+ @Test
+ public void testCloneListProperty()
+ {
+ final String key = "list";
+ config.addProperty(key, "value1");
+ config.addProperty(key, "value2");
+ BaseConfiguration config2 = (BaseConfiguration) config.clone();
+ config2.addProperty(key, "value3");
+ assertEquals("Wrong number of original properties", 2, config.getList(
+ key).size());
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestBaseConfigurationXMLReader.java b/src/test/java/org/apache/commons/configuration/TestBaseConfigurationXMLReader.java
new file mode 100644
index 0000000..fb35562
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestBaseConfigurationXMLReader.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Iterator;
+
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMResult;
+import javax.xml.transform.sax.SAXSource;
+
+import org.apache.commons.jxpath.JXPathContext;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Test class for BaseConfigurationXMLReader.
+ *
+ * @version $Id: TestBaseConfigurationXMLReader.java 1222835 2011-12-23 20:37:22Z oheger $
+ */
+public class TestBaseConfigurationXMLReader
+{
+ private static final String[] CONTINENTS =
+ {
+ "Africa", "America", "Asia", "Australia", "Europe"
+ };
+
+ private BaseConfiguration config;
+ private BaseConfigurationXMLReader configReader;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ config = new BaseConfiguration();
+ config.addProperty("world.continents.continent", Arrays.asList(CONTINENTS));
+ config.addProperty("world.greeting", "Hello");
+ config.addProperty("world.greeting", "Salute");
+ config.addProperty("world.wish", "Peace");
+ config.addProperty("application.mail.smtp", "smtp.mymail.org");
+ config.addProperty("application.mail.pop", "pop3.mymail.org");
+ config.addProperty("application.mail.account.type", "pop3");
+ config.addProperty("application.mail.account.user", "postmaster");
+ config.addProperty("application.mail.account.pwd", "?.-gulp*#");
+ config.addProperty("application.mail.timeout", new Integer(42));
+ config.addProperty("test", Boolean.TRUE);
+
+ configReader = new BaseConfigurationXMLReader(config);
+ }
+
+ @Test
+ public void testParse() throws Exception
+ {
+ checkDocument(configReader, "config");
+ }
+
+ @Test(expected = SAXException.class)
+ public void testParseSAXException() throws IOException, SAXException
+ {
+ configReader.setContentHandler(new TestContentHandler());
+ configReader.parse("systemID");
+ }
+
+ @Test(expected = IOException.class)
+ public void testParseIOException() throws SAXException, IOException
+ {
+ BaseConfigurationXMLReader reader = new BaseConfigurationXMLReader();
+ reader.parse("document");
+ }
+
+ @Test
+ public void testSetRootName() throws Exception
+ {
+ BaseConfigurationXMLReader reader = new BaseConfigurationXMLReader(config);
+ reader.setRootName("apache");
+ checkDocument(reader, "apache");
+ }
+
+ private void checkDocument(BaseConfigurationXMLReader creader,
+ String rootName) throws Exception
+ {
+ SAXSource source = new SAXSource(creader, new InputSource());
+ DOMResult result = new DOMResult();
+ Transformer trans = TransformerFactory.newInstance().newTransformer();
+ trans.transform(source, result);
+ Node root = ((Document) result.getNode()).getDocumentElement();
+ JXPathContext ctx = JXPathContext.newContext(root);
+
+ assertEquals("Wrong root name", rootName, root.getNodeName());
+ assertEquals("Wrong number of children", 3, ctx.selectNodes("/*").size());
+
+ check(ctx, "world/continents/continent", CONTINENTS);
+ check(ctx, "world/greeting", new String[] { "Hello", "Salute" });
+ check(ctx, "world/wish", "Peace");
+ check(ctx, "application/mail/smtp", "smtp.mymail.org");
+ check(ctx, "application/mail/timeout", "42");
+ check(ctx, "application/mail/account/type", "pop3");
+ check(ctx, "application/mail/account/user", "postmaster");
+ check(ctx, "test", "true");
+ }
+
+ /**
+ * Helper method for checking values in the created document.
+ *
+ * @param ctx the JXPath context
+ * @param path the path to be checked
+ * @param values the expected element values
+ */
+ private void check(JXPathContext ctx, String path, String[] values)
+ {
+ Iterator<?> it = ctx.iterate(path);
+ for (int i = 0; i < values.length; i++)
+ {
+ assertTrue("Too few values", it.hasNext());
+ assertEquals("Wrong property value", values[i], it.next());
+ }
+ assertFalse("Too many values", it.hasNext());
+ }
+
+ private void check(JXPathContext ctx, String path, String value)
+ {
+ check(ctx, path, new String[]
+ { value });
+ }
+
+ // A ContentHandler that raises an exception
+ private static class TestContentHandler extends DefaultHandler
+ {
+ @Override
+ public void characters(char[] ch, int start, int length)
+ throws SAXException
+ {
+ throw new SAXException("Test exception during parsing");
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestBaseNullConfiguration.java b/src/test/java/org/apache/commons/configuration/TestBaseNullConfiguration.java
new file mode 100644
index 0000000..e6e7ab6
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestBaseNullConfiguration.java
@@ -0,0 +1,434 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests some basic functions of the BaseConfiguration class. Missing keys might
+ * return null.
+ *
+ * @version $Id: TestBaseNullConfiguration.java 1222842 2011-12-23 20:56:58Z oheger $
+ */
+public class TestBaseNullConfiguration
+{
+ protected BaseConfiguration config;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ config = new BaseConfiguration();
+ config.setThrowExceptionOnMissing(false);
+ }
+
+ @Test
+ public void testThrowExceptionOnMissing()
+ {
+ assertFalse("Throw Exception Property is set!", config.isThrowExceptionOnMissing());
+ }
+
+ @Test
+ public void testGetProperty()
+ {
+ /* should be empty and return null */
+ assertEquals("This returns null", config.getProperty("foo"), null);
+
+ /* add a real value, and get it two different ways */
+ config.setProperty("number", "1");
+ assertEquals("This returns '1'", config.getProperty("number"), "1");
+ assertEquals("This returns '1'", config.getString("number"), "1");
+ }
+
+ @Test
+ public void testGetByte()
+ {
+ config.setProperty("number", "1");
+ byte oneB = 1;
+ byte twoB = 2;
+ assertEquals("This returns 1(byte)", oneB, config.getByte("number"));
+ assertEquals("This returns 1(byte)", oneB, config.getByte("number", twoB));
+ assertEquals("This returns 2(default byte)", twoB, config.getByte("numberNotInConfig", twoB));
+ assertEquals("This returns 1(Byte)", new Byte(oneB), config.getByte("number", new Byte("2")));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetByteUnknown()
+ {
+ config.getByte("numberNotInConfig");
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetByteIncompatibleType()
+ {
+ config.setProperty("test.empty", "");
+ config.getByte("test.empty");
+ }
+
+ @Test
+ public void testGetShort()
+ {
+ config.setProperty("numberS", "1");
+ short oneS = 1;
+ short twoS = 2;
+ assertEquals("This returns 1(short)", oneS, config.getShort("numberS"));
+ assertEquals("This returns 1(short)", oneS, config.getShort("numberS", twoS));
+ assertEquals("This returns 2(default short)", twoS, config.getShort("numberNotInConfig", twoS));
+ assertEquals("This returns 1(Short)", new Short(oneS), config.getShort("numberS", new Short("2")));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetShortUnknown()
+ {
+ config.getShort("numberNotInConfig");
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetShortIncompatibleType()
+ {
+ config.setProperty("test.empty", "");
+ config.getShort("test.empty");
+ }
+
+ @Test
+ public void testGetLong()
+ {
+ config.setProperty("numberL", "1");
+ long oneL = 1;
+ long twoL = 2;
+ assertEquals("This returns 1(long)", oneL, config.getLong("numberL"));
+ assertEquals("This returns 1(long)", oneL, config.getLong("numberL", twoL));
+ assertEquals("This returns 2(default long)", twoL, config.getLong("numberNotInConfig", twoL));
+ assertEquals("This returns 1(Long)", new Long(oneL), config.getLong("numberL", new Long("2")));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetLongUnknown()
+ {
+ config.getLong("numberNotInConfig");
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetLongIncompatibleTypes()
+ {
+ config.setProperty("test.empty", "");
+ config.getLong("test.empty");
+ }
+
+ @Test
+ public void testGetFloat()
+ {
+ config.setProperty("numberF", "1.0");
+ float oneF = 1;
+ float twoF = 2;
+ assertEquals("This returns 1(float)", oneF, config.getFloat("numberF"), 0);
+ assertEquals("This returns 1(float)", oneF, config.getFloat("numberF", twoF), 0);
+ assertEquals("This returns 2(default float)", twoF, config.getFloat("numberNotInConfig", twoF), 0);
+ assertEquals("This returns 1(Float)", new Float(oneF), config.getFloat("numberF", new Float("2")));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetFloatUnknown()
+ {
+ config.getFloat("numberNotInConfig");
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetFloatIncompatibleType()
+ {
+ config.setProperty("test.empty", "");
+ config.getFloat("test.empty");
+ }
+
+ @Test
+ public void testGetDouble()
+ {
+ config.setProperty("numberD", "1.0");
+ double oneD = 1;
+ double twoD = 2;
+ assertEquals("This returns 1(double)", oneD, config.getDouble("numberD"), 0);
+ assertEquals("This returns 1(double)", oneD, config.getDouble("numberD", twoD), 0);
+ assertEquals("This returns 2(default double)", twoD, config.getDouble("numberNotInConfig", twoD), 0);
+ assertEquals("This returns 1(Double)", new Double(oneD), config.getDouble("numberD", new Double("2")));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetDoubleUnknown()
+ {
+ config.getDouble("numberNotInConfig");
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetDoubleIncompatibleType()
+ {
+ config.setProperty("test.empty", "");
+ config.getDouble("test.empty");
+ }
+
+ @Test
+ public void testGetBigDecimal()
+ {
+ config.setProperty("numberBigD", "123.456");
+ BigDecimal number = new BigDecimal("123.456");
+ BigDecimal defaultValue = new BigDecimal("654.321");
+
+ assertEquals("Existing key", number, config.getBigDecimal("numberBigD"));
+ assertEquals("Existing key with default value", number, config.getBigDecimal("numberBigD", defaultValue));
+ assertEquals("Missing key with default value", defaultValue, config.getBigDecimal("numberNotInConfig", defaultValue));
+ }
+
+ @Test
+ public void testGetBigDecimalUnknown()
+ {
+ assertNull("Missing Key is not null!", config.getBigDecimal("numberNotInConfig"));
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetBigDecimalIncompatibleType()
+ {
+ config.setProperty("test.empty", "");
+ config.getBigDecimal("test.empty");
+ }
+
+ @Test
+ public void testGetBigInteger()
+ {
+ config.setProperty("numberBigI", "1234567890");
+ BigInteger number = new BigInteger("1234567890");
+ BigInteger defaultValue = new BigInteger("654321");
+
+ assertEquals("Existing key", number, config.getBigInteger("numberBigI"));
+ assertEquals("Existing key with default value", number, config.getBigInteger("numberBigI", defaultValue));
+ assertEquals("Missing key with default value", defaultValue, config.getBigInteger("numberNotInConfig", defaultValue));
+ }
+
+ @Test
+ public void testGetBigIntegerUnknown()
+ {
+ assertNull("Missing Key is not null!", config.getBigInteger("numberNotInConfig"));
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetBigIntegerIncompatibleType()
+ {
+ config.setProperty("test.empty", "");
+ config.getBigInteger("test.empty");
+ }
+
+
+ @Test
+ public void testGetString()
+ {
+ config.setProperty("testString", "The quick brown fox");
+ String string = new String("The quick brown fox");
+ String defaultValue = new String("jumps over the lazy dog");
+
+ assertEquals("Existing key", string, config.getString("testString"));
+ assertEquals("Existing key with default value", string, config.getString("testString", defaultValue));
+ assertEquals("Missing key with default value", defaultValue, config.getString("stringNotInConfig", defaultValue));
+ }
+
+ @Test
+ public void testGetStringUnknown()
+ {
+ assertNull("Missing Key is not null!", config.getString("stringNotInConfig"));
+ }
+
+ @Test
+ public void testGetBoolean()
+ {
+ config.setProperty("boolA", Boolean.TRUE);
+ boolean boolT = true, boolF = false;
+ assertEquals("This returns true", boolT, config.getBoolean("boolA"));
+ assertEquals("This returns true, not the default", boolT, config.getBoolean("boolA", boolF));
+ assertEquals("This returns false(default)", boolF, config.getBoolean("boolNotInConfig", boolF));
+ assertEquals("This returns true(Boolean)", new Boolean(boolT), config.getBoolean("boolA", new Boolean(boolF)));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetBooleanUnknown()
+ {
+ config.getBoolean("numberNotInConfig");
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetBooleanIncompatibleType()
+ {
+ config.setProperty("test.empty", "");
+ config.getBoolean("test.empty");
+ }
+
+ @Test
+ public void testGetList()
+ {
+ config.addProperty("number", "1");
+ config.addProperty("number", "2");
+ List<Object> list = config.getList("number");
+ assertNotNull("The list is null", list);
+ assertEquals("List size", 2, list.size());
+ assertTrue("The number 1 is missing from the list", list.contains("1"));
+ assertTrue("The number 2 is missing from the list", list.contains("2"));
+ }
+
+ @Test
+ public void testGetListAsScalar()
+ {
+ config.addProperty("number", "1");
+ config.addProperty("number", "2");
+ assertEquals("Wrong value", "1", config.getString("number"));
+ }
+
+ @Test
+ public void testCommaSeparatedString()
+ {
+ String prop = "hey, that's a test";
+ config.setProperty("prop.string", prop);
+ List<Object> list = config.getList("prop.string");
+ assertEquals("Wrong number of elements", 2, list.size());
+ assertEquals("Wrong element 1", "hey", list.get(0));
+ }
+
+ @Test
+ public void testCommaSeparatedStringEscaped()
+ {
+ String prop2 = "hey\\, that's a test";
+ config.clearProperty("prop.string");
+ config.setProperty("prop.string", prop2);
+ assertEquals("Wrong value", "hey, that's a test", config.getString("prop.string"));
+ }
+
+ @Test
+ public void testPropertyAccess()
+ {
+ config.clearProperty("prop.properties");
+ config.setProperty("prop.properties", "");
+ assertEquals(
+ "This returns an empty Properties object",
+ config.getProperties("prop.properties"),
+ new Properties());
+ config.clearProperty("prop.properties");
+ config.setProperty("prop.properties", "foo=bar, baz=moo, seal=clubber");
+
+ Properties p = new Properties();
+ p.setProperty("foo", "bar");
+ p.setProperty("baz", "moo");
+ p.setProperty("seal", "clubber");
+ assertEquals(
+ "This returns a filled in Properties object",
+ config.getProperties("prop.properties"),
+ p);
+ }
+
+ @Test
+ public void testSubset()
+ {
+ /*
+ * test subset : assure we don't reprocess the data elements
+ * when generating the subset
+ */
+
+ String prop = "hey, that's a test";
+ String prop2 = "hey\\, that's a test";
+ config.setProperty("prop.string", prop2);
+ config.setProperty("property.string", "hello");
+
+ Configuration subEprop = config.subset("prop");
+
+ assertEquals(
+ "Returns the full string",
+ prop,
+ subEprop.getString("string"));
+ assertEquals("Wrong list size", 1, subEprop.getList("string").size());
+
+ Iterator<String> it = subEprop.getKeys();
+ it.next();
+ assertFalse(it.hasNext());
+
+ subEprop = config.subset("prop.");
+ it = subEprop.getKeys();
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void testInterpolation() throws Exception
+ {
+ config.setProperty("applicationRoot", "/home/applicationRoot");
+ config.setProperty("db", "${applicationRoot}/db/hypersonic");
+ String unInterpolatedValue = "${applicationRoot2}/db/hypersonic";
+ config.setProperty("dbFailedInterpolate", unInterpolatedValue);
+ String dbProp = "/home/applicationRoot/db/hypersonic";
+
+ //construct a new config, using config as the defaults config for it.
+ BaseConfiguration superProp = config;
+
+ assertEquals(
+ "Checking interpolated variable",dbProp,
+ superProp.getString("db"));
+ assertEquals(
+ "lookup fails, leave variable as is",
+ superProp.getString("dbFailedInterpolate"),
+ unInterpolatedValue);
+
+ superProp.setProperty("arrayInt", "${applicationRoot}/1");
+ String[] arrayInt = superProp.getStringArray("arrayInt");
+ assertEquals(
+ "check first entry was interpolated",
+ "/home/applicationRoot/1",
+ arrayInt[0]);
+ }
+
+ @Test
+ public void testMultipleInterpolation() throws Exception
+ {
+ config.setProperty("test.base-level", "/base-level");
+ config.setProperty("test.first-level", "${test.base-level}/first-level");
+ config.setProperty(
+ "test.second-level",
+ "${test.first-level}/second-level");
+ config.setProperty(
+ "test.third-level",
+ "${test.second-level}/third-level");
+
+ String expectedValue =
+ "/base-level/first-level/second-level/third-level";
+
+ assertEquals(config.getString("test.third-level"), expectedValue);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testInterpolationLoop() throws Exception
+ {
+ config.setProperty("test.a", "${test.b}");
+ config.setProperty("test.b", "${test.a}");
+ config.getString("test.a");
+ }
+}
+
diff --git a/src/test/java/org/apache/commons/configuration/TestCatalogResolver.java b/src/test/java/org/apache/commons/configuration/TestCatalogResolver.java
new file mode 100644
index 0000000..3350d4a
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestCatalogResolver.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.commons.configuration.resolver.CatalogResolver;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for CatalogResolver.
+ *
+ * @version $Id: TestCatalogResolver.java 1223006 2011-12-24 19:41:41Z oheger $
+ */
+public class TestCatalogResolver
+{
+ private static final String CATALOG_FILES = "catalog.xml";
+ private static final String PUBLIC_FILE = "testResolver.xml";
+ private static final String REWRITE_SYSTEM_FILE = "test.properties.xml";
+ private static final String REWRITE_SCHEMA_FILE = "sample.xml";
+
+ private CatalogResolver resolver;
+ private XMLConfiguration config;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ resolver = new CatalogResolver();
+ resolver.setCatalogFiles(CATALOG_FILES);
+ // resolver.setDebug(true);
+ config = new XMLConfiguration();
+ config.setEntityResolver(resolver);
+ }
+
+ @After
+ public void tearDown() throws Exception
+ {
+ resolver = null;
+ config = null;
+ }
+
+ @Test
+ public void testPublic() throws Exception
+ {
+ config.setFileName(PUBLIC_FILE);
+ config.load();
+ }
+
+ @Test
+ public void testRewriteSystem() throws Exception
+ {
+ config.setFileName(REWRITE_SYSTEM_FILE);
+ config.load();
+ }
+
+ /**
+ * Tests that the schema can be resolved and that XMLConfiguration will
+ * validate the file using the schema.
+ * @throws Exception
+ */
+ @Test
+ public void testSchemaResolver() throws Exception
+ {
+ config.setFileName(REWRITE_SCHEMA_FILE);
+ config.setSchemaValidation(true);
+ config.load();
+ }
+
+ @Test
+ public void testDebug() throws Exception
+ {
+ resolver.setDebug(true);
+ // There is no really good way to check this except to do something
+ // that causes debug output.
+ }
+
+ @Test
+ public void testLogger() throws Exception
+ {
+ Log log = LogFactory.getLog(this.getClass());
+ resolver.setLogger(log);
+ assertNotNull("No Logger returned", resolver.getLogger());
+ assertTrue("Incorrect Logger", log == resolver.getLogger());
+ }
+
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestCombinedConfiguration.java b/src/test/java/org/apache/commons/configuration/TestCombinedConfiguration.java
new file mode 100644
index 0000000..79b32a9
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestCombinedConfiguration.java
@@ -0,0 +1,999 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.text.MessageFormat;
+import java.util.Collection;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import junit.framework.Assert;
+
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.configuration.reloading.FileAlwaysReloadingStrategy;
+import org.apache.commons.configuration.reloading.FileRandomReloadingStrategy;
+import org.apache.commons.configuration.tree.DefaultExpressionEngine;
+import org.apache.commons.configuration.tree.MergeCombiner;
+import org.apache.commons.configuration.tree.NodeCombiner;
+import org.apache.commons.configuration.tree.OverrideCombiner;
+import org.apache.commons.configuration.tree.UnionCombiner;
+import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Test class for CombinedConfiguration.
+ *
+ * @version $Id: TestCombinedConfiguration.java 1327061 2012-04-17 12:18:27Z rgoers $
+ */
+public class TestCombinedConfiguration
+{
+ /** Constant for the name of a sub configuration. */
+ private static final String TEST_NAME = "SUBCONFIG";
+
+ /** Constant for a test key. */
+ private static final String TEST_KEY = "test.value";
+
+ /** Constant for the name of the first child configuration.*/
+ private static final String CHILD1 = TEST_NAME + "1";
+
+ /** Constant for the name of the second child configuration.*/
+ private static final String CHILD2 = TEST_NAME + "2";
+
+ /** Constant for the name of the XML reload test file.*/
+ private static final String RELOAD_XML_NAME = "reload.xml";
+
+ /** Constant for the content of a XML reload test file.*/
+ private static final String RELOAD_XML_CONTENT = "<xml><xmlReload>{0}</xmlReload></xml>";
+
+ /** Constant for the name of the properties reload test file.*/
+ private static final String RELOAD_PROPS_NAME = "reload.properties";
+
+ /** Constant for the content of a properties reload test file.*/
+ private static final String RELOAD_PROPS_CONTENT = "propsReload = {0}";
+
+ /** Helper object for managing temporary files. */
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ /** The configuration to be tested. */
+ private CombinedConfiguration config;
+
+ /** The test event listener. */
+ private CombinedListener listener;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ config = new CombinedConfiguration();
+ listener = new CombinedListener();
+ config.addConfigurationListener(listener);
+ }
+
+ /**
+ * Tests accessing a newly created combined configuration.
+ */
+ @Test
+ public void testInit()
+ {
+ assertEquals("Already configurations contained", 0, config
+ .getNumberOfConfigurations());
+ assertTrue("Set of names is not empty", config.getConfigurationNames()
+ .isEmpty());
+ assertTrue("Wrong node combiner",
+ config.getNodeCombiner() instanceof UnionCombiner);
+ assertNull("Test config was found", config.getConfiguration(TEST_NAME));
+ assertFalse("Force reload check flag is set", config.isForceReloadCheck());
+ }
+
+ /**
+ * Tests adding a configuration (without further information).
+ */
+ @Test
+ public void testAddConfiguration()
+ {
+ AbstractConfiguration c = setUpTestConfiguration();
+ config.addConfiguration(c);
+ checkAddConfig(c);
+ assertEquals("Wrong number of configs", 1, config
+ .getNumberOfConfigurations());
+ assertTrue("Name list is not empty", config.getConfigurationNames()
+ .isEmpty());
+ assertSame("Added config not found", c, config.getConfiguration(0));
+ assertTrue("Wrong property value", config.getBoolean(TEST_KEY));
+ listener.checkEvent(1, 0);
+ }
+
+ /**
+ * Tests adding a configuration with a name.
+ */
+ @Test
+ public void testAddConfigurationWithName()
+ {
+ AbstractConfiguration c = setUpTestConfiguration();
+ config.addConfiguration(c, TEST_NAME);
+ checkAddConfig(c);
+ assertEquals("Wrong number of configs", 1, config
+ .getNumberOfConfigurations());
+ assertSame("Added config not found", c, config.getConfiguration(0));
+ assertSame("Added config not found by name", c, config
+ .getConfiguration(TEST_NAME));
+ Set<String> names = config.getConfigurationNames();
+ assertEquals("Wrong number of config names", 1, names.size());
+ assertTrue("Name not found", names.contains(TEST_NAME));
+ assertTrue("Wrong property value", config.getBoolean(TEST_KEY));
+ listener.checkEvent(1, 0);
+ }
+
+ /**
+ * Tests adding a configuration with a name when this name already exists.
+ * This should cause an exception.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testAddConfigurationWithNameTwice()
+ {
+ config.addConfiguration(setUpTestConfiguration(), TEST_NAME);
+ config.addConfiguration(setUpTestConfiguration(), TEST_NAME,
+ "prefix");
+ }
+
+ /**
+ * Tests adding a configuration and specifying an at position.
+ */
+ @Test
+ public void testAddConfigurationAt()
+ {
+ AbstractConfiguration c = setUpTestConfiguration();
+ config.addConfiguration(c, null, "my");
+ checkAddConfig(c);
+ assertTrue("Wrong property value", config.getBoolean("my." + TEST_KEY));
+ }
+
+ /**
+ * Tests adding a configuration with a complex at position. Here the at path
+ * contains a dot, which must be escaped.
+ */
+ @Test
+ public void testAddConfigurationComplexAt()
+ {
+ AbstractConfiguration c = setUpTestConfiguration();
+ config.addConfiguration(c, null, "This..is.a.complex");
+ checkAddConfig(c);
+ assertTrue("Wrong property value", config
+ .getBoolean("This..is.a.complex." + TEST_KEY));
+ }
+
+ /**
+ * Checks if a configuration was correctly added to the combined config.
+ *
+ * @param c the config to check
+ */
+ private void checkAddConfig(AbstractConfiguration c)
+ {
+ Collection<ConfigurationListener> listeners = c.getConfigurationListeners();
+ assertEquals("Wrong number of configuration listeners", 1, listeners
+ .size());
+ assertTrue("Combined config is no listener", listeners.contains(config));
+ }
+
+ /**
+ * Tests adding a null configuration. This should cause an exception to be
+ * thrown.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddNullConfiguration()
+ {
+ config.addConfiguration(null);
+ }
+
+ /**
+ * Tests accessing properties if no configurations have been added.
+ */
+ @Test
+ public void testAccessPropertyEmpty()
+ {
+ assertFalse("Found a key", config.containsKey(TEST_KEY));
+ assertNull("Key has a value", config.getString("test.comment"));
+ assertTrue("Config is not empty", config.isEmpty());
+ }
+
+ /**
+ * Tests accessing properties if multiple configurations have been added.
+ */
+ @Test
+ public void testAccessPropertyMulti()
+ {
+ config.addConfiguration(setUpTestConfiguration());
+ config.addConfiguration(setUpTestConfiguration(), null, "prefix1");
+ config.addConfiguration(setUpTestConfiguration(), null, "prefix2");
+ assertTrue("Prop1 not found", config.getBoolean(TEST_KEY));
+ assertTrue("Prop 2 not found", config.getBoolean("prefix1." + TEST_KEY));
+ assertTrue("Prop 3 not found", config.getBoolean("prefix2." + TEST_KEY));
+ assertFalse("Configuration is empty", config.isEmpty());
+ listener.checkEvent(3, 0);
+ }
+
+ /**
+ * Tests removing a configuration.
+ */
+ @Test
+ public void testRemoveConfiguration()
+ {
+ AbstractConfiguration c = setUpTestConfiguration();
+ config.addConfiguration(c);
+ checkAddConfig(c);
+ assertTrue("Config could not be removed", config.removeConfiguration(c));
+ checkRemoveConfig(c);
+ }
+
+ /**
+ * Tests removing a configuration by index.
+ */
+ @Test
+ public void testRemoveConfigurationAt()
+ {
+ AbstractConfiguration c = setUpTestConfiguration();
+ config.addConfiguration(c);
+ assertSame("Wrong config removed", c, config.removeConfigurationAt(0));
+ checkRemoveConfig(c);
+ }
+
+ /**
+ * Tests removing a configuration by name.
+ */
+ @Test
+ public void testRemoveConfigurationByName()
+ {
+ AbstractConfiguration c = setUpTestConfiguration();
+ config.addConfiguration(c, TEST_NAME);
+ assertSame("Wrong config removed", c, config
+ .removeConfiguration(TEST_NAME));
+ checkRemoveConfig(c);
+ }
+
+ /**
+ * Tests removing a configuration with a name.
+ */
+ @Test
+ public void testRemoveNamedConfiguration()
+ {
+ AbstractConfiguration c = setUpTestConfiguration();
+ config.addConfiguration(c, TEST_NAME);
+ config.removeConfiguration(c);
+ checkRemoveConfig(c);
+ }
+
+ /**
+ * Tests removing a named configuration by index.
+ */
+ @Test
+ public void testRemoveNamedConfigurationAt()
+ {
+ AbstractConfiguration c = setUpTestConfiguration();
+ config.addConfiguration(c, TEST_NAME);
+ assertSame("Wrong config removed", c, config.removeConfigurationAt(0));
+ checkRemoveConfig(c);
+ }
+
+ /**
+ * Tests removing a configuration that was not added prior.
+ */
+ @Test
+ public void testRemoveNonContainedConfiguration()
+ {
+ assertFalse("Could remove non contained config", config
+ .removeConfiguration(setUpTestConfiguration()));
+ listener.checkEvent(0, 0);
+ }
+
+ /**
+ * Tests removing a configuration by name, which is not contained.
+ */
+ @Test
+ public void testRemoveConfigurationByUnknownName()
+ {
+ assertNull("Could remove configuration by unknown name", config
+ .removeConfiguration("unknownName"));
+ listener.checkEvent(0, 0);
+ }
+
+ /**
+ * Tests whether a configuration was completely removed.
+ *
+ * @param c the removed configuration
+ */
+ private void checkRemoveConfig(AbstractConfiguration c)
+ {
+ assertTrue("Listener was not removed", c.getConfigurationListeners()
+ .isEmpty());
+ assertEquals("Wrong number of contained configs", 0, config
+ .getNumberOfConfigurations());
+ assertTrue("Name was not removed", config.getConfigurationNames()
+ .isEmpty());
+ listener.checkEvent(2, 0);
+ }
+
+ /**
+ * Tests if an update of a contained configuration leeds to an invalidation
+ * of the combined configuration.
+ */
+ @Test
+ public void testUpdateContainedConfiguration()
+ {
+ AbstractConfiguration c = setUpTestConfiguration();
+ config.addConfiguration(c);
+ c.addProperty("test.otherTest", "yes");
+ assertEquals("New property not found", "yes", config
+ .getString("test.otherTest"));
+ listener.checkEvent(2, 0);
+ }
+
+ /**
+ * Tests if setting a node combiner causes an invalidation.
+ */
+ @Test
+ public void testSetNodeCombiner()
+ {
+ NodeCombiner combiner = new UnionCombiner();
+ config.setNodeCombiner(combiner);
+ assertSame("Node combiner was not set", combiner, config
+ .getNodeCombiner());
+ listener.checkEvent(1, 0);
+ }
+
+ /**
+ * Tests setting a null node combiner. This should cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetNullNodeCombiner()
+ {
+ config.setNodeCombiner(null);
+ }
+
+ /**
+ * Tests cloning a combined configuration.
+ */
+ @Test
+ public void testClone()
+ {
+ config.addConfiguration(setUpTestConfiguration());
+ config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "conf2");
+ config.addConfiguration(new PropertiesConfiguration(), "props");
+
+ CombinedConfiguration cc2 = (CombinedConfiguration) config.clone();
+ assertEquals("Wrong number of contained configurations", config
+ .getNumberOfConfigurations(), cc2.getNumberOfConfigurations());
+ assertSame("Wrong node combiner", config.getNodeCombiner(), cc2
+ .getNodeCombiner());
+ assertEquals("Wrong number of names", config.getConfigurationNames()
+ .size(), cc2.getConfigurationNames().size());
+ assertTrue("Event listeners were cloned", cc2
+ .getConfigurationListeners().isEmpty());
+
+ StrictConfigurationComparator comp = new StrictConfigurationComparator();
+ for (int i = 0; i < config.getNumberOfConfigurations(); i++)
+ {
+ assertNotSame("Configuration at " + i + " was not cloned", config
+ .getConfiguration(i), cc2.getConfiguration(i));
+ assertEquals("Wrong config class at " + i, config.getConfiguration(
+ i).getClass(), cc2.getConfiguration(i).getClass());
+ assertTrue("Configs not equal at " + i, comp.compare(config
+ .getConfiguration(i), cc2.getConfiguration(i)));
+ }
+
+ assertTrue("Combined configs not equal", comp.compare(config, cc2));
+ }
+
+ /**
+ * Tests if the cloned configuration is decoupled from the original.
+ */
+ @Test
+ public void testCloneModify()
+ {
+ config.addConfiguration(setUpTestConfiguration(), TEST_NAME);
+ CombinedConfiguration cc2 = (CombinedConfiguration) config.clone();
+ assertTrue("Name is missing", cc2.getConfigurationNames().contains(
+ TEST_NAME));
+ cc2.removeConfiguration(TEST_NAME);
+ assertFalse("Names in original changed", config.getConfigurationNames()
+ .isEmpty());
+ }
+
+ /**
+ * Tests clearing a combined configuration. This should remove all contained
+ * configurations.
+ */
+ @Test
+ public void testClear()
+ {
+ config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "test");
+ config.addConfiguration(setUpTestConfiguration());
+
+ config.clear();
+ assertEquals("Still configs contained", 0, config
+ .getNumberOfConfigurations());
+ assertTrue("Still names contained", config.getConfigurationNames()
+ .isEmpty());
+ assertTrue("Config is not empty", config.isEmpty());
+
+ listener.checkEvent(3, 2);
+ }
+
+ /**
+ * Tests if file-based configurations can be reloaded.
+ */
+ @Test
+ public void testReloading() throws Exception
+ {
+ config.setForceReloadCheck(true);
+ File testXmlFile = writeReloadFile(RELOAD_XML_NAME, RELOAD_XML_CONTENT, 0);
+ File testPropsFile = writeReloadFile(RELOAD_PROPS_NAME, RELOAD_PROPS_CONTENT, 0);
+ XMLConfiguration c1 = new XMLConfiguration(testXmlFile);
+ c1.setReloadingStrategy(new FileAlwaysReloadingStrategy());
+ PropertiesConfiguration c2 = new PropertiesConfiguration(testPropsFile);
+ c2.setThrowExceptionOnMissing(true);
+ c2.setReloadingStrategy(new FileAlwaysReloadingStrategy());
+ config.addConfiguration(c1);
+ config.addConfiguration(c2);
+ assertEquals("Wrong xml reload value", 0, config.getInt("xmlReload"));
+ assertEquals("Wrong props reload value", 0, config
+ .getInt("propsReload"));
+
+ writeReloadFile(RELOAD_XML_NAME, RELOAD_XML_CONTENT, 1);
+ assertEquals("XML reload not detected", 1, config.getInt("xmlReload"));
+ config.setForceReloadCheck(false);
+ writeReloadFile(RELOAD_PROPS_NAME, RELOAD_PROPS_CONTENT, 1);
+ assertEquals("Props reload detected though check flag is false", 0, config
+ .getInt("propsReload"));
+ }
+
+ /**
+ * Tests whether the reload check works with a subnode configuration. This
+ * test is related to CONFIGURATION-341.
+ */
+ @Test
+ public void testReloadingSubnodeConfig() throws IOException,
+ ConfigurationException
+ {
+ config.setForceReloadCheck(true);
+ File testXmlFile = writeReloadFile(RELOAD_XML_NAME, RELOAD_XML_CONTENT,
+ 0);
+ XMLConfiguration c1 = new XMLConfiguration(testXmlFile);
+ c1.setReloadingStrategy(new FileAlwaysReloadingStrategy());
+ final String prefix = "reloadCheck";
+ config.addConfiguration(c1, CHILD1, prefix);
+ SubnodeConfiguration sub = config.configurationAt(prefix, true);
+ writeReloadFile(RELOAD_XML_NAME, RELOAD_XML_CONTENT, 1);
+ assertEquals("Reload not detected", 1, sub.getInt("xmlReload"));
+ }
+
+ /**
+ * Tests whether reloading works for a combined configuration nested in
+ * another combined configuration.
+ */
+ @Test
+ public void testReloadingNestedCC() throws IOException,
+ ConfigurationException
+ {
+ config.setForceReloadCheck(true);
+ File testXmlFile =
+ writeReloadFile(RELOAD_XML_NAME, RELOAD_XML_CONTENT, 0);
+ File testPropsFile =
+ writeReloadFile(RELOAD_PROPS_NAME, RELOAD_PROPS_CONTENT, 0);
+ XMLConfiguration c1 = new XMLConfiguration(testXmlFile);
+ c1.setReloadingStrategy(new FileAlwaysReloadingStrategy());
+ PropertiesConfiguration c2 = new PropertiesConfiguration(testPropsFile);
+ c2.setReloadingStrategy(new FileAlwaysReloadingStrategy());
+ config.addConfiguration(c2);
+ CombinedConfiguration cc2 = new CombinedConfiguration();
+ cc2.setForceReloadCheck(true);
+ cc2.addConfiguration(c1);
+ config.addConfiguration(cc2);
+ assertEquals("Wrong xml reload value", 0, config.getInt("xmlReload"));
+ writeReloadFile(RELOAD_XML_NAME, RELOAD_XML_CONTENT, 1);
+ assertEquals("XML reload not detected", 1, config.getInt("xmlReload"));
+ }
+
+ /**
+ * Prepares a test of the getSource() method.
+ */
+ private void setUpSourceTest()
+ {
+ HierarchicalConfiguration c1 = new HierarchicalConfiguration();
+ PropertiesConfiguration c2 = new PropertiesConfiguration();
+ c1.addProperty(TEST_KEY, TEST_NAME);
+ c2.addProperty("another.key", "test");
+ config.addConfiguration(c1, CHILD1);
+ config.addConfiguration(c2, CHILD2);
+ }
+
+ /**
+ * Tests the gestSource() method when the source property is defined in a
+ * hierarchical configuration.
+ */
+ @Test
+ public void testGetSourceHierarchical()
+ {
+ setUpSourceTest();
+ assertEquals("Wrong source configuration", config
+ .getConfiguration(CHILD1), config.getSource(TEST_KEY));
+ }
+
+ /**
+ * Tests whether the source configuration can be detected for non
+ * hierarchical configurations.
+ */
+ @Test
+ public void testGetSourceNonHierarchical()
+ {
+ setUpSourceTest();
+ assertEquals("Wrong source configuration", config
+ .getConfiguration(CHILD2), config.getSource("another.key"));
+ }
+
+ /**
+ * Tests the getSource() method when the passed in key is not contained.
+ * Result should be null in this case.
+ */
+ @Test
+ public void testGetSourceUnknown()
+ {
+ setUpSourceTest();
+ assertNull("Wrong result for unknown key", config
+ .getSource("an.unknown.key"));
+ }
+
+ /**
+ * Tests the getSource() method when a null key is passed in. This should
+ * cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetSourceNull()
+ {
+ config.getSource(null);
+ }
+
+ /**
+ * Tests the getSource() method when the passed in key belongs to the
+ * combined configuration itself.
+ */
+ @Test
+ public void testGetSourceCombined()
+ {
+ setUpSourceTest();
+ final String key = "yet.another.key";
+ config.addProperty(key, Boolean.TRUE);
+ assertEquals("Wrong source for key", config, config.getSource(key));
+ }
+
+ /**
+ * Tests the getSource() method when the passed in key refers to multiple
+ * values, which are all defined in the same source configuration.
+ */
+ @Test
+ public void testGetSourceMulti()
+ {
+ setUpSourceTest();
+ final String key = "list.key";
+ config.getConfiguration(CHILD1).addProperty(key, "1,2,3");
+ assertEquals("Wrong source for multi-value property", config
+ .getConfiguration(CHILD1), config.getSource(key));
+ }
+
+ /**
+ * Tests the getSource() method when the passed in key refers to multiple
+ * values defined by different sources. This should cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetSourceMultiSources()
+ {
+ setUpSourceTest();
+ final String key = "list.key";
+ config.getConfiguration(CHILD1).addProperty(key, "1,2,3");
+ config.getConfiguration(CHILD2).addProperty(key, "a,b,c");
+ config.getSource(key);
+ }
+
+ /**
+ * Tests whether escaped list delimiters are treated correctly.
+ */
+ @Test
+ public void testEscapeListDelimiters()
+ {
+ PropertiesConfiguration sub = new PropertiesConfiguration();
+ sub.addProperty("test.pi", "3\\,1415");
+ config.addConfiguration(sub);
+ assertEquals("Wrong value", "3,1415", config.getString("test.pi"));
+ }
+
+ /**
+ * Tests whether an invalidate event is fired only after a change. This test
+ * is related to CONFIGURATION-315.
+ */
+ @Test
+ public void testInvalidateAfterChange()
+ {
+ ConfigurationEvent event = new ConfigurationEvent(config, 0, null,
+ null, true);
+ config.configurationChanged(event);
+ assertEquals("Invalidate event fired", 0, listener.invalidateEvents);
+ event = new ConfigurationEvent(config, 0, null, null, false);
+ config.configurationChanged(event);
+ assertEquals("No invalidate event fired", 1, listener.invalidateEvents);
+ }
+
+ /**
+ * Tests using a conversion expression engine for child configurations with
+ * strange keys. This test is related to CONFIGURATION-336.
+ */
+ @Test
+ public void testConversionExpressionEngine()
+ {
+ PropertiesConfiguration child = new PropertiesConfiguration();
+ child.addProperty("test(a)", "1,2,3");
+ config.addConfiguration(child);
+ DefaultExpressionEngine engineQuery = new DefaultExpressionEngine();
+ engineQuery.setIndexStart("<");
+ engineQuery.setIndexEnd(">");
+ config.setExpressionEngine(engineQuery);
+ DefaultExpressionEngine engineConvert = new DefaultExpressionEngine();
+ engineConvert.setIndexStart("[");
+ engineConvert.setIndexEnd("]");
+ config.setConversionExpressionEngine(engineConvert);
+ assertEquals("Wrong property 1", "1", config.getString("test(a)<0>"));
+ assertEquals("Wrong property 2", "2", config.getString("test(a)<1>"));
+ assertEquals("Wrong property 3", "3", config.getString("test(a)<2>"));
+ }
+
+ /**
+ * Tests whether reload operations can cause a deadlock when the combined
+ * configuration is accessed concurrently. This test is related to
+ * CONFIGURATION-344.
+ */
+ @Test
+ public void testDeadlockWithReload() throws ConfigurationException,
+ InterruptedException
+ {
+ final PropertiesConfiguration child = new PropertiesConfiguration(
+ "test.properties");
+ child.setReloadingStrategy(new FileAlwaysReloadingStrategy());
+ config.addConfiguration(child);
+ final int count = 1000;
+
+ class TestDeadlockReloadThread extends Thread
+ {
+ boolean error = false;
+
+ @Override
+ public void run()
+ {
+ for (int i = 0; i < count && !error; i++)
+ {
+ try
+ {
+ if (!child.getBoolean("configuration.loaded"))
+ {
+ error = true;
+ }
+ }
+ catch (NoSuchElementException nsex)
+ {
+ error = true;
+ }
+ }
+ }
+ }
+
+ TestDeadlockReloadThread reloadThread = new TestDeadlockReloadThread();
+ reloadThread.start();
+ for (int i = 0; i < count; i++)
+ {
+ assertEquals("Wrong value of combined property", 10, config
+ .getInt("test.integer"));
+ }
+ reloadThread.join();
+ assertFalse("Failure in thread", reloadThread.error);
+ }
+
+ @Test
+ public void testGetConfigurations() throws Exception
+ {
+ config.addConfiguration(setUpTestConfiguration());
+ config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "conf2");
+ AbstractConfiguration pc = new PropertiesConfiguration();
+ config.addConfiguration(pc, "props");
+ List<AbstractConfiguration> list = config.getConfigurations();
+ assertNotNull("No list of configurations returned", list);
+ assertTrue("Incorrect number of configurations", list.size() == 3);
+ AbstractConfiguration c = list.get(2);
+ assertTrue("Incorrect configuration", c == pc);
+ }
+
+ @Test
+ public void testGetConfigurationNameList() throws Exception
+ {
+ config.addConfiguration(setUpTestConfiguration());
+ config.addConfiguration(setUpTestConfiguration(), TEST_NAME, "conf2");
+ AbstractConfiguration pc = new PropertiesConfiguration();
+ config.addConfiguration(pc, "props");
+ List<String> list = config.getConfigurationNameList();
+ assertNotNull("No list of configurations returned", list);
+ assertTrue("Incorrect number of configurations", list.size() == 3);
+ String name = list.get(1);
+ assertNotNull("No name returned", name);
+ assertTrue("Incorrect configuration name", TEST_NAME.equals(name));
+ }
+
+ /**
+ * Tests whether changes on a sub node configuration that is part of a
+ * combined configuration are detected. This test is related to
+ * CONFIGURATION-368.
+ */
+ @Test
+ public void testReloadWithSubNodeConfig() throws Exception
+ {
+ final String reloadContent = "<config><default><xmlReload1>{0}</xmlReload1></default></config>";
+ config.setForceReloadCheck(true);
+ config.setNodeCombiner(new OverrideCombiner());
+ File testXmlFile1 = writeReloadFile(RELOAD_XML_NAME, reloadContent, 0);
+ final String prefix1 = "default";
+ XMLConfiguration c1 = new XMLConfiguration(testXmlFile1);
+ SubnodeConfiguration sub1 = c1.configurationAt(prefix1, true);
+ assertEquals("Inital test for sub config 1 failed", 0, sub1
+ .getInt("xmlReload1"));
+ config.addConfiguration(sub1);
+ assertEquals(
+ "Could not get value for sub config 1 from combined config", 0,
+ config.getInt("xmlReload1"));
+ c1.setReloadingStrategy(new FileAlwaysReloadingStrategy());
+ writeReloadFile(RELOAD_XML_NAME, reloadContent, 1);
+ assertEquals("Reload of sub config 1 not detected", 1, config
+ .getInt("xmlReload1"));
+ }
+
+ @Test
+ public void testConcurrentGetAndReload() throws Exception
+ {
+ final int threadCount = 5;
+ final int loopCount = 1000;
+ config.setForceReloadCheck(true);
+ config.setNodeCombiner(new MergeCombiner());
+ final XMLConfiguration xml = new XMLConfiguration("configA.xml");
+ xml.setReloadingStrategy(new FileRandomReloadingStrategy());
+ config.addConfiguration(xml);
+ final XMLConfiguration xml2 = new XMLConfiguration("configB.xml");
+ xml2.setReloadingStrategy(new FileRandomReloadingStrategy());
+ config.addConfiguration(xml2);
+ config.setExpressionEngine(new XPathExpressionEngine());
+
+ assertEquals(config.getString("/property[@name='config']/@value"), "100");
+
+ Thread testThreads[] = new Thread[threadCount];
+ int failures[] = new int[threadCount];
+
+ for (int i = 0; i < testThreads.length; ++i)
+ {
+ testThreads[i] = new ReloadThread(config, failures, i, loopCount);
+ testThreads[i].start();
+ }
+
+ int totalFailures = 0;
+ for (int i = 0; i < testThreads.length; ++i)
+ {
+ testThreads[i].join();
+ totalFailures += failures[i];
+ }
+ assertTrue(totalFailures + " failures Occurred", totalFailures == 0);
+ }
+
+ /**
+ * Tests whether a combined configuration can be copied to an XML
+ * configuration. This test is related to CONFIGURATION-445.
+ */
+ @Test
+ public void testCombinedCopyToXML() throws ConfigurationException
+ {
+ XMLConfiguration x1 = new XMLConfiguration();
+ x1.addProperty("key1", "value1");
+ x1.addProperty("key1[@override]", "USER1");
+ x1.addProperty("key2", "value2");
+ x1.addProperty("key2[@override]", "USER2");
+ XMLConfiguration x2 = new XMLConfiguration();
+ x2.addProperty("key2", "value2.2");
+ x2.addProperty("key2[@override]", "USER2");
+ config.setNodeCombiner(new OverrideCombiner());
+ config.addConfiguration(x2);
+ config.addConfiguration(x1);
+ XMLConfiguration x3 = new XMLConfiguration(config);
+ assertEquals("Wrong element value", "value2.2", x3.getString("key2"));
+ assertEquals("Wrong attribute value", "USER2",
+ x3.getString("key2[@override]"));
+ StringWriter w = new StringWriter();
+ x3.save(w);
+ String s = w.toString();
+ x3 = new XMLConfiguration();
+ x3.load(new StringReader(s));
+ assertEquals("Wrong element value after load", "value2.2",
+ x3.getString("key2"));
+ assertEquals("Wrong attribute value after load", "USER2",
+ x3.getString("key2[@override]"));
+ }
+
+ private class ReloadThread extends Thread
+ {
+ CombinedConfiguration combined;
+ int[] failures;
+ int index;
+ int count;
+
+ ReloadThread(CombinedConfiguration config, int[] failures, int index, int count)
+ {
+ combined = config;
+ this.failures = failures;
+ this.index = index;
+ this.count = count;
+ }
+ @Override
+ public void run()
+ {
+ failures[index] = 0;
+ for (int i = 0; i < count; i++)
+ {
+ try
+ {
+ String value = combined.getString("/property[@name='config']/@value");
+ if (value == null || !value.equals("100"))
+ {
+ ++failures[index];
+ }
+ }
+ catch (Exception ex)
+ {
+ ++failures[index];
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper method for writing a file. The file is also added to a list and
+ * will be deleted in teadDown() automatically.
+ *
+ * @param file the file to be written
+ * @param content the file's content
+ * @throws IOException if an error occurs
+ */
+ private void writeFile(File file, String content) throws IOException
+ {
+ PrintWriter out = null;
+ try
+ {
+ out = new PrintWriter(new FileWriter(file));
+ out.print(content);
+ }
+ finally
+ {
+ if (out != null)
+ {
+ out.close();
+ }
+ }
+ }
+
+ /**
+ * Helper method for writing a test file. The file will be created in the
+ * test directory. It is also scheduled for automatic deletion after the
+ * test.
+ *
+ * @param fileName the name of the test file
+ * @param content the content of the file
+ * @return the <code>File</code> object for the test file
+ * @throws IOException if an error occurs
+ */
+ private File writeFile(String fileName, String content) throws IOException
+ {
+ File file = new File(folder.getRoot(), fileName);
+ writeFile(file, content);
+ return file;
+ }
+
+ /**
+ * Writes a file for testing reload operations.
+ *
+ * @param name the name of the reload test file
+ * @param content the content of the file
+ * @param value the value of the reload test property
+ * @return the file that was written
+ * @throws IOException if an error occurs
+ */
+ private File writeReloadFile(String name, String content, int value)
+ throws IOException
+ {
+ return writeFile(name, MessageFormat.format(content, new Object[] {
+ new Integer(value)
+ }));
+ }
+
+ /**
+ * Helper method for creating a test configuration to be added to the
+ * combined configuration.
+ *
+ * @return the test configuration
+ */
+ private AbstractConfiguration setUpTestConfiguration()
+ {
+ HierarchicalConfiguration config = new HierarchicalConfiguration();
+ config.addProperty(TEST_KEY, Boolean.TRUE);
+ config.addProperty("test.comment", "This is a test");
+ return config;
+ }
+
+ /**
+ * Test event listener class for checking if the expected invalidate events
+ * are fired.
+ */
+ static class CombinedListener implements ConfigurationListener
+ {
+ int invalidateEvents;
+
+ int otherEvents;
+
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ if (event.getType() == CombinedConfiguration.EVENT_COMBINED_INVALIDATE)
+ {
+ invalidateEvents++;
+ }
+ else
+ {
+ otherEvents++;
+ }
+ }
+
+ /**
+ * Checks if the expected number of events was fired.
+ *
+ * @param expectedInvalidate the expected number of invalidate events
+ * @param expectedOthers the expected number of other events
+ */
+ public void checkEvent(int expectedInvalidate, int expectedOthers)
+ {
+ Assert.assertEquals("Wrong number of invalidate events",
+ expectedInvalidate, invalidateEvents);
+ Assert.assertEquals("Wrong number of other events", expectedOthers,
+ otherEvents);
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestCompositeConfiguration.java b/src/test/java/org/apache/commons/configuration/TestCompositeConfiguration.java
new file mode 100644
index 0000000..02b7006
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestCompositeConfiguration.java
@@ -0,0 +1,931 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.configuration.reloading.FileAlwaysReloadingStrategy;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test loading multiple configurations.
+ *
+ * @version $Id: TestCompositeConfiguration.java 1233058 2012-01-18 20:49:12Z oheger $
+ */
+public class TestCompositeConfiguration
+{
+ /** Constant for a test property to be checked.*/
+ private static final String TEST_PROPERTY = "test.source.property";
+
+ protected PropertiesConfiguration conf1;
+ protected PropertiesConfiguration conf2;
+ protected XMLConfiguration xmlConf;
+ protected CompositeConfiguration cc;
+
+ /**
+ * The File that we test with
+ */
+ private String testProperties = ConfigurationAssert.getTestFile("test.properties").getAbsolutePath();
+ private String testProperties2 = ConfigurationAssert.getTestFile("test2.properties").getAbsolutePath();
+ private String testPropertiesXML = ConfigurationAssert.getTestFile("test.xml").getAbsolutePath();
+
+ @Before
+ public void setUp() throws Exception
+ {
+ cc = new CompositeConfiguration();
+ conf1 = new PropertiesConfiguration(testProperties);
+ conf2 = new PropertiesConfiguration(testProperties2);
+ xmlConf = new XMLConfiguration(new File(testPropertiesXML));
+
+ cc.setThrowExceptionOnMissing(true);
+ }
+
+ @Test
+ public void testThrowExceptionOnMissing()
+ {
+ assertTrue("Throw Exception Property is not set!", cc.isThrowExceptionOnMissing());
+ }
+
+ @Test
+ public void testAddRemoveConfigurations() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ assertEquals("Number of configurations", 2, cc.getNumberOfConfigurations());
+ cc.addConfiguration(conf1);
+ assertEquals("Number of configurations", 2, cc.getNumberOfConfigurations());
+ cc.addConfiguration(conf2);
+ assertEquals("Number of configurations", 3, cc.getNumberOfConfigurations());
+ cc.removeConfiguration(conf1);
+ assertEquals("Number of configurations", 2, cc.getNumberOfConfigurations());
+ cc.clear();
+ assertEquals("Number of configurations", 1, cc.getNumberOfConfigurations());
+ }
+
+ @Test
+ public void testGetPropertyWIncludes() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(conf2);
+ List<Object> l = cc.getList("packages");
+ assertTrue(l.contains("packagea"));
+ }
+
+ @Test
+ public void testGetProperty() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(conf2);
+ assertEquals("Make sure we get the property from conf1 first", "test.properties", cc.getString("propertyInOrder"));
+ cc.clear();
+
+ cc.addConfiguration(conf2);
+ cc.addConfiguration(conf1);
+ assertEquals("Make sure we get the property from conf2 first", "test2.properties", cc.getString("propertyInOrder"));
+ }
+
+ @Test
+ public void testCantRemoveMemoryConfig() throws Exception
+ {
+ cc.clear();
+ assertEquals(1, cc.getNumberOfConfigurations());
+
+ Configuration internal = cc.getConfiguration(0);
+ cc.removeConfiguration(internal);
+
+ assertEquals(1, cc.getNumberOfConfigurations());
+ }
+
+ @Test
+ public void testGetPropertyMissing() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(conf2);
+ try
+ {
+ assertNull(cc.getString("bogus.property"));
+ fail("Should have thrown a NoSuchElementException");
+ }
+ catch (NoSuchElementException nsee)
+ {
+ assertTrue(nsee.getMessage().indexOf("bogus.property") > -1);
+ }
+
+ assertTrue("Should be false", !cc.getBoolean("test.missing.boolean", false));
+ assertTrue("Should be true", cc.getBoolean("test.missing.boolean.true", true));
+ }
+
+ /**
+ * Tests {@code List} parsing.
+ */
+ @Test
+ public void testMultipleTypesOfConfigs() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+ assertEquals("Make sure we get the property from conf1 first", 1, cc.getInt("test.short"));
+ cc.clear();
+
+ cc.addConfiguration(xmlConf);
+ cc.addConfiguration(conf1);
+ assertEquals("Make sure we get the property from xml", 8, cc.getInt("test.short"));
+ }
+
+ @Test
+ public void testPropertyExistsInOnlyOneConfig() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+ assertEquals("value", cc.getString("element"));
+ }
+
+ /**
+ * Tests getting a default when the key doesn't exist
+ */
+ @Test
+ public void testDefaultValueWhenKeyMissing() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+ assertEquals("default", cc.getString("bogus", "default"));
+ assertTrue(1.4 == cc.getDouble("bogus", 1.4));
+ assertTrue(1.4 == cc.getDouble("bogus", 1.4));
+ }
+
+ @Test
+ public void testGettingConfiguration() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+ assertEquals(PropertiesConfiguration.class, cc.getConfiguration(0).getClass());
+ assertEquals(XMLConfiguration.class, cc.getConfiguration(1).getClass());
+ }
+
+ /**
+ * Tests setting values. These are set in memory mode only!
+ */
+ @Test
+ public void testClearingProperty() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+ cc.clearProperty("test.short");
+ assertTrue("Make sure test.short is gone!", !cc.containsKey("test.short"));
+ }
+
+ /**
+ * Tests adding values. Make sure they _DON'T_ override any other properties but add to the
+ * existing properties and keep sequence
+ */
+ @Test
+ public void testAddingProperty() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+
+ String[] values = cc.getStringArray("test.short");
+
+ assertEquals("Number of values before add is wrong!", 1, values.length);
+ assertEquals("First Value before add is wrong", "1", values[0]);
+
+ cc.addProperty("test.short", "88");
+
+ values = cc.getStringArray("test.short");
+
+ assertEquals("Number of values is wrong!", 2, values.length);
+ assertEquals("First Value is wrong", "1", values[0]);
+ assertEquals("Third Value is wrong", "88", values[1]);
+ }
+
+ /**
+ * Tests setting values. These are set in memory mode only!
+ */
+ @Test
+ public void testSettingMissingProperty() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+ cc.setProperty("my.new.property", "supernew");
+ assertEquals("supernew", cc.getString("my.new.property"));
+ }
+
+ /**
+ * Tests retrieving subsets of configurations
+ */
+ @Test
+ public void testGettingSubset() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+
+ Configuration subset = cc.subset("test");
+ assertNotNull(subset);
+ assertFalse("Shouldn't be empty", subset.isEmpty());
+ assertEquals("Make sure the initial loaded configs subset overrides any later add configs subset", "1", subset.getString("short"));
+
+ cc.setProperty("test.short", "43");
+ subset = cc.subset("test");
+ assertEquals("Make sure the initial loaded configs subset overrides any later add configs subset", "43", subset.getString("short"));
+ }
+
+ /**
+ * Tests subsets and still can resolve elements
+ */
+ @Test
+ public void testSubsetCanResolve() throws Exception
+ {
+ cc = new CompositeConfiguration();
+ final BaseConfiguration config = new BaseConfiguration();
+ config.addProperty("subset.tempfile", "${java.io.tmpdir}/file.tmp");
+ cc.addConfiguration(config);
+ cc.addConfiguration(ConfigurationConverter.getConfiguration(System.getProperties()));
+
+ Configuration subset = cc.subset("subset");
+ assertEquals(System.getProperty("java.io.tmpdir") + "/file.tmp", subset.getString("tempfile"));
+ }
+
+ /**
+ * Tests {@code List} parsing.
+ */
+ @Test
+ public void testList() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+
+ List<Object> packages = cc.getList("packages");
+ // we should get 3 packages here
+ assertEquals(3, packages.size());
+
+ List<Object> defaultList = new ArrayList<Object>();
+ defaultList.add("1");
+ defaultList.add("2");
+
+ packages = cc.getList("packages.which.dont.exist", defaultList);
+ // we should get 2 packages here
+ assertEquals(2, packages.size());
+
+ }
+
+ /**
+ * Tests {@code String} array parsing.
+ */
+ @Test
+ public void testStringArray() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+
+ String[] packages = cc.getStringArray("packages");
+ // we should get 3 packages here
+ assertEquals(3, packages.length);
+
+ packages = cc.getStringArray("packages.which.dont.exist");
+ // we should get 0 packages here
+ assertEquals(0, packages.length);
+ }
+
+ @Test
+ public void testGetList()
+ {
+ Configuration conf1 = new BaseConfiguration();
+ conf1.addProperty("array", "value1");
+ conf1.addProperty("array", "value2");
+
+ Configuration conf2 = new BaseConfiguration();
+ conf2.addProperty("array", "value3");
+ conf2.addProperty("array", "value4");
+
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(conf2);
+
+ // check the composite 'array' property
+ List<Object> list = cc.getList("array");
+ assertNotNull("null list", list);
+ assertEquals("list size", 2, list.size());
+ assertTrue("'value1' not found in the list", list.contains("value1"));
+ assertTrue("'value2' not found in the list", list.contains("value2"));
+
+ // add an element to the list in the composite configuration
+ cc.addProperty("array", "value5");
+
+ // test the new list
+ list = cc.getList("array");
+ assertNotNull("null list", list);
+ assertEquals("list size", 3, list.size());
+ assertTrue("'value1' not found in the list", list.contains("value1"));
+ assertTrue("'value2' not found in the list", list.contains("value2"));
+ assertTrue("'value5' not found in the list", list.contains("value5"));
+ }
+
+ /**
+ * Tests {@code getKeys} preserves the order
+ */
+ @Test
+ public void testGetKeysPreservesOrder() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ List<String> orderedList = new ArrayList<String>();
+ for (Iterator<String> keys = conf1.getKeys(); keys.hasNext();)
+ {
+ orderedList.add(keys.next());
+ }
+ List<String> iteratedList = new ArrayList<String>();
+ for (Iterator<String> keys = cc.getKeys(); keys.hasNext();)
+ {
+ iteratedList.add(keys.next());
+ }
+ assertEquals(orderedList.size(), iteratedList.size());
+ for (int i = 0; i < orderedList.size(); i++)
+ {
+ assertEquals(orderedList.get(i), iteratedList.get(i));
+ }
+ }
+
+ /**
+ * Tests {@code getKeys(String key)} preserves the order
+ */
+ @Test
+ public void testGetKeys2PreservesOrder() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ List<String> orderedList = new ArrayList<String>();
+ for (Iterator<String> keys = conf1.getKeys("test"); keys.hasNext();)
+ {
+ orderedList.add(keys.next());
+ }
+ List<String> iteratedList = new ArrayList<String>();
+ for (Iterator<String> keys = cc.getKeys("test"); keys.hasNext();)
+ {
+ iteratedList.add(keys.next());
+ }
+ assertEquals(orderedList.size(), iteratedList.size());
+ for (int i = 0; i < orderedList.size(); i++)
+ {
+ assertEquals(orderedList.get(i), iteratedList.get(i));
+ }
+ }
+
+ @Test
+ public void testGetStringWithDefaults()
+ {
+ BaseConfiguration defaults = new BaseConfiguration();
+ defaults.addProperty("default", "default string");
+
+ CompositeConfiguration c = new CompositeConfiguration(defaults);
+ c.setThrowExceptionOnMissing(cc.isThrowExceptionOnMissing());
+ c.addProperty("string", "test string");
+
+ assertEquals("test string", c.getString("string"));
+ try
+ {
+ c.getString("XXX");
+ fail("Should throw NoSuchElementException exception");
+ }
+ catch (NoSuchElementException e)
+ {
+ //ok
+ }
+ catch (Exception e)
+ {
+ fail("Should throw NoSuchElementException exception, not " + e);
+ }
+
+ //test defaults
+ assertEquals("test string", c.getString("string", "some default value"));
+ assertEquals("default string", c.getString("default"));
+ assertEquals("default string", c.getString("default", "some default value"));
+ assertEquals("some default value", c.getString("XXX", "some default value"));
+ }
+
+ @Test
+ public void testCheckingInMemoryConfiguration() throws Exception
+ {
+ String TEST_KEY = "testKey";
+ Configuration defaults = new PropertiesConfiguration();
+ defaults.setProperty(TEST_KEY, "testValue");
+ Configuration testConfiguration = new CompositeConfiguration(defaults);
+ assertTrue(testConfiguration.containsKey(TEST_KEY));
+ assertFalse(testConfiguration.isEmpty());
+ boolean foundTestKey = false;
+ Iterator<String> i = testConfiguration.getKeys();
+ //assertTrue(i instanceof IteratorChain);
+ //IteratorChain ic = (IteratorChain)i;
+ //assertEquals(2,i.size());
+ for (; i.hasNext();)
+ {
+ String key = i.next();
+ if (key.equals(TEST_KEY))
+ {
+ foundTestKey = true;
+ }
+ }
+ assertTrue(foundTestKey);
+ testConfiguration.clearProperty(TEST_KEY);
+ assertFalse(testConfiguration.containsKey(TEST_KEY));
+ }
+
+ @Test
+ public void testStringArrayInterpolation()
+ {
+ CompositeConfiguration config = new CompositeConfiguration();
+ config.addProperty("base", "foo");
+ config.addProperty("list", "${base}.bar1");
+ config.addProperty("list", "${base}.bar2");
+ config.addProperty("list", "${base}.bar3");
+
+ String[] array = config.getStringArray("list");
+ assertEquals("size", 3, array.length);
+ assertEquals("1st element", "foo.bar1", array[0]);
+ assertEquals("2nd element", "foo.bar2", array[1]);
+ assertEquals("3rd element", "foo.bar3", array[2]);
+ }
+
+ /**
+ * Tests whether global interpolation works with lists.
+ */
+ @Test
+ public void testListInterpolation()
+ {
+ PropertiesConfiguration c1 = new PropertiesConfiguration();
+ c1.addProperty("c1.value", "test1");
+ c1.addProperty("c1.value", "${c2.value}");
+ cc.addConfiguration(c1);
+ PropertiesConfiguration c2 = new PropertiesConfiguration();
+ c2.addProperty("c2.value", "test2");
+ cc.addConfiguration(c2);
+ List<Object> lst = cc.getList("c1.value");
+ assertEquals("Wrong list size", 2, lst.size());
+ assertEquals("Wrong first element", "test1", lst.get(0));
+ assertEquals("Wrong second element", "test2", lst.get(1));
+ }
+
+ /**
+ * Tests interpolation in combination with reloading.
+ */
+ @Test
+ public void testInterpolationWithReload() throws IOException,
+ ConfigurationException
+ {
+ File testFile = new File("target/testConfig.properties");
+ final String propFirst = "first.name";
+ final String propFull = "full.name";
+
+ try
+ {
+ writeTestConfig(testFile, propFirst, "John");
+ PropertiesConfiguration c1 = new PropertiesConfiguration(testFile);
+ c1.setReloadingStrategy(new FileAlwaysReloadingStrategy());
+ PropertiesConfiguration c2 = new PropertiesConfiguration();
+ c2.addProperty(propFull, "${" + propFirst + "} Doe");
+ CompositeConfiguration cc = new CompositeConfiguration();
+ cc.addConfiguration(c1);
+ cc.addConfiguration(c2);
+ assertEquals("Wrong name", "John Doe", cc.getString(propFull));
+
+ writeTestConfig(testFile, propFirst, "Jane");
+ assertEquals("First name not changed", "Jane", c1
+ .getString(propFirst));
+ assertEquals("First name not changed in composite", "Jane", cc
+ .getString(propFirst));
+ assertEquals("Full name not changed", "Jane Doe", cc
+ .getString(propFull));
+ }
+ finally
+ {
+ if (testFile.exists())
+ {
+ testFile.delete();
+ }
+ }
+ }
+
+ /**
+ * Writes a test properties file containing a single property definition.
+ *
+ * @param f the file to write
+ * @param prop the property name
+ * @param value the property value
+ * @throws IOException if an error occurs
+ */
+ private void writeTestConfig(File f, String prop, String value)
+ throws IOException
+ {
+ PrintWriter out = new PrintWriter(new FileWriter(f));
+ out.print(prop);
+ out.print("=");
+ out.println(value);
+ out.close();
+ }
+
+ @Test
+ public void testInstanciateWithCollection()
+ {
+ Collection<Configuration> configs = new ArrayList<Configuration>();
+ configs.add(xmlConf);
+ configs.add(conf1);
+ configs.add(conf2);
+
+ CompositeConfiguration config = new CompositeConfiguration(configs);
+ assertEquals("Number of configurations", 4, config.getNumberOfConfigurations());
+ assertTrue("The in memory configuration is not empty", config.getInMemoryConfiguration().isEmpty());
+ }
+
+ @Test
+ public void testClone()
+ {
+ CompositeConfiguration cc2 = (CompositeConfiguration) cc.clone();
+ assertEquals("Wrong number of contained configurations", cc
+ .getNumberOfConfigurations(), cc2.getNumberOfConfigurations());
+
+ StrictConfigurationComparator comp = new StrictConfigurationComparator();
+ for (int i = 0; i < cc.getNumberOfConfigurations(); i++)
+ {
+ assertEquals("Wrong configuration class at " + i, cc
+ .getConfiguration(i).getClass(), cc2.getConfiguration(i)
+ .getClass());
+ assertNotSame("Configuration was not cloned", cc
+ .getConfiguration(i), cc2.getConfiguration(i));
+ assertTrue("Configurations at " + i + " not equal", comp.compare(cc
+ .getConfiguration(i), cc2.getConfiguration(i)));
+ }
+
+ assertTrue("Configurations are not equal", comp.compare(cc, cc2));
+ }
+
+ /**
+ * Tests cloning if one of the contained configurations does not support
+ * this operation. This should cause an exception.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testCloneNotSupported()
+ {
+ cc.addConfiguration(new NonCloneableConfiguration());
+ cc.clone();
+ }
+
+ /**
+ * Ensures that event listeners are not cloned.
+ */
+ @Test
+ public void testCloneEventListener()
+ {
+ cc.addConfigurationListener(new TestEventListenerImpl());
+ CompositeConfiguration cc2 = (CompositeConfiguration) cc.clone();
+ assertTrue("Listeners have been cloned", cc2
+ .getConfigurationListeners().isEmpty());
+ }
+
+ /**
+ * Tests whether add property events are triggered.
+ */
+ @Test
+ public void testEventAddProperty()
+ {
+ TestEventListenerImpl l = new TestEventListenerImpl();
+ cc.addConfigurationListener(l);
+ cc.addProperty("test", "value");
+ assertEquals("No add events received", 2, l.eventCount);
+ }
+
+ /**
+ * Tests whether set property events are triggered.
+ */
+ @Test
+ public void testEventSetProperty()
+ {
+ TestEventListenerImpl l = new TestEventListenerImpl();
+ cc.addConfigurationListener(l);
+ cc.setProperty("test", "value");
+ assertEquals("No set events received", 2, l.eventCount);
+ }
+
+ /**
+ * Tests whether clear property events are triggered.
+ */
+ @Test
+ public void testEventClearProperty()
+ {
+ cc.addConfiguration(conf1);
+ assertTrue("Wrong value for property", cc
+ .getBoolean("configuration.loaded"));
+ TestEventListenerImpl l = new TestEventListenerImpl();
+ cc.addConfigurationListener(l);
+ cc.clearProperty("configuration.loaded");
+ assertFalse("Key still present", cc.containsKey("configuration.loaded"));
+ assertEquals("No clear events received", 2, l.eventCount);
+ }
+
+ /**
+ * Tests changing the list delimiter character.
+ */
+ @Test
+ public void testSetListDelimiter()
+ {
+ cc.setListDelimiter('/');
+ checkSetListDelimiter();
+ }
+
+ /**
+ * Tests whether the correct list delimiter is set after a clear operation.
+ */
+ @Test
+ public void testSetListDelimiterAfterClear()
+ {
+ cc.setListDelimiter('/');
+ cc.clear();
+ checkSetListDelimiter();
+ }
+
+ /**
+ * Helper method for testing whether the list delimiter is correctly
+ * handled.
+ */
+ private void checkSetListDelimiter()
+ {
+ cc.addProperty("test.list", "a/b/c");
+ cc.addProperty("test.property", "a,b,c");
+ assertEquals("Wrong number of list elements", 3, cc
+ .getList("test.list").size());
+ assertEquals("Wrong value of property", "a,b,c", cc
+ .getString("test.property"));
+ }
+
+ /**
+ * Tests whether list splitting can be disabled.
+ */
+ @Test
+ public void testSetDelimiterParsingDisabled()
+ {
+ cc.setDelimiterParsingDisabled(true);
+ checkSetListDelimiterParsingDisabled();
+ }
+
+ /**
+ * Tests whether the list parsing flag is correctly handled after a clear()
+ * operation.
+ */
+ @Test
+ public void testSetDelimiterParsingDisabledAfterClear()
+ {
+ cc.setDelimiterParsingDisabled(true);
+ cc.clear();
+ checkSetListDelimiterParsingDisabled();
+ }
+
+ /**
+ * Helper method for checking whether the list parsing flag is correctly
+ * handled.
+ */
+ private void checkSetListDelimiterParsingDisabled()
+ {
+ cc.addProperty("test.property", "a,b,c");
+ assertEquals("Wrong value of property", "a,b,c", cc
+ .getString("test.property"));
+ }
+
+ /**
+ * Prepares a test of the getSource() method.
+ */
+ private void setUpSourceTest()
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(conf2);
+ }
+
+ /**
+ * Tests the getSource() method if the property is defined in a single child
+ * configuration.
+ */
+ @Test
+ public void testGetSourceSingle()
+ {
+ setUpSourceTest();
+ conf1.addProperty(TEST_PROPERTY, Boolean.TRUE);
+ assertSame("Wrong source configuration", conf1, cc
+ .getSource(TEST_PROPERTY));
+ }
+
+ /**
+ * Tests the getSource() method for an unknown property key.
+ */
+ @Test
+ public void testGetSourceUnknown()
+ {
+ setUpSourceTest();
+ assertNull("Wrong source for unknown key", cc.getSource(TEST_PROPERTY));
+ }
+
+ /**
+ * Tests the getSource() method for a property contained in the in memory
+ * configuration.
+ */
+ @Test
+ public void testGetSourceInMemory()
+ {
+ setUpSourceTest();
+ cc.addProperty(TEST_PROPERTY, Boolean.TRUE);
+ assertSame("Source not found in in-memory config", cc
+ .getInMemoryConfiguration(), cc.getSource(TEST_PROPERTY));
+ }
+
+ /**
+ * Tests the getSource() method if the property is defined by multiple child
+ * configurations. In this case an exception should be thrown.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetSourceMultiple()
+ {
+ setUpSourceTest();
+ conf1.addProperty(TEST_PROPERTY, Boolean.TRUE);
+ cc.addProperty(TEST_PROPERTY, "a value");
+ cc.getSource(TEST_PROPERTY);
+ }
+
+ /**
+ * Tests the getSource() method for a null key. This should cause an
+ * exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetSourceNull()
+ {
+ cc.getSource(null);
+ }
+
+ /**
+ * Prepares a test for interpolation with multiple configurations and
+ * similar properties.
+ */
+ private void prepareInterpolationTest()
+ {
+ PropertiesConfiguration p = new PropertiesConfiguration();
+ p.addProperty("foo", "initial");
+ p.addProperty("bar", "${foo}");
+ p.addProperty("prefix.foo", "override");
+
+ cc.addConfiguration(p.subset("prefix"));
+ cc.addConfiguration(p);
+ assertEquals("Wrong value on direct access", "override", cc
+ .getString("bar"));
+ }
+
+ /**
+ * Tests querying a list when a tricky interpolation is involved. This is
+ * related to CONFIGURATION-339.
+ */
+ @Test
+ public void testGetListWithInterpolation()
+ {
+ prepareInterpolationTest();
+ List<Object> lst = cc.getList("bar");
+ assertEquals("Wrong number of values", 1, lst.size());
+ assertEquals("Wrong value in list", "override", lst.get(0));
+ }
+
+ /**
+ * Tests querying a string array when a tricky interpolation is involved.
+ */
+ @Test
+ public void testGetStringArrayWithInterpolation()
+ {
+ prepareInterpolationTest();
+ String[] values = cc.getStringArray("bar");
+ assertEquals("Wrong number of values", 1, values.length);
+ assertEquals("Wrong value in array", "override", values[0]);
+ }
+
+ /**
+ * Tests whether interpolation works if multiple configurations are
+ * involved. This test is related to CONFIGURATION-441.
+ */
+ @Test
+ public void testInterpolationInMultipleConfigs()
+ {
+ Configuration c1 = new PropertiesConfiguration();
+ c1.addProperty("property.one", "one");
+ c1.addProperty("property.two", "two");
+ Configuration c2 = new PropertiesConfiguration();
+ c2.addProperty("property.one.ref", "${property.one}");
+ cc.addConfiguration(c1);
+ cc.addConfiguration(c2);
+ assertEquals("Wrong interpolated value", "one",
+ cc.getString("property.one.ref"));
+ }
+
+ /**
+ * Tests the behavior of setListDelimiter() if the in-memory configuration
+ * is not derived from BaseConfiguration. This test is related to
+ * CONFIGURATION-476.
+ */
+ @Test
+ public void testSetListDelimiterInMemoryConfigNonBaseConfig()
+ {
+ Configuration inMemoryConfig = EasyMock.createMock(Configuration.class);
+ EasyMock.replay(inMemoryConfig);
+ cc = new CompositeConfiguration(inMemoryConfig);
+ cc.setListDelimiter(';');
+ }
+
+ /**
+ * Tests the behavior of setDelimiterParsingDisabled() if the in-memory
+ * configuration is not derived from BaseConfiguration. This test is related
+ * to CONFIGURATION-476.
+ */
+ @Test
+ public void testSetDelimiterParsingDisabledInMemoryConfigNonBaseConfig()
+ {
+ Configuration inMemoryConfig = EasyMock.createMock(Configuration.class);
+ EasyMock.replay(inMemoryConfig);
+ cc = new CompositeConfiguration(inMemoryConfig);
+ cc.setDelimiterParsingDisabled(true);
+ }
+
+ /**
+ * Tests whether a configuration can act as both regular child configuration
+ * and in-memory configuration. This test is related to CONFIGURATION-471.
+ */
+ @Test
+ public void testUseChildConfigAsInMemoryConfig()
+ {
+ conf1.setProperty(TEST_PROPERTY, "conf1");
+ conf2.setProperty(TEST_PROPERTY, "conf2");
+ cc.addConfiguration(conf1, true);
+ cc.addConfiguration(conf2);
+ assertEquals("Wrong number of configurations", 2,
+ cc.getNumberOfConfigurations());
+ assertEquals("Wrong property", "conf1", cc.getString(TEST_PROPERTY));
+ cc.addProperty("newProperty", "newValue");
+ assertEquals("Not added to in-memory config", "newValue",
+ conf1.getString("newProperty"));
+ }
+
+ /**
+ * Tests whether the in-memory configuration can be replaced by a new child
+ * configuration.
+ */
+ @Test
+ public void testReplaceInMemoryConfig()
+ {
+ conf1.setProperty(TEST_PROPERTY, "conf1");
+ conf2.setProperty(TEST_PROPERTY, "conf2");
+ cc.addConfiguration(conf1, true);
+ cc.addProperty("newProperty1", "newValue1");
+ cc.addConfiguration(conf2, true);
+ cc.addProperty("newProperty2", "newValue2");
+ assertEquals("Wrong property", "conf1", cc.getString(TEST_PROPERTY));
+ assertEquals("Not added to in-memory config", "newValue1",
+ conf1.getString("newProperty1"));
+ assertEquals("In-memory config not changed", "newValue2",
+ conf2.getString("newProperty2"));
+ }
+
+ /**
+ * A test configuration event listener that counts the number of received
+ * events. Used for testing the event facilities.
+ */
+ static class TestEventListenerImpl implements ConfigurationListener
+ {
+ /** The number of received events.*/
+ int eventCount;
+
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ eventCount++;
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestCompositeConfigurationNonStringProperties.java b/src/test/java/org/apache/commons/configuration/TestCompositeConfigurationNonStringProperties.java
new file mode 100644
index 0000000..a37976e
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestCompositeConfigurationNonStringProperties.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import org.junit.Before;
+
+
+/**
+ * Test if non-string properties are handled correctly.
+ *
+ * @version $Id: TestCompositeConfigurationNonStringProperties.java 1222445 2011-12-22 20:53:47Z oheger $
+ */
+public class TestCompositeConfigurationNonStringProperties extends BaseNonStringProperties
+{
+ /** The File that we test with */
+ private String testProperties = ConfigurationAssert.getTestFile("test.properties").getAbsolutePath();
+
+ @Before
+ public void setUp() throws Exception
+ {
+ CompositeConfiguration cc = new CompositeConfiguration();
+ cc.addConfiguration(new PropertiesConfiguration(testProperties));
+ conf = cc;
+ nonStringTestHolder.setConfiguration(conf);
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestConfiguration.java b/src/test/java/org/apache/commons/configuration/TestConfiguration.java
new file mode 100644
index 0000000..69b0cb6
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestConfiguration.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestConfiguration
+{
+ @Test
+ public void testConfigurationGetList()
+ {
+ final List<String> defaults = new ArrayList<String>();
+
+ String key = UUID.randomUUID().toString();
+ for (int i = 0; i < 10; i++) {
+ defaults.add(UUID.randomUUID().toString());
+ }
+
+ final Configuration c = new MapConfiguration(Collections.<String, String>emptyMap());
+
+ final List<Object> values = c.getList(key, defaults);
+
+ Assert.assertEquals(defaults, values);
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestConfigurationConverter.java b/src/test/java/org/apache/commons/configuration/TestConfigurationConverter.java
new file mode 100644
index 0000000..676a56a
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestConfigurationConverter.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.commons.collections.ExtendedProperties;
+import org.junit.Test;
+
+/**
+ * Tests the ConfigurationConverter class.
+ *
+ * @author Martin Poeschl
+ * @author Emmanuel Bourg
+ * @version $Id: TestConfigurationConverter.java 1223010 2011-12-24 20:21:36Z oheger $
+ */
+public class TestConfigurationConverter
+{
+ @Test
+ public void testExtendedPropertiesToConfiguration()
+ {
+ ExtendedProperties eprops = new ExtendedProperties();
+ eprops.setProperty("string", "teststring");
+ eprops.setProperty("int", "123");
+ eprops.addProperty("list", "item 1");
+ eprops.addProperty("list", "item 2");
+
+ Configuration config = ConfigurationConverter.getConfiguration(eprops);
+
+ assertEquals("This returns 'teststring'", "teststring", config.getString("string"));
+ List<Object> item1 = config.getList("list");
+ assertEquals("This returns 'item 1'", "item 1", item1.get(0));
+
+ assertEquals("This returns 123", 123, config.getInt("int"));
+ }
+
+ @Test
+ public void testPropertiesToConfiguration()
+ {
+ Properties props = new Properties();
+ props.setProperty("string", "teststring");
+ props.setProperty("int", "123");
+ props.setProperty("list", "item 1, item 2");
+
+ Configuration config = ConfigurationConverter.getConfiguration(props);
+
+ assertEquals("This returns 'teststring'", "teststring", config.getString("string"));
+ List<Object> item1 = config.getList("list");
+ assertEquals("This returns 'item 1'", "item 1", item1.get(0));
+
+ assertEquals("This returns 123", 123, config.getInt("int"));
+ }
+
+ @Test
+ public void testConfigurationToExtendedProperties()
+ {
+ Configuration config = new BaseConfiguration();
+ config.setProperty("string", "teststring");
+ config.setProperty("int", "123");
+ config.addProperty("list", "item 1");
+ config.addProperty("list", "item 2");
+
+ ExtendedProperties eprops = ConfigurationConverter.getExtendedProperties(config);
+
+ assertEquals("This returns 'teststring'", "teststring", eprops.getString("string"));
+ List<?> list = eprops.getVector("list");
+ assertEquals("This returns 'item 1'", "item 1", list.get(0));
+ assertEquals("This returns 123", 123, eprops.getInt("int"));
+ }
+
+ @Test
+ public void testConfigurationToProperties()
+ {
+ BaseConfiguration config = new BaseConfiguration();
+ config.addProperty("string", "teststring");
+ config.addProperty("array", "item 1");
+ config.addProperty("array", "item 2");
+ config.addProperty("interpolated", "${string}");
+ config.addProperty("interpolated-array", "${interpolated}");
+ config.addProperty("interpolated-array", "${interpolated}");
+
+ Properties props = ConfigurationConverter.getProperties(config);
+
+ assertNotNull("null properties", props);
+ assertEquals("'string' property", "teststring", props.getProperty("string"));
+ assertEquals("'interpolated' property", "teststring", props.getProperty("interpolated"));
+ assertEquals("'array' property", "item 1,item 2", props.getProperty("array"));
+ assertEquals("'interpolated-array' property", "teststring,teststring", props.getProperty("interpolated-array"));
+
+ // change the list delimiter
+ config.setListDelimiter(';');
+ props = ConfigurationConverter.getProperties(config);
+ assertEquals("'array' property", "item 1;item 2", props.getProperty("array"));
+ }
+
+ /**
+ * Tests the conversion of a configuration object to properties if scalar
+ * values are involved. This test is related to CONFIGURATION-432.
+ */
+ @Test
+ public void testConfigurationToPropertiesScalarValue()
+ {
+ BaseConfiguration config = new BaseConfiguration();
+ config.addProperty("scalar", new Integer(42));
+ Properties props = ConfigurationConverter.getProperties(config);
+ assertEquals("Wrong value", "42", props.getProperty("scalar"));
+ }
+
+ @Test
+ public void testConfigurationToMap()
+ {
+ Configuration config = new BaseConfiguration();
+ config.addProperty("string", "teststring");
+
+ Map<Object, Object> map = ConfigurationConverter.getMap(config);
+
+ assertNotNull("null map", map);
+ assertEquals("'string' property", "teststring", map.get("string"));
+ }
+
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestConfigurationFactory.java b/src/test/java/org/apache/commons/configuration/TestConfigurationFactory.java
new file mode 100644
index 0000000..6fd0ff1
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestConfigurationFactory.java
@@ -0,0 +1,407 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.net.URL;
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+/**
+ * Test the ConfigurationFactory.
+ *
+ * @version $Id: TestConfigurationFactory.java 1223011 2011-12-24 20:30:45Z oheger $
+ */
+ at SuppressWarnings("deprecation")
+public class TestConfigurationFactory
+{
+ /** The Files that we test with */
+ private URL digesterRules = getClass().getResource("/digesterRules.xml");
+ private File testDigesterFile = ConfigurationAssert.getTestFile("testDigesterConfiguration.xml");
+ private File testDigesterFileReverseOrder =
+ ConfigurationAssert.getTestFile("testDigesterConfigurationReverseOrder.xml");
+ private File testDigesterFileNamespaceAware =
+ ConfigurationAssert.getTestFile("testDigesterConfigurationNamespaceAware.xml");
+ private File testDigesterFileBasePath =
+ ConfigurationAssert.getTestFile("testDigesterConfigurationBasePath.xml");
+ private File testDigesterFileEnhanced =
+ ConfigurationAssert.getTestFile("testDigesterConfiguration2.xml");
+ private File testDigesterFileComplete =
+ ConfigurationAssert.getTestFile("testDigesterConfiguration3.xml");
+ private File testDigesterFileOptional =
+ ConfigurationAssert.getTestFile("testDigesterOptionalConfiguration.xml");
+ private File testDigesterFileOptionalEx =
+ ConfigurationAssert.getTestFile("testDigesterOptionalConfigurationEx.xml");
+ private File testDigesterFileSysProps =
+ ConfigurationAssert.getTestFile("testDigesterConfigurationSysProps.xml");
+ private File testDigesterFileInitProps =
+ ConfigurationAssert.getTestFile("testDigesterConfigurationWithProps.xml");
+
+ private File testDigesterBadXML = ConfigurationAssert.getTestFile("testDigesterBadXML.xml");
+
+ private String testBasePath = new File("conf").getAbsolutePath();
+
+ private File testProperties = ConfigurationAssert.getTestFile("test.properties");
+ private File testAbsConfig = ConfigurationAssert.getOutFile("testAbsConfig.xml");
+
+ private Configuration configuration;
+ private CompositeConfiguration compositeConfiguration;
+ private ConfigurationFactory factory;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ System.setProperty("java.naming.factory.initial", "org.apache.commons.configuration.MockInitialContextFactory");
+ factory = new ConfigurationFactory();
+ }
+
+ @Test
+ public void testJNDI() throws Exception
+ {
+ JNDIConfiguration jndiConfiguration = new JNDIConfiguration();
+ Object o = jndiConfiguration.getProperty("test.boolean");
+ assertNotNull(o);
+ assertEquals("true", o.toString());
+ }
+
+ @Test
+ public void testLoadingConfiguration() throws Exception
+ {
+ factory.setConfigurationFileName(testDigesterFile.toString());
+
+ compositeConfiguration = (CompositeConfiguration) factory.getConfiguration();
+
+ assertEquals("Number of configurations", 4, compositeConfiguration.getNumberOfConfigurations());
+ assertEquals(PropertiesConfiguration.class, compositeConfiguration.getConfiguration(0).getClass());
+ assertEquals(XMLPropertiesConfiguration.class, compositeConfiguration.getConfiguration(1).getClass());
+ assertEquals(XMLConfiguration.class, compositeConfiguration.getConfiguration(2).getClass());
+
+ // check the first configuration
+ PropertiesConfiguration pc = (PropertiesConfiguration) compositeConfiguration.getConfiguration(0);
+ assertNotNull("Make sure we have a fileName: " + pc.getFileName(), pc.getFileName());
+
+ // check some properties
+ assertTrue("Make sure we have loaded our key", compositeConfiguration.getBoolean("test.boolean"));
+ assertEquals("I'm complex!", compositeConfiguration.getProperty("element2.subelement.subsubelement"));
+ assertEquals("property in the XMLPropertiesConfiguration", "value1", compositeConfiguration.getProperty("key1"));
+ }
+
+ @Test
+ public void testLoadingConfigurationWithRulesXML() throws Exception
+ {
+ factory.setConfigurationFileName(testDigesterFile.toString());
+ factory.setDigesterRules(digesterRules);
+
+ compositeConfiguration = (CompositeConfiguration) factory.getConfiguration();
+
+ assertEquals("Number of configurations", 4, compositeConfiguration.getNumberOfConfigurations());
+ assertEquals(PropertiesConfiguration.class, compositeConfiguration.getConfiguration(0).getClass());
+ //assertEquals(XMLPropertiesConfiguration.class, compositeConfiguration.getConfiguration(1).getClass()); // doesn't work
+ assertEquals(XMLConfiguration.class, compositeConfiguration.getConfiguration(2).getClass());
+
+ // check the first configuration
+ PropertiesConfiguration pc = (PropertiesConfiguration) compositeConfiguration.getConfiguration(0);
+ assertNotNull("Make sure we have a fileName: " + pc.getFileName(), pc.getFileName());
+
+ // check some properties
+ assertTrue("Make sure we have loaded our key", pc.getBoolean("test.boolean"));
+ assertTrue("Make sure we have loaded our key", compositeConfiguration.getBoolean("test.boolean"));
+
+ assertEquals("I'm complex!", compositeConfiguration.getProperty("element2.subelement.subsubelement"));
+ }
+
+ @Test
+ public void testLoadingConfigurationReverseOrder() throws Exception
+ {
+ factory.setConfigurationFileName(testDigesterFileReverseOrder.toString());
+
+ configuration = factory.getConfiguration();
+
+ assertEquals("8", configuration.getProperty("test.short"));
+
+ factory.setConfigurationFileName(testDigesterFile.toString());
+
+ configuration = factory.getConfiguration();
+ assertEquals("1", configuration.getProperty("test.short"));
+ }
+
+ @Test
+ public void testLoadingConfigurationNamespaceAware() throws Exception
+ {
+ factory.setConfigurationFileName(testDigesterFileNamespaceAware.toString());
+ factory.setDigesterRuleNamespaceURI("namespace-one");
+
+ checkCompositeConfiguration();
+ }
+
+ @Test
+ public void testLoadingConfigurationBasePath() throws Exception
+ {
+ factory.setConfigurationFileName(testDigesterFileBasePath.toString());
+
+ factory.setBasePath(testBasePath);
+
+ //factory.setDigesterRuleNamespaceURI("namespace-one");
+
+ checkCompositeConfiguration();
+ }
+
+ @Test
+ public void testLoadingAdditional() throws Exception
+ {
+ factory.setConfigurationFileName(testDigesterFileEnhanced.toString());
+ factory.setBasePath(null);
+ checkUnionConfig();
+ }
+
+ @Test
+ public void testLoadingURL() throws Exception
+ {
+ factory.setConfigurationURL(testDigesterFileEnhanced.toURL());
+ checkUnionConfig();
+ }
+
+ @Test(expected = ConfigurationException.class)
+ public void testLoadingURLNonExisting() throws Exception
+ {
+ factory = new ConfigurationFactory();
+ File nonExistingFile = new File("conf/nonexisting.xml");
+ factory.setConfigurationURL(nonExistingFile.toURL());
+ factory.getConfiguration();
+ }
+
+ @Test
+ public void testThrowingConfigurationInitializationException() throws Exception
+ {
+ factory.setConfigurationFileName(testDigesterBadXML.toString());
+ try
+ {
+ factory.getConfiguration();
+ fail("Should have throw an Exception");
+ }
+ catch (ConfigurationException cle)
+ {
+ assertTrue("Unexpected cause: " + cle.getCause(),
+ cle.getCause() instanceof SAXException);
+ }
+ }
+
+ // Tests if properties from all sources can be loaded
+ @Test
+ public void testAllConfiguration() throws Exception
+ {
+ factory.setConfigurationURL(testDigesterFileComplete.toURL());
+ Configuration config = factory.getConfiguration();
+ assertFalse(config.isEmpty());
+ assertTrue(config instanceof CompositeConfiguration);
+ CompositeConfiguration cc = (CompositeConfiguration) config;
+ assertTrue(cc.getNumberOfConfigurations() > 1);
+ // Currently fails, should be 4? Only 2?
+ //assertEquals(4, cc.getNumberOfConfigurations());
+
+ assertNotNull(config.getProperty("tables.table(0).fields.field(2).name"));
+ assertNotNull(config.getProperty("element2.subelement.subsubelement"));
+ assertEquals("value", config.getProperty("element3"));
+ assertEquals("foo", config.getProperty("element3[@name]"));
+ assertNotNull(config.getProperty("mail.account.user"));
+
+ // test JNDIConfiguration
+ assertNotNull(config.getProperty("test.onlyinjndi"));
+ assertTrue(config.getBoolean("test.onlyinjndi"));
+
+ Configuration subset = config.subset("test");
+ assertNotNull(subset.getProperty("onlyinjndi"));
+ assertTrue(subset.getBoolean("onlyinjndi"));
+
+ // test SystemConfiguration
+ assertNotNull(config.getProperty("java.version"));
+ assertEquals(System.getProperty("java.version"), config.getString("java.version"));
+ }
+
+ // Checks if optional configurations work
+ @Test
+ public void testOptionalConfigurations() throws Exception
+ {
+ factory.setConfigurationURL(testDigesterFileOptional.toURL());
+ Configuration config = factory.getConfiguration();
+ assertTrue(config.getBoolean("test.boolean"));
+ assertEquals("value", config.getProperty("element"));
+
+ factory.setConfigurationURL(testDigesterFileOptionalEx.toURL());
+ try
+ {
+ config = factory.getConfiguration();
+ fail("Unexisting properties loaded!");
+ }
+ catch(ConfigurationException cex)
+ {
+ // fine
+ }
+ }
+
+ // Checks if a file with an absolute path can be loaded
+ @Test
+ public void testLoadAbsolutePath() throws Exception
+ {
+ try
+ {
+ FileWriter out = null;
+ try
+ {
+ out = new FileWriter(testAbsConfig);
+ out.write("<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>");
+ out.write("<configuration>");
+ out.write("<properties fileName=\"");
+ out.write(testProperties.getAbsolutePath());
+ out.write("\"/>");
+ out.write("</configuration>");
+ }
+ finally
+ {
+ if (out != null)
+ {
+ out.close();
+ }
+ }
+
+ factory.setConfigurationFileName(testAbsConfig.toString());
+ Configuration config = factory.getConfiguration();
+ assertTrue(config.getBoolean("configuration.loaded"));
+ }
+ finally
+ {
+ if (testAbsConfig.exists())
+ {
+ testAbsConfig.delete();
+ }
+ }
+ }
+
+ @Test
+ public void testBasePath() throws Exception
+ {
+ assertEquals(".", factory.getBasePath());
+ factory.setConfigurationFileName(testDigesterFile.getAbsolutePath());
+ // if no specific base path has been set, the base is determined
+ // from the file name
+ assertEquals(testDigesterFile.getParentFile().getAbsolutePath(),
+ factory.getBasePath());
+
+ String homeDir = System.getProperty("user.home");
+ factory = new ConfigurationFactory();
+ factory.setBasePath(homeDir);
+ factory.setConfigurationFileName(testDigesterFile.getAbsolutePath());
+ // if a base path was set, the file name does not play a role
+ assertEquals(homeDir, factory.getBasePath());
+
+ factory = new ConfigurationFactory(testDigesterFile.getAbsolutePath());
+ assertEquals(testDigesterFile.getParentFile().getAbsolutePath(),
+ factory.getBasePath());
+ factory.setBasePath(homeDir);
+ assertEquals(homeDir, factory.getBasePath());
+
+ factory = new ConfigurationFactory();
+ factory.setConfigurationURL(testDigesterFile.toURL());
+ assertEquals(testDigesterFile.toURL().toString(), factory.getBasePath());
+ }
+
+ // Tests if system properties can be resolved in the configuration
+ // definition
+ @Test
+ public void testLoadingWithSystemProperties() throws ConfigurationException
+ {
+ System.setProperty("config.file", "test.properties");
+ factory.setConfigurationFileName(testDigesterFileSysProps
+ .getAbsolutePath());
+ Configuration config = factory.getConfiguration();
+ assertTrue("Configuration not loaded", config
+ .getBoolean("configuration.loaded"));
+ }
+
+ // Tests if the properties of a configuration object are correctly set
+ // before it is loaded.
+ @Test
+ public void testLoadInitProperties() throws ConfigurationException
+ {
+ factory.setConfigurationFileName(testDigesterFileInitProps
+ .getAbsolutePath());
+ Configuration config = factory.getConfiguration();
+ PropertiesConfiguration c = (PropertiesConfiguration) ((CompositeConfiguration) config)
+ .getConfiguration(0);
+ assertEquals("List delimiter was not set", ';', c.getListDelimiter());
+ List<Object> l = c.getList("test.mixed.array");
+ assertEquals("Wrong number of list elements", 2, l.size());
+ assertEquals("List delimiter was not applied", "b, c, d", l.get(1));
+ }
+
+ private void checkUnionConfig() throws Exception
+ {
+ compositeConfiguration = (CompositeConfiguration) factory.getConfiguration();
+ assertEquals("Verify how many configs", 3, compositeConfiguration.getNumberOfConfigurations());
+
+ // Test if union was constructed correctly
+ Object prop = compositeConfiguration.getProperty("tables.table.name");
+ assertTrue(prop instanceof Collection);
+ assertEquals(3, ((Collection<?>) prop).size());
+ assertEquals("users", compositeConfiguration.getProperty("tables.table(0).name"));
+ assertEquals("documents", compositeConfiguration.getProperty("tables.table(1).name"));
+ assertEquals("tasks", compositeConfiguration.getProperty("tables.table(2).name"));
+
+ prop = compositeConfiguration.getProperty("tables.table.fields.field.name");
+ assertTrue(prop instanceof Collection);
+ assertEquals(17, ((Collection<?>) prop).size());
+
+ assertEquals("smtp.mydomain.org", compositeConfiguration.getString("mail.host.smtp"));
+ assertEquals("pop3.mydomain.org", compositeConfiguration.getString("mail.host.pop"));
+
+ // This was overriden
+ assertEquals("masterOfPost", compositeConfiguration.getString("mail.account.user"));
+ assertEquals("topsecret", compositeConfiguration.getString("mail.account.psswd"));
+
+ // This was overriden, too, but not in additional section
+ assertEquals("enhanced factory", compositeConfiguration.getString("test.configuration"));
+ }
+
+ private void checkCompositeConfiguration() throws Exception
+ {
+ compositeConfiguration = (CompositeConfiguration) factory.getConfiguration();
+
+ assertEquals("Verify how many configs", 2, compositeConfiguration.getNumberOfConfigurations());
+ assertEquals(PropertiesConfiguration.class, compositeConfiguration.getConfiguration(0).getClass());
+
+ PropertiesConfiguration pc = (PropertiesConfiguration) compositeConfiguration.getConfiguration(0);
+ assertNotNull("Make sure we have a fileName:" + pc.getFileName(), pc.getFileName());
+ assertTrue("Make sure we have loaded our key", pc.getBoolean("test.boolean"));
+ assertTrue("Make sure we have loaded our key", compositeConfiguration.getBoolean("test.boolean"));
+
+ Object property = compositeConfiguration.getProperty("element2.subelement.subsubelement");
+ assertNull("Should have returned a null", property);
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestConfigurationKey.java b/src/test/java/org/apache/commons/configuration/TestConfigurationKey.java
new file mode 100644
index 0000000..1192d2f
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestConfigurationKey.java
@@ -0,0 +1,262 @@
+package org.apache.commons.configuration;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.NoSuchElementException;
+
+import org.junit.Test;
+
+/**
+ * Test class for ConfigurationKey.
+ *
+ * @version $Id: TestConfigurationKey.java 1231749 2012-01-15 20:48:56Z oheger $
+ */
+ at SuppressWarnings("deprecation")
+public class TestConfigurationKey
+{
+ private static final String TESTPROPS = "tables.table(0).fields.field(1)";
+
+ private static final String TESTATTR = "[@dataType]";
+
+ private static final String TESTKEY = TESTPROPS + TESTATTR;
+
+ @Test
+ public void testAppend()
+ {
+ ConfigurationKey key = new ConfigurationKey();
+ key.append("tables").append("table.").appendIndex(0);
+ key.append("fields.").append("field").appendIndex(1);
+ key.appendAttribute("dataType");
+ assertEquals(TESTKEY, key.toString());
+ }
+
+ @Test
+ public void testIterate()
+ {
+ ConfigurationKey key = new ConfigurationKey(TESTKEY);
+ ConfigurationKey.KeyIterator it = key.iterator();
+ assertTrue(it.hasNext());
+ assertEquals("tables", it.nextKey());
+ assertEquals("table", it.nextKey());
+ assertTrue(it.hasIndex());
+ assertEquals(0, it.getIndex());
+ assertEquals("fields", it.nextKey());
+ assertFalse(it.hasIndex());
+ assertEquals("field", it.nextKey(true));
+ assertEquals(1, it.getIndex());
+ assertFalse(it.isAttribute());
+ assertEquals("field", it.currentKey(true));
+ assertEquals("dataType", it.nextKey());
+ assertEquals("[@dataType]", it.currentKey(true));
+ assertTrue(it.isAttribute());
+ assertFalse(it.hasNext());
+ try
+ {
+ it.next();
+ fail("Could iterate over the iteration's end!");
+ }
+ catch(NoSuchElementException nex)
+ {
+ //ok
+ }
+
+ key = new ConfigurationKey();
+ assertFalse(key.iterator().hasNext());
+ key.append("simple");
+ it = key.iterator();
+ assertTrue(it.hasNext());
+ assertEquals("simple", it.next());
+ try
+ {
+ it.remove();
+ fail("Could remove key component!");
+ }
+ catch(UnsupportedOperationException uex)
+ {
+ //ok
+ }
+ }
+
+ @Test
+ public void testAttribute()
+ {
+ assertTrue(ConfigurationKey.isAttributeKey(TESTATTR));
+ assertFalse(ConfigurationKey.isAttributeKey(TESTPROPS));
+ assertFalse(ConfigurationKey.isAttributeKey(TESTKEY));
+
+ ConfigurationKey key = new ConfigurationKey(TESTPROPS);
+ key.append(TESTATTR);
+ assertEquals(TESTKEY, key.toString());
+ }
+
+ @Test
+ public void testLength()
+ {
+ ConfigurationKey key = new ConfigurationKey(TESTPROPS);
+ assertEquals(TESTPROPS.length(), key.length());
+ key.appendAttribute("dataType");
+ assertEquals(TESTKEY.length(), key.length());
+ key.setLength(TESTPROPS.length());
+ assertEquals(TESTPROPS.length(), key.length());
+ assertEquals(TESTPROPS, key.toString());
+ }
+
+ @Test
+ public void testConstructAttributeKey()
+ {
+ assertEquals("[@attribute]", ConfigurationKey.constructAttributeKey("attribute"));
+ assertEquals("attribute", ConfigurationKey.attributeName("[@attribute]"));
+ assertEquals("attribute", ConfigurationKey.attributeName("attribute"));
+ }
+
+ @Test
+ public void testEquals()
+ {
+ ConfigurationKey k1 = new ConfigurationKey(TESTKEY);
+ ConfigurationKey k2 = new ConfigurationKey(TESTKEY);
+ assertTrue(k1.equals(k2));
+ assertTrue(k2.equals(k1));
+ assertEquals(k1.hashCode(), k2.hashCode());
+ k2.append("anotherPart");
+ assertFalse(k1.equals(k2));
+ assertFalse(k2.equals(k1));
+ assertFalse(k1.equals(null));
+ assertTrue(k1.equals(TESTKEY));
+ }
+
+ @Test
+ public void testCommonKey()
+ {
+ ConfigurationKey k1 = new ConfigurationKey(TESTKEY);
+ ConfigurationKey k2 = new ConfigurationKey("tables.table(0).name");
+ ConfigurationKey kc = k1.commonKey(k2);
+ assertEquals(new ConfigurationKey("tables.table(0)"), kc);
+ assertEquals(kc, k2.commonKey(k1));
+
+ k2 = new ConfigurationKey("tables.table(1).fields.field(1)");
+ kc = k1.commonKey(k2);
+ assertEquals(new ConfigurationKey("tables"), kc);
+
+ k2 = new ConfigurationKey("completely.different.key");
+ kc = k1.commonKey(k2);
+ assertEquals(0, kc.length());
+
+ k2 = new ConfigurationKey();
+ kc = k1.commonKey(k2);
+ assertEquals(0, kc.length());
+
+ kc = k1.commonKey(k1);
+ assertEquals(kc, k1);
+
+ try
+ {
+ kc.commonKey(null);
+ fail("Could construct common key with null key!");
+ }
+ catch(IllegalArgumentException iex)
+ {
+ //ok
+ }
+ }
+
+ @Test
+ public void testDifferenceKey()
+ {
+ ConfigurationKey k1 = new ConfigurationKey(TESTKEY);
+ ConfigurationKey kd = k1.differenceKey(k1);
+ assertEquals(0, kd.length());
+
+ ConfigurationKey k2 = new ConfigurationKey("tables.table(0).name");
+ kd = k1.differenceKey(k2);
+ assertEquals("name", kd.toString());
+
+ k2 = new ConfigurationKey("tables.table(1).fields.field(1)");
+ kd = k1.differenceKey(k2);
+ assertEquals("table(1).fields.field(1)", kd.toString());
+
+ k2 = new ConfigurationKey("completely.different.key");
+ kd = k1.differenceKey(k2);
+ assertEquals(k2, kd);
+ }
+
+ @Test
+ public void testEscapedDelimiters()
+ {
+ ConfigurationKey k = new ConfigurationKey();
+ k.append("my..elem");
+ k.append("trailing..dot..");
+ k.append("strange");
+ assertEquals("my..elem.trailing..dot...strange", k.toString());
+
+ ConfigurationKey.KeyIterator kit = k.iterator();
+ assertEquals("my.elem", kit.nextKey());
+ assertEquals("trailing.dot.", kit.nextKey());
+ assertEquals("strange", kit.nextKey());
+ assertFalse(kit.hasNext());
+ }
+
+ /**
+ * Tests some funny keys.
+ */
+ @Test
+ public void testIterateStrangeKeys()
+ {
+ ConfigurationKey k = new ConfigurationKey("key.");
+ ConfigurationKey.KeyIterator it = k.iterator();
+ assertTrue(it.hasNext());
+ assertEquals("key", it.next());
+ assertFalse(it.hasNext());
+
+ k = new ConfigurationKey(".");
+ it = k.iterator();
+ assertFalse(it.hasNext());
+
+ k = new ConfigurationKey("key().index()undefined(0).test");
+ it = k.iterator();
+ assertEquals("key()", it.next());
+ assertFalse(it.hasIndex());
+ assertEquals("index()undefined", it.nextKey(false));
+ assertTrue(it.hasIndex());
+ assertEquals(0, it.getIndex());
+ }
+
+ /**
+ * Tests iterating over an attribute key that has an index.
+ */
+ @Test
+ public void testAttributeKeyWithIndex()
+ {
+ ConfigurationKey k = new ConfigurationKey(TESTATTR);
+ k.appendIndex(0);
+ assertEquals("Wrong attribute key with index", TESTATTR + "(0)", k.toString());
+
+ ConfigurationKey.KeyIterator it = k.iterator();
+ assertTrue("No first element", it.hasNext());
+ it.next();
+ assertTrue("Index not found", it.hasIndex());
+ assertEquals("Incorrect index", 0, it.getIndex());
+ assertTrue("Attribute not found", it.isAttribute());
+ assertEquals("Wrong plain key", "dataType", it.currentKey(false));
+ assertEquals("Wrong decorated key", TESTATTR, it.currentKey(true));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestConfigurationMap.java b/src/test/java/org/apache/commons/configuration/TestConfigurationMap.java
new file mode 100644
index 0000000..30803f8
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestConfigurationMap.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author <a href="mailto:ricardo.gladwell at btinternet.com">Ricardo Gladwell</a>
+ */
+public class TestConfigurationMap
+{
+
+ ConfigurationMap map;
+
+ String[] properties = {
+ "booleanProperty",
+ "doubleProperty",
+ "floatProperty",
+ "intProperty",
+ "longProperty",
+ "shortProperty",
+ "stringProperty"
+ };
+
+ Object[] values = {
+ Boolean.TRUE,
+ new Double(Double.MAX_VALUE),
+ new Float(Float.MAX_VALUE),
+ new Integer(Integer.MAX_VALUE),
+ new Long(Long.MAX_VALUE),
+ new Short(Short.MAX_VALUE),
+ "This is a string"
+ };
+
+ /**
+ * Set up instance variables required by this test case.
+ */
+ @Before
+ public void setUp() throws Exception
+ {
+ BaseConfiguration configuration = new BaseConfiguration();
+ for(int i = 0; i < properties.length ; i++)
+ configuration.setProperty(properties[i], values[i]);
+ map = new ConfigurationMap(configuration);
+ }
+
+ /**
+ * Tear down instance variables required by this test case.
+ */
+ @After
+ public void tearDown()
+ {
+ map = null;
+ }
+
+ /**
+ * Class under test for Object put(Object, Object)
+ */
+ @Test
+ public void testPut()
+ {
+ for(int i = 0; i < properties.length; i++) {
+ Object object = map.put(properties[i], values[i]);
+ assertNotNull("Returned null from put.",object);
+ assertEquals("Returned wrong result.",values[i],object);
+ object = map.get(properties[i]);
+ assertNotNull("Returned null from get.",object);
+ assertEquals("Returned wrong result.",values[i],object);
+ }
+ }
+
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestConfigurationSet.java b/src/test/java/org/apache/commons/configuration/TestConfigurationSet.java
new file mode 100644
index 0000000..6d57c8f
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestConfigurationSet.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+import java.util.Map;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author rgladwel
+ */
+public class TestConfigurationSet {
+
+ ConfigurationMap.ConfigurationSet set;
+
+ String[] properties = {
+ "booleanProperty",
+ "doubleProperty",
+ "floatProperty",
+ "intProperty",
+ "longProperty",
+ "shortProperty",
+ "stringProperty"
+ };
+
+ Object[] values = {
+ Boolean.TRUE,
+ new Double(Double.MAX_VALUE),
+ new Float(Float.MAX_VALUE),
+ new Integer(Integer.MAX_VALUE),
+ new Long(Long.MAX_VALUE),
+ new Short(Short.MAX_VALUE),
+ "This is a string"
+ };
+
+ /**
+ * Set up instance variables required by this test case.
+ */
+ @Before
+ public void setUp() throws Exception
+ {
+ BaseConfiguration configuration = new BaseConfiguration();
+ for(int i = 0; i < properties.length ; i++)
+ configuration.setProperty(properties[i], values[i]);
+ set = new ConfigurationMap.ConfigurationSet(configuration);
+ }
+
+ /**
+ * Tear down instance variables required by this test case.
+ */
+ @After
+ public void tearDown()
+ {
+ set = null;
+ }
+
+ @Test
+ public void testSize() {
+ assertEquals("Entry set does not match properties size.", properties.length, set.size());
+ }
+
+ /**
+ * Class under test for Iterator iterator()
+ */
+ @Test
+ public void testIterator() {
+ Iterator<Map.Entry<Object, Object>> iterator = set.iterator();
+ while(iterator.hasNext()) {
+ Map.Entry<Object, Object> entry = iterator.next();
+ boolean found = false;
+ for(int i = 0; i < properties.length; i++) {
+ if(entry.getKey().equals(properties[i])) {
+ assertEquals("Incorrect value for property " +
+ properties[i],values[i],entry.getValue());
+ found = true;
+ }
+ }
+ assertTrue("Could not find property " + entry.getKey(),found);
+ iterator.remove();
+ }
+ assertTrue("Iterator failed to remove all properties.",set.isEmpty());
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestConfigurationUtils.java b/src/test/java/org/apache/commons/configuration/TestConfigurationUtils.java
new file mode 100644
index 0000000..47f3bcc
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestConfigurationUtils.java
@@ -0,0 +1,459 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import junitx.framework.ListAssert;
+
+import org.apache.commons.configuration.tree.DefaultExpressionEngine;
+import org.apache.commons.configuration.tree.ExpressionEngine;
+import org.junit.Test;
+
+import com.mockobjects.dynamic.Mock;
+
+/**
+ * Tests the ConfigurationUtils class
+ *
+ * @version $Id: TestConfigurationUtils.java 1301996 2012-03-17 20:30:39Z sebb $
+ */
+public class TestConfigurationUtils
+{
+ protected Configuration config = new BaseConfiguration();
+
+ @Test
+ public void testToString()
+ {
+ String lineSeparator = System.getProperty("line.separator");
+
+ assertEquals("String representation of an empty configuration", "", ConfigurationUtils.toString(config));
+
+ config.setProperty("one", "1");
+ assertEquals("String representation of a configuration", "one=1", ConfigurationUtils.toString(config));
+
+ config.setProperty("two", "2");
+ assertEquals("String representation of a configuration", "one=1" + lineSeparator + "two=2" , ConfigurationUtils.toString(config));
+
+ config.clearProperty("one");
+ assertEquals("String representation of a configuration", "two=2" , ConfigurationUtils.toString(config));
+
+ config.setProperty("one","1");
+ assertEquals("String representation of a configuration", "two=2" + lineSeparator + "one=1" , ConfigurationUtils.toString(config));
+ }
+
+ @Test
+ public void testGetURL() throws Exception
+ {
+ assertEquals(
+ "http://localhost:8080/webapp/config/config.xml",
+ ConfigurationUtils
+ .getURL(
+ "http://localhost:8080/webapp/config/baseConfig.xml",
+ "config.xml")
+ .toString());
+ assertEquals(
+ "http://localhost:8080/webapp/config/config.xml",
+ ConfigurationUtils
+ .getURL(
+ "http://localhost:8080/webapp/baseConfig.xml",
+ "config/config.xml")
+ .toString());
+ URL url = ConfigurationUtils.getURL(null, "config.xml");
+ assertEquals("file", url.getProtocol());
+ assertEquals("", url.getHost());
+
+ assertEquals(
+ "http://localhost:8080/webapp/config/config.xml",
+ ConfigurationUtils
+ .getURL(
+ "ftp://ftp.server.com/downloads/baseConfig.xml",
+ "http://localhost:8080/webapp/config/config.xml")
+ .toString());
+ assertEquals(
+ "http://localhost:8080/webapp/config/config.xml",
+ ConfigurationUtils
+ .getURL(null, "http://localhost:8080/webapp/config/config.xml")
+ .toString());
+ File absFile = new File("config.xml").getAbsoluteFile();
+ assertEquals(
+ absFile.toURI().toURL(),
+ ConfigurationUtils.getURL(
+ "http://localhost:8080/webapp/config/baseConfig.xml",
+ absFile.getAbsolutePath()));
+ assertEquals(
+ absFile.toURI().toURL(),
+ ConfigurationUtils.getURL(null, absFile.getAbsolutePath()));
+
+ assertEquals(absFile.toURI().toURL(),
+ ConfigurationUtils.getURL(absFile.getParent(), "config.xml"));
+ }
+
+ @Test
+ public void testGetBasePath() throws Exception
+ {
+ URL url = new URL("http://xyz.net/foo/bar.xml");
+ assertEquals("base path of " + url, "http://xyz.net/foo/", ConfigurationUtils.getBasePath(url));
+
+ url = new URL("http://xyz.net/foo/");
+ assertEquals("base path of " + url, "http://xyz.net/foo/", ConfigurationUtils.getBasePath(url));
+
+ url = new URL("http://xyz.net/foo");
+ assertEquals("base path of " + url, "http://xyz.net/", ConfigurationUtils.getBasePath(url));
+
+ url = new URL("http://xyz.net/");
+ assertEquals("base path of " + url, "http://xyz.net/", ConfigurationUtils.getBasePath(url));
+
+ url = new URL("http://xyz.net");
+ assertEquals("base path of " + url, "http://xyz.net", ConfigurationUtils.getBasePath(url));
+ }
+
+ @Test
+ public void testGetFileName() throws Exception
+ {
+ assertEquals("file name for a null URL", null, ConfigurationUtils.getFileName(null));
+
+ URL url = new URL("http://xyz.net/foo/");
+ assertEquals("file for a directory URL " + url, null, ConfigurationUtils.getFileName(url));
+
+ url = new URL("http://xyz.net/foo/bar.xml");
+ assertEquals("file name for a valid URL " + url, "bar.xml", ConfigurationUtils.getFileName(url));
+ }
+
+ @Test
+ public void testCopy()
+ {
+ // create the source configuration
+ Configuration conf1 = new BaseConfiguration();
+ conf1.addProperty("key1", "value1");
+ conf1.addProperty("key2", "value2");
+
+ // create the target configuration
+ Configuration conf2 = new BaseConfiguration();
+ conf2.addProperty("key1", "value3");
+ conf2.addProperty("key2", "value4");
+
+ // copy the source configuration into the target configuration
+ ConfigurationUtils.copy(conf1, conf2);
+
+ assertEquals("'key1' property", "value1", conf2.getProperty("key1"));
+ assertEquals("'key2' property", "value2", conf2.getProperty("key2"));
+ }
+
+ @Test
+ public void testAppend()
+ {
+ // create the source configuration
+ Configuration conf1 = new BaseConfiguration();
+ conf1.addProperty("key1", "value1");
+ conf1.addProperty("key2", "value2");
+
+ // create the target configuration
+ Configuration conf2 = new BaseConfiguration();
+ conf2.addProperty("key1", "value3");
+ conf2.addProperty("key2", "value4");
+
+ // append the source configuration to the target configuration
+ ConfigurationUtils.append(conf1, conf2);
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add("value3");
+ expected.add("value1");
+ ListAssert.assertEquals("'key1' property", expected, conf2.getList("key1"));
+
+ expected = new ArrayList<Object>();
+ expected.add("value4");
+ expected.add("value2");
+ ListAssert.assertEquals("'key2' property", expected, conf2.getList("key2"));
+ }
+
+ @Test
+ public void testGetFile() throws Exception
+ {
+ File directory = new File("target");
+ File reference = new File(directory, "test.txt").getAbsoluteFile();
+
+ assertEquals(reference, ConfigurationUtils.getFile(null, reference.getAbsolutePath()));
+ assertEquals(reference, ConfigurationUtils.getFile(directory.getAbsolutePath(), reference.getAbsolutePath()));
+ assertEquals(reference, ConfigurationUtils.getFile(directory.getAbsolutePath(), reference.getName()));
+ assertEquals(reference, ConfigurationUtils.getFile(directory.toURI().toURL().toString(), reference.getName()));
+ assertEquals(reference, ConfigurationUtils.getFile("invalid", reference.toURI().toURL().toString()));
+ assertEquals(reference, ConfigurationUtils.getFile(
+ "jar:file:/C:/myjar.jar!/my-config.xml/someprops.properties",
+ reference.getAbsolutePath()));
+ }
+
+ /**
+ * Tests whether a "+" character in the file name is handled correctly by
+ * fileFromURL(). This test is related to CONFIGURATION-415.
+ */
+ @Test
+ public void testFileFromURLWithPlus() throws MalformedURLException
+ {
+ File file = new File(new File("target"), "foo+bar.txt")
+ .getAbsoluteFile();
+ URL fileURL = file.toURI().toURL();
+ File file2 = ConfigurationUtils.fileFromURL(fileURL);
+ assertEquals("Wrong file", file, file2);
+ }
+
+ /**
+ * Tests whether fileFromURL() handles null URLs correctly.
+ */
+ @Test
+ public void testFileFromURLNull() throws Exception
+ {
+ assertNull("Wrong file for null URL", ConfigurationUtils
+ .fileFromURL(null));
+ }
+
+ @Test
+ public void testLocateWithNullTCCL() throws Exception
+ {
+ ClassLoader cl = Thread.currentThread().getContextClassLoader();
+ try
+ {
+ Thread.currentThread().setContextClassLoader(null);
+ assertNull(ConfigurationUtils.locate("abase", "aname"));
+ // This assert fails when maven 2 is used, so commented out
+ //assertNotNull(ConfigurationUtils.locate("test.xml"));
+ }
+ finally
+ {
+ Thread.currentThread().setContextClassLoader(cl);
+ }
+ }
+
+ /**
+ * Tests converting a configuration into a hierarchical one.
+ */
+ @Test
+ public void testConvertToHierarchical()
+ {
+ Configuration conf = new BaseConfiguration();
+ for (int i = 0; i < 10; i++)
+ {
+ conf.addProperty("test" + i, "value" + i);
+ conf.addProperty("test.list", "item" + i);
+ }
+
+ HierarchicalConfiguration hc = ConfigurationUtils
+ .convertToHierarchical(conf);
+ for (Iterator<String> it = conf.getKeys(); it.hasNext();)
+ {
+ String key = it.next();
+ assertEquals("Wrong value for key " + key, conf.getProperty(key),
+ hc.getProperty(key));
+ }
+ }
+
+ /**
+ * Tests converting a configuration into a hierarchical one that is already
+ * hierarchical.
+ */
+ @Test
+ public void testConvertHierarchicalToHierarchical()
+ {
+ Configuration conf = new HierarchicalConfiguration();
+ conf.addProperty("test", "yes");
+ assertSame("Wrong configuration returned", conf, ConfigurationUtils
+ .convertToHierarchical(conf));
+ }
+
+ /**
+ * Tests converting a null configuration to a hierarchical one. The result
+ * should be null, too.
+ */
+ @Test
+ public void testConvertNullToHierarchical()
+ {
+ assertNull("Wrong conversion result for null config",
+ ConfigurationUtils.convertToHierarchical(null));
+ }
+
+ /**
+ * Tests converting a configuration into a hierarchical one if some of its
+ * properties contain escaped list delimiter characters.
+ */
+ @Test
+ public void testConvertToHierarchicalDelimiters()
+ {
+ Configuration conf = new BaseConfiguration();
+ conf.addProperty("test.key", "1\\,2\\,3");
+ assertEquals("Wrong property value", "1,2,3", conf
+ .getString("test.key"));
+ HierarchicalConfiguration hc = ConfigurationUtils
+ .convertToHierarchical(conf);
+ assertEquals("Escaped list delimiters not correctly handled", "1,2,3",
+ hc.getString("test.key"));
+ }
+
+ /**
+ * Tests converting a configuration to a hierarchical one using a specific
+ * expression engine.
+ */
+ @Test
+ public void testConvertToHierarchicalEngine()
+ {
+ Configuration conf = new BaseConfiguration();
+ conf.addProperty("test(a)", Boolean.TRUE);
+ conf.addProperty("test(b)", Boolean.FALSE);
+ DefaultExpressionEngine engine = new DefaultExpressionEngine();
+ engine.setIndexStart("[");
+ engine.setIndexEnd("]");
+ HierarchicalConfiguration hc = ConfigurationUtils
+ .convertToHierarchical(conf, engine);
+ assertTrue("Wrong value for test(a)", hc.getBoolean("test(a)"));
+ assertFalse("Wrong value for test(b)", hc.getBoolean("test(b)"));
+ }
+
+ /**
+ * Tests converting an already hierarchical configuration using an
+ * expression engine. The new engine should be set.
+ */
+ @Test
+ public void testConvertHierarchicalToHierarchicalEngine()
+ {
+ HierarchicalConfiguration hc = new HierarchicalConfiguration();
+ ExpressionEngine engine = new DefaultExpressionEngine();
+ assertSame("Created new configuration", hc, ConfigurationUtils
+ .convertToHierarchical(hc, engine));
+ assertSame("Engine was not set", engine, hc.getExpressionEngine());
+ }
+
+ /**
+ * Tests converting an already hierarchical configuration using a null
+ * expression engine. In this case the expression engine of the
+ * configuration should not be touched.
+ */
+ @Test
+ public void testConvertHierarchicalToHierarchicalNullEngine()
+ {
+ HierarchicalConfiguration hc = new HierarchicalConfiguration();
+ ExpressionEngine engine = new DefaultExpressionEngine();
+ hc.setExpressionEngine(engine);
+ assertSame("Created new configuration", hc, ConfigurationUtils
+ .convertToHierarchical(hc, null));
+ assertSame("Expression engine was changed", engine, hc
+ .getExpressionEngine());
+ }
+
+ /**
+ * Tests converting a configuration to a hierarchical one that contains a
+ * property with multiple values. This test is related to CONFIGURATION-346.
+ */
+ @Test
+ public void testConvertToHierarchicalMultiValues()
+ {
+ BaseConfiguration config = new BaseConfiguration();
+ config.addProperty("test", "1,2,3");
+ HierarchicalConfiguration hc = ConfigurationUtils
+ .convertToHierarchical(config);
+ assertEquals("Wrong value 1", 1, hc.getInt("test(0)"));
+ assertEquals("Wrong value 2", 2, hc.getInt("test(1)"));
+ assertEquals("Wrong value 3", 3, hc.getInt("test(2)"));
+ }
+
+ /**
+ * Tests cloning a configuration that supports this operation.
+ */
+ @Test
+ public void testCloneConfiguration()
+ {
+ HierarchicalConfiguration conf = new HierarchicalConfiguration();
+ conf.addProperty("test", "yes");
+ HierarchicalConfiguration copy = (HierarchicalConfiguration) ConfigurationUtils
+ .cloneConfiguration(conf);
+ assertNotSame("Same object was returned", conf, copy);
+ assertEquals("Property was not cloned", "yes", copy.getString("test"));
+ }
+
+ /**
+ * Tests cloning a configuration that does not support this operation. This
+ * should cause an exception.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testCloneConfigurationNotSupported()
+ {
+ Configuration myNonCloneableConfig = new NonCloneableConfiguration();
+ ConfigurationUtils.cloneConfiguration(myNonCloneableConfig);
+ }
+
+ /**
+ * Tests cloning a <b>null</b> configuration.
+ */
+ @Test
+ public void testCloneConfigurationNull()
+ {
+ assertNull("Wrong return value", ConfigurationUtils
+ .cloneConfiguration(null));
+ }
+
+ /**
+ * Tests whether runtime exceptions can be enabled.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testEnableRuntimeExceptions()
+ {
+ PropertiesConfiguration config = new PropertiesConfiguration()
+ {
+ @Override
+ protected void addPropertyDirect(String key, Object value)
+ {
+ // always simulate an exception
+ fireError(EVENT_ADD_PROPERTY, key, value, new RuntimeException(
+ "A faked exception!"));
+ }
+ };
+ config.clearErrorListeners();
+ ConfigurationUtils.enableRuntimeExceptions(config);
+ config.addProperty("test", "testValue");
+ }
+
+ /**
+ * Tries to enable runtime exceptions for a configuration that does not
+ * inherit from EventSource. This should cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testEnableRuntimeExceptionsInvalid()
+ {
+ ConfigurationUtils.enableRuntimeExceptions((Configuration) new Mock(
+ Configuration.class).proxy());
+ }
+
+ /**
+ * Tries to enable runtime exceptions for a null configuration. This should
+ * cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testEnableRuntimeExceptionsNull()
+ {
+ ConfigurationUtils.enableRuntimeExceptions(null);
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestDataConfiguration.java b/src/test/java/org/apache/commons/configuration/TestDataConfiguration.java
new file mode 100644
index 0000000..7797b62
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestDataConfiguration.java
@@ -0,0 +1,2402 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.awt.Color;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.net.URL;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.NoSuchElementException;
+
+import junitx.framework.ArrayAssert;
+import junitx.framework.ListAssert;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Emmanuel Bourg
+ * @version $Id: TestDataConfiguration.java 1535297 2013-10-24 07:53:37Z henning $
+ */
+public class TestDataConfiguration
+{
+ private DataConfiguration conf;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ conf = new DataConfiguration(new BaseConfiguration());
+
+ // empty value
+ conf.addProperty("empty", "");
+
+ // lists of boolean
+ conf.addProperty("boolean.list1", "true");
+ conf.addProperty("boolean.list1", "false");
+ conf.addProperty("boolean.list2", "true, false");
+ conf.addProperty("boolean.list3", Boolean.TRUE);
+ conf.addProperty("boolean.list3", Boolean.FALSE);
+ conf.addPropertyDirect("boolean.list4", new Boolean[] { Boolean.TRUE, Boolean.FALSE });
+ conf.addPropertyDirect("boolean.list5", new boolean[] { true, false });
+ List<Object> booleans = new ArrayList<Object>();
+ booleans.add(Boolean.TRUE);
+ booleans.add(Boolean.FALSE);
+ conf.addProperty("boolean.list6", booleans);
+ conf.addProperty("boolean.string", "true");
+ conf.addProperty("boolean.object", Boolean.TRUE);
+ conf.addProperty("boolean.list.interpolated", "${boolean.string},false");
+
+ // lists of bytes
+ conf.addProperty("byte.list1", "1");
+ conf.addProperty("byte.list1", "2");
+ conf.addProperty("byte.list2", "1, 2");
+ conf.addProperty("byte.list3", new Byte("1"));
+ conf.addProperty("byte.list3", new Byte("2"));
+ conf.addPropertyDirect("byte.list4", new Byte[] { new Byte("1"), new Byte("2") });
+ conf.addPropertyDirect("byte.list5", new byte[] { 1, 2 });
+ List<Object> bytes = new ArrayList<Object>();
+ bytes.add(new Byte("1"));
+ bytes.add(new Byte("2"));
+ conf.addProperty("byte.list6", bytes);
+ conf.addProperty("byte.string", "1");
+ conf.addProperty("byte.object", new Byte("1"));
+ conf.addProperty("byte.list.interpolated", "${byte.string},2");
+
+ // lists of shorts
+ conf.addProperty("short.list1", "1");
+ conf.addProperty("short.list1", "2");
+ conf.addProperty("short.list2", "1, 2");
+ conf.addProperty("short.list3", new Short("1"));
+ conf.addProperty("short.list3", new Short("2"));
+ conf.addPropertyDirect("short.list4", new Short[] { new Short("1"), new Short("2") });
+ conf.addPropertyDirect("short.list5", new short[] { 1, 2 });
+ List<Object> shorts = new ArrayList<Object>();
+ shorts.add(new Short("1"));
+ shorts.add(new Short("2"));
+ conf.addProperty("short.list6", shorts);
+ conf.addProperty("short.string", "1");
+ conf.addProperty("short.object", new Short("1"));
+ conf.addProperty("short.list.interpolated", "${short.string},2");
+
+ // lists of integers
+ conf.addProperty("integer.list1", "1");
+ conf.addProperty("integer.list1", "2");
+ conf.addProperty("integer.list2", "1, 2");
+ conf.addProperty("integer.list3", new Integer("1"));
+ conf.addProperty("integer.list3", new Integer("2"));
+ conf.addPropertyDirect("integer.list4", new Integer[] { new Integer("1"), new Integer("2") });
+ conf.addPropertyDirect("integer.list5", new int[] { 1, 2 });
+ List<Object> integers = new ArrayList<Object>();
+ integers.add(new Integer("1"));
+ integers.add(new Integer("2"));
+ conf.addProperty("integer.list6", integers);
+ conf.addProperty("integer.string", "1");
+ conf.addProperty("integer.object", new Integer("1"));
+ conf.addProperty("integer.list.interpolated", "${integer.string},2");
+
+ // lists of longs
+ conf.addProperty("long.list1", "1");
+ conf.addProperty("long.list1", "2");
+ conf.addProperty("long.list2", "1, 2");
+ conf.addProperty("long.list3", new Long("1"));
+ conf.addProperty("long.list3", new Long("2"));
+ conf.addPropertyDirect("long.list4", new Long[] { new Long("1"), new Long("2") });
+ conf.addPropertyDirect("long.list5", new long[] { 1, 2 });
+ List<Object> longs = new ArrayList<Object>();
+ longs.add(new Long("1"));
+ longs.add(new Long("2"));
+ conf.addProperty("long.list6", longs);
+ conf.addProperty("long.string", "1");
+ conf.addProperty("long.object", new Long("1"));
+ conf.addProperty("long.list.interpolated", "${long.string},2");
+
+ // lists of floats
+ conf.addProperty("float.list1", "1");
+ conf.addProperty("float.list1", "2");
+ conf.addProperty("float.list2", "1, 2");
+ conf.addProperty("float.list3", new Float("1"));
+ conf.addProperty("float.list3", new Float("2"));
+ conf.addPropertyDirect("float.list4", new Float[] { new Float("1"), new Float("2") });
+ conf.addPropertyDirect("float.list5", new float[] { 1, 2 });
+ List<Object> floats = new ArrayList<Object>();
+ floats.add(new Float("1"));
+ floats.add(new Float("2"));
+ conf.addProperty("float.list6", floats);
+ conf.addProperty("float.string", "1");
+ conf.addProperty("float.object", new Float("1"));
+ conf.addProperty("float.list.interpolated", "${float.string},2");
+
+ // lists of doubles
+ conf.addProperty("double.list1", "1");
+ conf.addProperty("double.list1", "2");
+ conf.addProperty("double.list2", "1, 2");
+ conf.addProperty("double.list3", new Double("1"));
+ conf.addProperty("double.list3", new Double("2"));
+ conf.addPropertyDirect("double.list4", new Double[] { new Double("1"), new Double("2") });
+ conf.addPropertyDirect("double.list5", new double[] { 1, 2 });
+ List<Object> doubles = new ArrayList<Object>();
+ doubles.add(new Double("1"));
+ doubles.add(new Double("2"));
+ conf.addProperty("double.list6", doubles);
+ conf.addProperty("double.string", "1");
+ conf.addProperty("double.object", new Double("1"));
+ conf.addProperty("double.list.interpolated", "${double.string},2");
+
+ // lists of big integers
+ conf.addProperty("biginteger.list1", "1");
+ conf.addProperty("biginteger.list1", "2");
+ conf.addProperty("biginteger.list2", "1, 2");
+ conf.addProperty("biginteger.list3", new BigInteger("1"));
+ conf.addProperty("biginteger.list3", new BigInteger("2"));
+ conf.addPropertyDirect("biginteger.list4", new BigInteger[] { new BigInteger("1"), new BigInteger("2") });
+ List<Object> bigintegers = new ArrayList<Object>();
+ bigintegers.add(new BigInteger("1"));
+ bigintegers.add(new BigInteger("2"));
+ conf.addProperty("biginteger.list6", bigintegers);
+ conf.addProperty("biginteger.string", "1");
+ conf.addProperty("biginteger.object", new BigInteger("1"));
+ conf.addProperty("biginteger.list.interpolated", "${biginteger.string},2");
+
+ // lists of big decimals
+ conf.addProperty("bigdecimal.list1", "1");
+ conf.addProperty("bigdecimal.list1", "2");
+ conf.addProperty("bigdecimal.list2", "1, 2");
+ conf.addProperty("bigdecimal.list3", new BigDecimal("1"));
+ conf.addProperty("bigdecimal.list3", new BigDecimal("2"));
+ conf.addPropertyDirect("bigdecimal.list4", new BigDecimal[] { new BigDecimal("1"), new BigDecimal("2") });
+ List<Object> bigdecimals = new ArrayList<Object>();
+ bigdecimals.add(new BigDecimal("1"));
+ bigdecimals.add(new BigDecimal("2"));
+ conf.addProperty("bigdecimal.list6", bigdecimals);
+ conf.addProperty("bigdecimal.string", "1");
+ conf.addProperty("bigdecimal.object", new BigDecimal("1"));
+ conf.addProperty("bigdecimal.list.interpolated", "${bigdecimal.string},2");
+
+ // URLs
+ String url1 = "http://jakarta.apache.org";
+ String url2 = "http://www.apache.org";
+ conf.addProperty("url.string", url1);
+ conf.addProperty("url.string.interpolated", "${url.string}");
+ conf.addProperty("url.object", new URL(url1));
+ conf.addProperty("url.list1", url1);
+ conf.addProperty("url.list1", url2);
+ conf.addProperty("url.list2", url1 + ", " + url2);
+ conf.addProperty("url.list3", new URL(url1));
+ conf.addProperty("url.list3", new URL(url2));
+ conf.addPropertyDirect("url.list4", new URL[] { new URL(url1), new URL(url2) });
+ List<Object> urls = new ArrayList<Object>();
+ urls.add(new URL(url1));
+ urls.add(new URL(url2));
+ conf.addProperty("url.list6", urls);
+ conf.addProperty("url.list.interpolated", "${url.string}," + url2);
+
+ // Locales
+ conf.addProperty("locale.string", "fr");
+ conf.addProperty("locale.string.interpolated", "${locale.string}");
+ conf.addProperty("locale.object", Locale.FRENCH);
+ conf.addProperty("locale.list1", "fr");
+ conf.addProperty("locale.list1", "de");
+ conf.addProperty("locale.list2", "fr, de");
+ conf.addProperty("locale.list3", Locale.FRENCH);
+ conf.addProperty("locale.list3", Locale.GERMAN);
+ conf.addPropertyDirect("locale.list4", new Locale[] { Locale.FRENCH, Locale.GERMAN });
+ List<Object> locales = new ArrayList<Object>();
+ locales.add(Locale.FRENCH);
+ locales.add(Locale.GERMAN);
+ conf.addProperty("locale.list6", locales);
+ conf.addProperty("locale.list.interpolated", "${locale.string},de");
+
+ // Colors
+ String color1 = "FF0000";
+ String color2 = "0000FF";
+ conf.addProperty("color.string", color1);
+ conf.addProperty("color.string.interpolated", "${color.string}");
+ conf.addProperty("color.object", Color.red);
+ conf.addProperty("color.list1", color1);
+ conf.addProperty("color.list1", color2);
+ conf.addProperty("color.list2", color1 + ", " + color2);
+ conf.addProperty("color.list3", Color.red);
+ conf.addProperty("color.list3", Color.blue);
+ conf.addPropertyDirect("color.list4", new Color[] { Color.red, Color.blue });
+ List<Object> colors = new ArrayList<Object>();
+ colors.add(Color.red);
+ colors.add(Color.blue);
+ conf.addProperty("color.list6", colors);
+ conf.addProperty("color.list.interpolated", "${color.string}," + color2);
+
+ // Dates & Calendars
+ String pattern = "yyyy-MM-dd";
+ DateFormat format = new SimpleDateFormat(pattern);
+ conf.setProperty(DataConfiguration.DATE_FORMAT_KEY, pattern);
+
+ Date date1 = format.parse("2004-01-01");
+ Date date2 = format.parse("2004-12-31");
+ Calendar calendar1 = Calendar.getInstance();
+ calendar1.setTime(date1);
+ Calendar calendar2 = Calendar.getInstance();
+ calendar2.setTime(date2);
+
+ conf.addProperty("date.string", "2004-01-01");
+ conf.addProperty("date.string.interpolated", "${date.string}");
+ conf.addProperty("date.object", date1);
+ conf.addProperty("date.list1", "2004-01-01");
+ conf.addProperty("date.list1", "2004-12-31");
+ conf.addProperty("date.list2", "2004-01-01, 2004-12-31");
+ conf.addProperty("date.list3", date1);
+ conf.addProperty("date.list3", date2);
+ conf.addPropertyDirect("date.list4", new Date[] { date1, date2 });
+ conf.addPropertyDirect("date.list5", new Calendar[] { calendar1, calendar2 });
+ List<Object> dates = new ArrayList<Object>();
+ dates.add(date1);
+ dates.add(date2);
+ conf.addProperty("date.list6", dates);
+ conf.addProperty("date.list.interpolated", "${date.string},2004-12-31");
+ conf.addPropertyDirect("date.list7", new String[] { "2004-01-01", "2004-12-31" });
+
+ conf.addProperty("calendar.string", "2004-01-01");
+ conf.addProperty("calendar.string.interpolated", "${calendar.string}");
+ conf.addProperty("calendar.object", calendar1);
+ conf.addProperty("calendar.list1", "2004-01-01");
+ conf.addProperty("calendar.list1", "2004-12-31");
+ conf.addProperty("calendar.list2", "2004-01-01, 2004-12-31");
+ conf.addProperty("calendar.list3", calendar1);
+ conf.addProperty("calendar.list3", calendar2);
+ conf.addPropertyDirect("calendar.list4", new Calendar[] { calendar1, calendar2 });
+ conf.addPropertyDirect("calendar.list5", new Date[] { date1, date2 });
+ List<Object> calendars = new ArrayList<Object>();
+ calendars.add(date1);
+ calendars.add(date2);
+ conf.addProperty("calendar.list6", calendars);
+ conf.addProperty("calendar.list.interpolated", "${calendar.string},2004-12-31");
+ conf.addPropertyDirect("calendar.list7", new String[] { "2004-01-01", "2004-12-31" });
+
+ // host address
+ conf.addProperty("ip.string", "127.0.0.1");
+ conf.addProperty("ip.string.interpolated", "${ip.string}");
+ conf.addProperty("ip.object", InetAddress.getByName("127.0.0.1"));
+
+ // email address
+ conf.addProperty("email.string", "ebourg at apache.org");
+ conf.addProperty("email.string.interpolated", "${email.string}");
+ conf.addProperty("email.object", createInternetAddress("ebourg at apache.org"));
+ }
+
+ @Test
+ public void testGetConfiguration()
+ {
+ Configuration baseconf = new BaseConfiguration();
+ DataConfiguration conf = new DataConfiguration(baseconf);
+
+ assertEquals("base configuration", baseconf, conf.getConfiguration());
+ }
+
+ @Test
+ public void testIsEmpty()
+ {
+ Configuration baseconf = new BaseConfiguration();
+ DataConfiguration conf = new DataConfiguration(baseconf);
+
+ assertTrue("not empty", conf.isEmpty());
+
+ baseconf.setProperty("foo", "bar");
+
+ assertFalse("empty", conf.isEmpty());
+ }
+
+ @Test
+ public void testContainsKey()
+ {
+ Configuration baseconf = new BaseConfiguration();
+ DataConfiguration conf = new DataConfiguration(baseconf);
+
+ assertFalse(conf.containsKey("foo"));
+
+ baseconf.setProperty("foo", "bar");
+
+ assertTrue(conf.containsKey("foo"));
+ }
+
+ @Test
+ public void testGetKeys()
+ {
+ Configuration baseconf = new BaseConfiguration();
+ DataConfiguration conf = new DataConfiguration(baseconf);
+
+ baseconf.setProperty("foo", "bar");
+
+ Iterator<String> it = conf.getKeys();
+ assertTrue("the iterator is empty", it.hasNext());
+ assertEquals("unique key", "foo", it.next());
+ assertFalse("the iterator is not exhausted", it.hasNext());
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetInvalidType()
+ {
+ conf.get(Boolean.class, "url.object", null);
+ }
+
+ @Test
+ public void testGetUnknown()
+ {
+ assertNull("non null object for a missing key", conf.get(Object.class, "unknownkey"));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetUnknownException()
+ {
+ conf.setThrowExceptionOnMissing(true);
+ conf.get(Object.class, "unknownkey");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetArrayInvalidDefaultType()
+ {
+ conf.getArray(Boolean.class, "unknownkey", new URL[] {});
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetPrimitiveArrayInvalidType()
+ {
+ conf.getArray(Boolean.TYPE, "calendar.list4");
+ }
+
+ @Test
+ public void testGetBooleanArray()
+ {
+ // missing list
+ boolean[] defaultValue = new boolean[] { false, true };
+ ArrayAssert.assertEquals(defaultValue, conf.getBooleanArray("boolean.list", defaultValue));
+
+ boolean[] expected = new boolean[] { true, false };
+
+ // list of strings
+ ArrayAssert.assertEquals(expected, conf.getBooleanArray("boolean.list1"));
+
+ // list of strings, comma separated
+ ArrayAssert.assertEquals(expected, conf.getBooleanArray("boolean.list2"));
+
+ // list of Boolean objects
+ ArrayAssert.assertEquals(expected, conf.getBooleanArray("boolean.list3"));
+
+ // array of Boolean objects
+ ArrayAssert.assertEquals(expected, conf.getBooleanArray("boolean.list4"));
+
+ // array of boolean primitives
+ ArrayAssert.assertEquals(expected, conf.getBooleanArray("boolean.list5"));
+
+ // list of Boolean objects
+ ArrayAssert.assertEquals(expected, conf.getBooleanArray("boolean.list6"));
+
+ // list of interpolated values
+ ArrayAssert.assertEquals(expected, conf.getBooleanArray("boolean.list.interpolated"));
+
+ // single boolean values
+ ArrayAssert.assertEquals(new boolean[] { true }, conf.getBooleanArray("boolean.string"));
+ ArrayAssert.assertEquals(new boolean[] { true }, conf.getBooleanArray("boolean.object"));
+
+ // empty array
+ ArrayAssert.assertEquals(new boolean[] { }, conf.getBooleanArray("empty"));
+ }
+
+ @Test
+ public void testGetBooleanList()
+ {
+ // missing list
+ ListAssert.assertEquals(null, conf.getBooleanList("boolean.list", null));
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add(Boolean.TRUE);
+ expected.add(Boolean.FALSE);
+
+ // list of strings
+ ListAssert.assertEquals(expected, conf.getBooleanList("boolean.list1"));
+
+ // list of strings, comma separated
+ ListAssert.assertEquals(expected, conf.getBooleanList("boolean.list2"));
+
+ // list of Boolean objects
+ ListAssert.assertEquals(expected, conf.getBooleanList("boolean.list3"));
+
+ // array of Boolean objects
+ ListAssert.assertEquals(expected, conf.getBooleanList("boolean.list4"));
+
+ // array of boolean primitives
+ ListAssert.assertEquals(expected, conf.getBooleanList("boolean.list5"));
+
+ // list of Boolean objects
+ ListAssert.assertEquals(expected, conf.getBooleanList("boolean.list6"));
+
+ // list of interpolated values
+ ListAssert.assertEquals(expected, conf.getBooleanList("boolean.list.interpolated"));
+
+ // single boolean values
+ expected = new ArrayList<Object>();
+ expected.add(Boolean.TRUE);
+ ListAssert.assertEquals(expected, conf.getBooleanList("boolean.string"));
+ ListAssert.assertEquals(expected, conf.getBooleanList("boolean.object"));
+
+ // empty list
+ ListAssert.assertEquals(new ArrayList<Object>(), conf.getBooleanList("empty"));
+ }
+
+ @Test
+ public void testGetByteArray()
+ {
+ // missing list
+ byte[] defaultValue = new byte[] { 1, 2};
+ ArrayAssert.assertEquals(defaultValue, conf.getByteArray("byte.list", defaultValue));
+
+ byte[] expected = new byte[] { 1, 2 };
+
+ // list of strings
+ ArrayAssert.assertEquals(expected, conf.getByteArray("byte.list1"));
+
+ // list of strings, comma separated
+ ArrayAssert.assertEquals(expected, conf.getByteArray("byte.list2"));
+
+ // list of Byte objects
+ ArrayAssert.assertEquals(expected, conf.getByteArray("byte.list3"));
+
+ // array of Byte objects
+ ArrayAssert.assertEquals(expected, conf.getByteArray("byte.list4"));
+
+ // array of byte primitives
+ ArrayAssert.assertEquals(expected, conf.getByteArray("byte.list5"));
+
+ // list of Byte objects
+ ArrayAssert.assertEquals(expected, conf.getByteArray("byte.list6"));
+
+ // list of interpolated values
+ ArrayAssert.assertEquals(expected, conf.getByteArray("byte.list.interpolated"));
+
+ // single byte values
+ ArrayAssert.assertEquals(new byte[] { 1 }, conf.getByteArray("byte.string"));
+ ArrayAssert.assertEquals(new byte[] { 1 }, conf.getByteArray("byte.object"));
+
+ // empty array
+ ArrayAssert.assertEquals(new byte[] { }, conf.getByteArray("empty"));
+ }
+
+ @Test
+ public void testGetByteList()
+ {
+ // missing list
+ ListAssert.assertEquals(null, conf.getByteList("byte.list", null));
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add(new Byte("1"));
+ expected.add(new Byte("2"));
+
+ // list of strings
+ ListAssert.assertEquals(expected, conf.getByteList("byte.list1"));
+
+ // list of strings, comma separated
+ ListAssert.assertEquals(expected, conf.getByteList("byte.list2"));
+
+ // list of Byte objects
+ ListAssert.assertEquals(expected, conf.getByteList("byte.list3"));
+
+ // array of Byte objects
+ ListAssert.assertEquals(expected, conf.getByteList("byte.list4"));
+
+ // array of byte primitives
+ ListAssert.assertEquals(expected, conf.getByteList("byte.list5"));
+
+ // list of Byte objects
+ ListAssert.assertEquals(expected, conf.getByteList("byte.list6"));
+
+ // list of interpolated values
+ ListAssert.assertEquals(expected, conf.getByteList("byte.list.interpolated"));
+
+ // single byte values
+ expected = new ArrayList<Object>();
+ expected.add(new Byte("1"));
+ ListAssert.assertEquals(expected, conf.getByteList("byte.string"));
+ ListAssert.assertEquals(expected, conf.getByteList("byte.object"));
+
+ // empty list
+ ListAssert.assertEquals(new ArrayList<Object>(), conf.getByteList("empty"));
+ }
+
+ @Test
+ public void testGetShortArray()
+ {
+ // missing list
+ short[] defaultValue = new short[] { 2, 1};
+ ArrayAssert.assertEquals(defaultValue, conf.getShortArray("short.list", defaultValue));
+
+ short[] expected = new short[] { 1, 2 };
+
+ // list of strings
+ ArrayAssert.assertEquals(expected, conf.getShortArray("short.list1"));
+
+ // list of strings, comma separated
+ ArrayAssert.assertEquals(expected, conf.getShortArray("short.list2"));
+
+ // list of Byte objects
+ ArrayAssert.assertEquals(expected, conf.getShortArray("short.list3"));
+
+ // array of Byte objects
+ ArrayAssert.assertEquals(expected, conf.getShortArray("short.list4"));
+
+ // array of byte primitives
+ ArrayAssert.assertEquals(expected, conf.getShortArray("short.list5"));
+
+ // list of Byte objects
+ ArrayAssert.assertEquals(expected, conf.getShortArray("short.list6"));
+
+ // list of interpolated values
+ ArrayAssert.assertEquals(expected, conf.getShortArray("short.list.interpolated"));
+
+ // single byte values
+ ArrayAssert.assertEquals(new short[] { 1 }, conf.getShortArray("short.string"));
+ ArrayAssert.assertEquals(new short[] { 1 }, conf.getShortArray("short.object"));
+
+ // empty array
+ ArrayAssert.assertEquals(new short[] { }, conf.getShortArray("empty"));
+ }
+
+ @Test
+ public void testGetShortList()
+ {
+ // missing list
+ ListAssert.assertEquals(null, conf.getShortList("short.list", null));
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add(new Short("1"));
+ expected.add(new Short("2"));
+
+ // list of strings
+ ListAssert.assertEquals(expected, conf.getShortList("short.list1"));
+
+ // list of strings, comma separated
+ ListAssert.assertEquals(expected, conf.getShortList("short.list2"));
+
+ // list of Short objects
+ ListAssert.assertEquals(expected, conf.getShortList("short.list3"));
+
+ // array of Short objects
+ ListAssert.assertEquals(expected, conf.getShortList("short.list4"));
+
+ // array of short primitives
+ ListAssert.assertEquals(expected, conf.getShortList("short.list5"));
+
+ // list of Short objects
+ ListAssert.assertEquals(expected, conf.getShortList("short.list6"));
+
+ // list of interpolated values
+ ListAssert.assertEquals(expected, conf.getShortList("short.list.interpolated"));
+
+ // single short values
+ expected = new ArrayList<Object>();
+ expected.add(new Short("1"));
+ ListAssert.assertEquals(expected, conf.getShortList("short.string"));
+ ListAssert.assertEquals(expected, conf.getShortList("short.object"));
+
+ // empty list
+ ListAssert.assertEquals(new ArrayList<Object>(), conf.getShortList("empty"));
+ }
+
+ @Test
+ public void testGetIntegerArray()
+ {
+ // missing list
+ int[] defaultValue = new int[] { 2, 1};
+ ArrayAssert.assertEquals(defaultValue, conf.getIntArray("integer.list", defaultValue));
+
+ int[] expected = new int[] { 1, 2 };
+
+ // list of strings
+ ArrayAssert.assertEquals(expected, conf.getIntArray("integer.list1"));
+
+ // list of strings, comma separated
+ ArrayAssert.assertEquals(expected, conf.getIntArray("integer.list2"));
+
+ // list of Integer objects
+ ArrayAssert.assertEquals(expected, conf.getIntArray("integer.list3"));
+
+ // array of Integer objects
+ ArrayAssert.assertEquals(expected, conf.getIntArray("integer.list4"));
+
+ // array of int primitives
+ ArrayAssert.assertEquals(expected, conf.getIntArray("integer.list5"));
+
+ // list of Integer objects
+ ArrayAssert.assertEquals(expected, conf.getIntArray("integer.list6"));
+
+ // list of interpolated values
+ ArrayAssert.assertEquals(expected, conf.getIntArray("integer.list.interpolated"));
+
+ // single int values
+ ArrayAssert.assertEquals(new int[] { 1 }, conf.getIntArray("integer.string"));
+ ArrayAssert.assertEquals(new int[] { 1 }, conf.getIntArray("integer.object"));
+
+ // empty array
+ ArrayAssert.assertEquals(new int[] { }, conf.getIntArray("empty"));
+ }
+
+ @Test
+ public void testGetIntegerList()
+ {
+ // missing list
+ ListAssert.assertEquals(null, conf.getIntegerList("integer.list", null));
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add(new Integer("1"));
+ expected.add(new Integer("2"));
+
+ // list of strings
+ ListAssert.assertEquals(expected, conf.getIntegerList("integer.list1"));
+
+ // list of strings, comma separated
+ ListAssert.assertEquals(expected, conf.getIntegerList("integer.list2"));
+
+ // list of Integer objects
+ ListAssert.assertEquals(expected, conf.getIntegerList("integer.list3"));
+
+ // array of Integer objects
+ ListAssert.assertEquals(expected, conf.getIntegerList("integer.list4"));
+
+ // array of int primitives
+ ListAssert.assertEquals(expected, conf.getIntegerList("integer.list5"));
+
+ // list of Integer objects
+ ListAssert.assertEquals(expected, conf.getIntegerList("integer.list6"));
+
+ // list of interpolated values
+ ListAssert.assertEquals(expected, conf.getIntegerList("integer.list.interpolated"));
+
+ // single int values
+ expected = new ArrayList<Object>();
+ expected.add(new Integer("1"));
+ ListAssert.assertEquals(expected, conf.getIntegerList("integer.string"));
+ ListAssert.assertEquals(expected, conf.getIntegerList("integer.object"));
+
+ // empty list
+ ListAssert.assertEquals(new ArrayList<Object>(), conf.getIntegerList("empty"));
+ }
+
+ @Test
+ public void testGetLongArray()
+ {
+ // missing list
+ long[] defaultValue = new long[] { 2, 1};
+ ArrayAssert.assertEquals(defaultValue, conf.getLongArray("long.list", defaultValue));
+
+ long[] expected = new long[] { 1, 2 };
+
+ // list of strings
+ ArrayAssert.assertEquals(expected, conf.getLongArray("long.list1"));
+
+ // list of strings, comma separated
+ ArrayAssert.assertEquals(expected, conf.getLongArray("long.list2"));
+
+ // list of Long objects
+ ArrayAssert.assertEquals(expected, conf.getLongArray("long.list3"));
+
+ // array of Long objects
+ ArrayAssert.assertEquals(expected, conf.getLongArray("long.list4"));
+
+ // array of long primitives
+ ArrayAssert.assertEquals(expected, conf.getLongArray("long.list5"));
+
+ // list of Long objects
+ ArrayAssert.assertEquals(expected, conf.getLongArray("long.list6"));
+
+ // list of interpolated values
+ ArrayAssert.assertEquals(expected, conf.getLongArray("long.list.interpolated"));
+
+ // single long values
+ ArrayAssert.assertEquals(new long[] { 1 }, conf.getLongArray("long.string"));
+ ArrayAssert.assertEquals(new long[] { 1 }, conf.getLongArray("long.object"));
+
+ // empty array
+ ArrayAssert.assertEquals(new long[] { }, conf.getLongArray("empty"));
+ }
+
+ @Test
+ public void testGetLongList()
+ {
+ // missing list
+ ListAssert.assertEquals(null, conf.getLongList("long.list", null));
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add(new Long("1"));
+ expected.add(new Long("2"));
+
+ // list of strings
+ ListAssert.assertEquals(expected, conf.getLongList("long.list1"));
+
+ // list of strings, comma separated
+ ListAssert.assertEquals(expected, conf.getLongList("long.list2"));
+
+ // list of Long objects
+ ListAssert.assertEquals(expected, conf.getLongList("long.list3"));
+
+ // array of Long objects
+ ListAssert.assertEquals(expected, conf.getLongList("long.list4"));
+
+ // array of long primitives
+ ListAssert.assertEquals(expected, conf.getLongList("long.list5"));
+
+ // list of Long objects
+ ListAssert.assertEquals(expected, conf.getLongList("long.list6"));
+
+ // list of interpolated values
+ ListAssert.assertEquals(expected, conf.getLongList("long.list.interpolated"));
+
+ // single long values
+ expected = new ArrayList<Object>();
+ expected.add(new Long("1"));
+ ListAssert.assertEquals(expected, conf.getLongList("long.string"));
+ ListAssert.assertEquals(expected, conf.getLongList("long.object"));
+
+ // empty list
+ ListAssert.assertEquals(new ArrayList<Object>(), conf.getLongList("empty"));
+ }
+
+ @Test
+ public void testGetFloatArray()
+ {
+ // missing list
+ float[] defaultValue = new float[] { 2, 1};
+ ArrayAssert.assertEquals(defaultValue, conf.getFloatArray("float.list", defaultValue), 0);
+
+ float[] expected = new float[] { 1, 2 };
+
+ // list of strings
+ ArrayAssert.assertEquals(expected, conf.getFloatArray("float.list1"), 0);
+
+ // list of strings, comma separated
+ ArrayAssert.assertEquals(expected, conf.getFloatArray("float.list2"), 0);
+
+ // list of Float objects
+ ArrayAssert.assertEquals(expected, conf.getFloatArray("float.list3"), 0);
+
+ // array of Float objects
+ ArrayAssert.assertEquals(expected, conf.getFloatArray("float.list4"), 0);
+
+ // array of float primitives
+ ArrayAssert.assertEquals(expected, conf.getFloatArray("float.list5"), 0);
+
+ // list of Float objects
+ ArrayAssert.assertEquals(expected, conf.getFloatArray("float.list6"), 0);
+
+ // list of interpolated values
+ ArrayAssert.assertEquals(expected, conf.getFloatArray("float.list.interpolated"), 0);
+
+ // single float values
+ ArrayAssert.assertEquals(new float[] { 1 }, conf.getFloatArray("float.string"), 0);
+ ArrayAssert.assertEquals(new float[] { 1 }, conf.getFloatArray("float.object"), 0);
+
+ // empty array
+ ArrayAssert.assertEquals(new float[] { }, conf.getFloatArray("empty"), 0);
+ }
+
+ @Test
+ public void testGetFloatList()
+ {
+ // missing list
+ ListAssert.assertEquals(null, conf.getFloatList("float.list", null));
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add(new Float("1"));
+ expected.add(new Float("2"));
+
+ // list of strings
+ ListAssert.assertEquals(expected, conf.getFloatList("float.list1"));
+
+ // list of strings, comma separated
+ ListAssert.assertEquals(expected, conf.getFloatList("float.list2"));
+
+ // list of Float objects
+ ListAssert.assertEquals(expected, conf.getFloatList("float.list3"));
+
+ // array of Float objects
+ ListAssert.assertEquals(expected, conf.getFloatList("float.list4"));
+
+ // array of float primitives
+ ListAssert.assertEquals(expected, conf.getFloatList("float.list5"));
+
+ // list of Float objects
+ ListAssert.assertEquals(expected, conf.getFloatList("float.list6"));
+
+ // list of interpolated values
+ ListAssert.assertEquals(expected, conf.getFloatList("float.list.interpolated"));
+
+ // single float values
+ expected = new ArrayList<Object>();
+ expected.add(new Float("1"));
+ ListAssert.assertEquals(expected, conf.getFloatList("float.string"));
+ ListAssert.assertEquals(expected, conf.getFloatList("float.object"));
+
+ // empty list
+ ListAssert.assertEquals(new ArrayList<Object>(), conf.getFloatList("empty"));
+ }
+
+ @Test
+ public void testGetDoubleArray()
+ {
+ // missing list
+ double[] defaultValue = new double[] { 2, 1 };
+ ArrayAssert.assertEquals(defaultValue, conf.getDoubleArray("double.list", defaultValue), 0);
+
+ double[] expected = new double[] { 1, 2 };
+
+ // list of strings
+ ArrayAssert.assertEquals(expected, conf.getDoubleArray("double.list1"), 0);
+
+ // list of strings, comma separated
+ ArrayAssert.assertEquals(expected, conf.getDoubleArray("double.list2"), 0);
+
+ // list of Double objects
+ ArrayAssert.assertEquals(expected, conf.getDoubleArray("double.list3"), 0);
+
+ // array of Double objects
+ ArrayAssert.assertEquals(expected, conf.getDoubleArray("double.list4"), 0);
+
+ // array of double primitives
+ ArrayAssert.assertEquals(expected, conf.getDoubleArray("double.list5"), 0);
+
+ // list of Double objects
+ ArrayAssert.assertEquals(expected, conf.getDoubleArray("double.list6"), 0);
+
+ // list of interpolated values
+ ArrayAssert.assertEquals(expected, conf.getDoubleArray("double.list.interpolated"), 0);
+
+ // single double values
+ ArrayAssert.assertEquals(new double[] { 1 }, conf.getDoubleArray("double.string"), 0);
+ ArrayAssert.assertEquals(new double[] { 1 }, conf.getDoubleArray("double.object"), 0);
+
+ // empty array
+ ArrayAssert.assertEquals(new double[] { }, conf.getDoubleArray("empty"), 0);
+ }
+
+ @Test
+ public void testGetDoubleList()
+ {
+ // missing list
+ ListAssert.assertEquals(null, conf.getDoubleList("double.list", null));
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add(new Double("1"));
+ expected.add(new Double("2"));
+
+ // list of strings
+ ListAssert.assertEquals(expected, conf.getDoubleList("double.list1"));
+
+ // list of strings, comma separated
+ ListAssert.assertEquals(expected, conf.getDoubleList("double.list2"));
+
+ // list of Double objects
+ ListAssert.assertEquals(expected, conf.getDoubleList("double.list3"));
+
+ // array of Double objects
+ ListAssert.assertEquals(expected, conf.getDoubleList("double.list4"));
+
+ // array of double primitives
+ ListAssert.assertEquals(expected, conf.getDoubleList("double.list5"));
+
+ // list of Double objects
+ ListAssert.assertEquals(expected, conf.getDoubleList("double.list6"));
+
+ // list of interpolated values
+ ListAssert.assertEquals(expected, conf.getDoubleList("double.list.interpolated"));
+
+ // single double values
+ expected = new ArrayList<Object>();
+ expected.add(new Double("1"));
+ ListAssert.assertEquals(expected, conf.getDoubleList("double.string"));
+ ListAssert.assertEquals(expected, conf.getDoubleList("double.object"));
+
+ // empty list
+ ListAssert.assertEquals(new ArrayList<Object>(), conf.getDoubleList("empty"));
+ }
+
+ @Test
+ public void testGetBigIntegerArray()
+ {
+ // missing list
+ BigInteger[] defaultValue = new BigInteger[] { new BigInteger("2"), new BigInteger("1") };
+ ArrayAssert.assertEquals(defaultValue, conf.getBigIntegerArray("biginteger.list", defaultValue));
+
+ BigInteger[] expected = new BigInteger[] { new BigInteger("1"), new BigInteger("2") };
+
+ // list of strings
+ ArrayAssert.assertEquals(expected, conf.getBigIntegerArray("biginteger.list1"));
+
+ // list of strings, comma separated
+ ArrayAssert.assertEquals(expected, conf.getBigIntegerArray("biginteger.list2"));
+
+ // list of BigInteger objects
+ ArrayAssert.assertEquals(expected, conf.getBigIntegerArray("biginteger.list3"));
+
+ // array of BigInteger objects
+ ArrayAssert.assertEquals(expected, conf.getBigIntegerArray("biginteger.list4"));
+
+ // list of BigInteger objects
+ ArrayAssert.assertEquals(expected, conf.getBigIntegerArray("biginteger.list6"));
+
+ // list of interpolated values
+ ArrayAssert.assertEquals(expected, conf.getBigIntegerArray("biginteger.list.interpolated"));
+
+ // single BigInteger values
+ ArrayAssert.assertEquals(new BigInteger[] { new BigInteger("1") }, conf.getBigIntegerArray("biginteger.string"));
+ ArrayAssert.assertEquals(new BigInteger[] { new BigInteger("1") }, conf.getBigIntegerArray("biginteger.object"));
+
+ // empty array
+ ArrayAssert.assertEquals(new BigInteger[] { }, conf.getBigIntegerArray("empty"));
+ }
+
+ @Test
+ public void testGetBigIntegerList()
+ {
+ // missing list
+ ListAssert.assertEquals(null, conf.getBigIntegerList("biginteger.list", null));
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add(new BigInteger("1"));
+ expected.add(new BigInteger("2"));
+
+ // list of strings
+ ListAssert.assertEquals(expected, conf.getBigIntegerList("biginteger.list1"));
+
+ // list of strings, comma separated
+ ListAssert.assertEquals(expected, conf.getBigIntegerList("biginteger.list2"));
+
+ // list of BigInteger objects
+ ListAssert.assertEquals(expected, conf.getBigIntegerList("biginteger.list3"));
+
+ // array of BigInteger objects
+ ListAssert.assertEquals(expected, conf.getBigIntegerList("biginteger.list4"));
+
+ // list of BigInteger objects
+ ListAssert.assertEquals(expected, conf.getBigIntegerList("biginteger.list6"));
+
+ // list of interpolated values
+ ListAssert.assertEquals(expected, conf.getBigIntegerList("biginteger.list.interpolated"));
+
+ // single BigInteger values
+ expected = new ArrayList<Object>();
+ expected.add(new BigInteger("1"));
+ ListAssert.assertEquals(expected, conf.getBigIntegerList("biginteger.string"));
+ ListAssert.assertEquals(expected, conf.getBigIntegerList("biginteger.object"));
+
+ // empty list
+ ListAssert.assertEquals(new ArrayList<Object>(), conf.getBigIntegerList("empty"));
+ }
+
+ @Test
+ public void testGetBigDecimalArray()
+ {
+ // missing list
+ BigDecimal[] defaultValue = new BigDecimal[] { new BigDecimal("2"), new BigDecimal("1") };
+ ArrayAssert.assertEquals(defaultValue, conf.getBigDecimalArray("bigdecimal.list", defaultValue));
+
+ BigDecimal[] expected = new BigDecimal[] { new BigDecimal("1"), new BigDecimal("2") };
+
+ // list of strings
+ ArrayAssert.assertEquals(expected, conf.getBigDecimalArray("bigdecimal.list1"));
+
+ // list of strings, comma separated
+ ArrayAssert.assertEquals(expected, conf.getBigDecimalArray("bigdecimal.list2"));
+
+ // list of BigDecimal objects
+ ArrayAssert.assertEquals(expected, conf.getBigDecimalArray("bigdecimal.list3"));
+
+ // array of BigDecimal objects
+ ArrayAssert.assertEquals(expected, conf.getBigDecimalArray("bigdecimal.list4"));
+
+ // list of BigDecimal objects
+ ArrayAssert.assertEquals(expected, conf.getBigDecimalArray("bigdecimal.list6"));
+
+ // list of interpolated values
+ ArrayAssert.assertEquals(expected, conf.getBigDecimalArray("bigdecimal.list.interpolated"));
+
+ // single BigDecimal values
+ ArrayAssert.assertEquals(new BigDecimal[] { new BigDecimal("1") }, conf.getBigDecimalArray("bigdecimal.string"));
+ ArrayAssert.assertEquals(new BigDecimal[] { new BigDecimal("1") }, conf.getBigDecimalArray("bigdecimal.object"));
+
+ // empty array
+ ArrayAssert.assertEquals(new BigDecimal[] { }, conf.getBigDecimalArray("empty"));
+ }
+
+ @Test
+ public void testGetBigDecimalList()
+ {
+ // missing list
+ ListAssert.assertEquals(null, conf.getBigDecimalList("bigdecimal.list", null));
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add(new BigDecimal("1"));
+ expected.add(new BigDecimal("2"));
+
+ // list of strings
+ ListAssert.assertEquals(expected, conf.getBigDecimalList("bigdecimal.list1"));
+
+ // list of strings, comma separated
+ ListAssert.assertEquals(expected, conf.getBigDecimalList("bigdecimal.list2"));
+
+ // list of BigDecimal objects
+ ListAssert.assertEquals(expected, conf.getBigDecimalList("bigdecimal.list3"));
+
+ // array of BigDecimal objects
+ ListAssert.assertEquals(expected, conf.getBigDecimalList("bigdecimal.list4"));
+
+ // list of BigDecimal objects
+ ListAssert.assertEquals(expected, conf.getBigDecimalList("bigdecimal.list6"));
+
+ // list of interpolated values
+ ListAssert.assertEquals(expected, conf.getBigDecimalList("bigdecimal.list.interpolated"));
+
+ // single BigDecimal values
+ expected = new ArrayList<Object>();
+ expected.add(new BigDecimal("1"));
+ ListAssert.assertEquals(expected, conf.getBigDecimalList("bigdecimal.string"));
+ ListAssert.assertEquals(expected, conf.getBigDecimalList("bigdecimal.object"));
+
+ // empty list
+ ListAssert.assertEquals(new ArrayList<Object>(), conf.getBigDecimalList("empty"));
+ }
+
+ @Test
+ public void testGetURL() throws Exception
+ {
+ // missing URL
+ URL defaultValue = new URL("http://www.google.com");
+ assertEquals(defaultValue, conf.getURL("url", defaultValue));
+
+ URL expected = new URL("http://jakarta.apache.org");
+
+ // URL string
+ assertEquals(expected, conf.getURL("url.string"));
+
+ // URL object
+ assertEquals(expected, conf.getURL("url.object"));
+
+ // interpolated value
+ assertEquals(expected, conf.getURL("url.string.interpolated"));
+ }
+
+ @Test
+ public void testGetURLArray() throws Exception
+ {
+ // missing list
+ URL[] defaultValue = new URL[] { new URL("http://www.apache.org"), new URL("http://jakarta.apache.org") };
+ ArrayAssert.assertEquals(defaultValue, conf.getURLArray("url.list", defaultValue));
+
+ URL[] expected = new URL[] { new URL("http://jakarta.apache.org"), new URL("http://www.apache.org") };
+
+ // list of strings
+ ArrayAssert.assertEquals(expected, conf.getURLArray("url.list1"));
+
+ // list of strings, comma separated
+ ArrayAssert.assertEquals(expected, conf.getURLArray("url.list2"));
+
+ // list of URL objects
+ ArrayAssert.assertEquals(expected, conf.getURLArray("url.list3"));
+
+ // array of URL objects
+ ArrayAssert.assertEquals(expected, conf.getURLArray("url.list4"));
+
+ // list of URL objects
+ ArrayAssert.assertEquals(expected, conf.getURLArray("url.list6"));
+
+ // list of interpolated values
+ ArrayAssert.assertEquals(expected, conf.getURLArray("url.list.interpolated"));
+
+ // single URL values
+ ArrayAssert.assertEquals(new URL[] { new URL("http://jakarta.apache.org") }, conf.getURLArray("url.string"));
+ ArrayAssert.assertEquals(new URL[] { new URL("http://jakarta.apache.org") }, conf.getURLArray("url.object"));
+
+ // empty array
+ ArrayAssert.assertEquals(new URL[] { }, conf.getURLArray("empty"));
+ }
+
+ @Test
+ public void testGetURLList() throws Exception
+ {
+ // missing list
+ ListAssert.assertEquals(null, conf.getURLList("url.list", null));
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add(new URL("http://jakarta.apache.org"));
+ expected.add(new URL("http://www.apache.org"));
+
+ // list of strings
+ ListAssert.assertEquals(expected, conf.getURLList("url.list1"));
+
+ // list of strings, comma separated
+ ListAssert.assertEquals(expected, conf.getURLList("url.list2"));
+
+ // list of URL objects
+ ListAssert.assertEquals(expected, conf.getURLList("url.list3"));
+
+ // array of URL objects
+ ListAssert.assertEquals(expected, conf.getURLList("url.list4"));
+
+ // list of URL objects
+ ListAssert.assertEquals(expected, conf.getURLList("url.list6"));
+
+ // list of interpolated values
+ ListAssert.assertEquals(expected, conf.getURLList("url.list.interpolated"));
+
+ // single URL values
+ expected = new ArrayList<Object>();
+ expected.add(new URL("http://jakarta.apache.org"));
+ ListAssert.assertEquals(expected, conf.getURLList("url.string"));
+ ListAssert.assertEquals(expected, conf.getURLList("url.object"));
+
+ // empty list
+ ListAssert.assertEquals(new ArrayList<Object>(), conf.getURLList("empty"));
+ }
+
+ @Test
+ public void testGetLocale()
+ {
+ // language
+ conf.setProperty("locale", "fr");
+ assertEquals("language", new Locale("fr", ""), conf.getLocale("locale"));
+
+ // language + variant
+ conf.setProperty("locale", "fr__POSIX");
+ assertEquals("language + variant", new Locale("fr", "", "POSIX"), conf.getLocale("locale"));
+
+ // country
+ conf.setProperty("locale", "_FR");
+ assertEquals("country", new Locale("", "FR"), conf.getLocale("locale"));
+
+ // country + variant
+ conf.setProperty("locale", "_FR_WIN");
+ assertEquals("country + variant", new Locale("", "FR", "WIN"), conf.getLocale("locale"));
+
+ // language + country
+ conf.setProperty("locale", "fr_FR");
+ assertEquals("language + country", new Locale("fr", "FR"), conf.getLocale("locale"));
+
+ // language + country + variant
+ conf.setProperty("locale", "fr_FR_MAC");
+ assertEquals("language + country + variant", new Locale("fr", "FR", "MAC"), conf.getLocale("locale"));
+
+ // default value
+ conf.setProperty("locale", "fr");
+ assertEquals("Existing key with default value", Locale.FRENCH, conf.getLocale("locale", Locale.GERMAN));
+ assertEquals("Missing key with default value", Locale.GERMAN, conf.getLocale("localeNotInConfig", Locale.GERMAN));
+
+ // interpolated value
+ assertEquals(Locale.FRENCH, conf.getLocale("locale.string.interpolated"));
+ }
+
+ @Test
+ public void testGetLocaleArray() throws Exception
+ {
+ // missing list
+ Locale[] defaultValue = new Locale[] { Locale.GERMAN, Locale.FRENCH };
+ ArrayAssert.assertEquals(defaultValue, conf.getLocaleArray("locale.list", defaultValue));
+
+ Locale[] expected = new Locale[] { Locale.FRENCH, Locale.GERMAN };
+
+ // list of strings
+ ArrayAssert.assertEquals(expected, conf.getLocaleArray("locale.list1"));
+
+ // list of strings, comma separated
+ ArrayAssert.assertEquals(expected, conf.getLocaleArray("locale.list2"));
+
+ // list of Locale objects
+ ArrayAssert.assertEquals(expected, conf.getLocaleArray("locale.list3"));
+
+ // array of Locale objects
+ ArrayAssert.assertEquals(expected, conf.getLocaleArray("locale.list4"));
+
+ // list of Locale objects
+ ArrayAssert.assertEquals(expected, conf.getLocaleArray("locale.list6"));
+
+ // list of interpolated values
+ ArrayAssert.assertEquals(expected, conf.getLocaleArray("locale.list.interpolated"));
+
+ // single Locale values
+ ArrayAssert.assertEquals(new Locale[] { Locale.FRENCH }, conf.getLocaleArray("locale.string"));
+ ArrayAssert.assertEquals(new Locale[] { Locale.FRENCH }, conf.getLocaleArray("locale.object"));
+
+ // empty array
+ ArrayAssert.assertEquals(new Locale[] { }, conf.getLocaleArray("empty"));
+ }
+
+ @Test
+ public void testGetLocaleList() throws Exception
+ {
+ // missing list
+ ListAssert.assertEquals(null, conf.getLocaleList("locale.list", null));
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add(Locale.FRENCH);
+ expected.add(Locale.GERMAN);
+
+ // list of strings
+ ListAssert.assertEquals(expected, conf.getLocaleList("locale.list1"));
+
+ // list of strings, comma separated
+ ListAssert.assertEquals(expected, conf.getLocaleList("locale.list2"));
+
+ // list of Locale objects
+ ListAssert.assertEquals(expected, conf.getLocaleList("locale.list3"));
+
+ // array of Locale objects
+ ListAssert.assertEquals(expected, conf.getLocaleList("locale.list4"));
+
+ // list of Locale objects
+ ListAssert.assertEquals(expected, conf.getLocaleList("locale.list6"));
+
+ // list of interpolated values
+ ListAssert.assertEquals(expected, conf.getLocaleList("locale.list.interpolated"));
+
+ // single Locale values
+ expected = new ArrayList<Object>();
+ expected.add(Locale.FRENCH);
+ ListAssert.assertEquals(expected, conf.getLocaleList("locale.string"));
+ ListAssert.assertEquals(expected, conf.getLocaleList("locale.object"));
+
+ // empty list
+ ListAssert.assertEquals(new ArrayList<Object>(), conf.getLocaleList("empty"));
+ }
+
+ @Test
+ public void testGetColor()
+ {
+ // RRGGBB
+ conf.setProperty("color", "FF0000");
+ assertEquals("color", Color.red, conf.getColor("color"));
+
+ // #RRGGBB
+ conf.setProperty("color", "#00FF00");
+ assertEquals("color", Color.green, conf.getColor("color"));
+
+ // #RRGGBBAA
+ conf.setProperty("color", "#01030507");
+ Color color = conf.getColor("color");
+ assertNotNull("null color", color);
+ assertEquals("red", 1, color.getRed());
+ assertEquals("green", 3, color.getGreen());
+ assertEquals("blue", 5, color.getBlue());
+ assertEquals("alpha", 7, color.getAlpha());
+
+ // interpolated value
+ assertEquals(Color.red, conf.getColor("color.string.interpolated"));
+
+ // default value
+ assertEquals(Color.cyan, conf.getColor("unknownkey", Color.cyan));
+ }
+
+ @Test
+ public void testGetColorArray() throws Exception
+ {
+ // missing list
+ Color[] defaultValue = new Color[] { Color.red, Color.blue };
+ ArrayAssert.assertEquals(defaultValue, conf.getColorArray("color.list", defaultValue));
+
+ Color[] expected = new Color[] { Color.red, Color.blue };
+
+ // list of strings
+ ArrayAssert.assertEquals(expected, conf.getColorArray("color.list1"));
+
+ // list of strings, comma separated
+ ArrayAssert.assertEquals(expected, conf.getColorArray("color.list2"));
+
+ // list of Color objects
+ ArrayAssert.assertEquals(expected, conf.getColorArray("color.list3"));
+
+ // array of Color objects
+ ArrayAssert.assertEquals(expected, conf.getColorArray("color.list4"));
+
+ // list of Color objects
+ ArrayAssert.assertEquals(expected, conf.getColorArray("color.list6"));
+
+ // list of interpolated values
+ ArrayAssert.assertEquals(expected, conf.getColorArray("color.list.interpolated"));
+
+ // single Color values
+ ArrayAssert.assertEquals(new Color[] { Color.red }, conf.getColorArray("color.string"));
+ ArrayAssert.assertEquals(new Color[] { Color.red }, conf.getColorArray("color.object"));
+
+ // empty array
+ ArrayAssert.assertEquals(new Color[] { }, conf.getColorArray("empty"));
+ }
+
+ @Test
+ public void testGetColorList() throws Exception
+ {
+ // missing list
+ ListAssert.assertEquals(null, conf.getColorList("color.list", null));
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add(Color.red);
+ expected.add(Color.blue);
+
+ // list of strings
+ ListAssert.assertEquals(expected, conf.getColorList("color.list1"));
+
+ // list of strings, comma separated
+ ListAssert.assertEquals(expected, conf.getColorList("color.list2"));
+
+ // list of Color objects
+ ListAssert.assertEquals(expected, conf.getColorList("color.list3"));
+
+ // array of Color objects
+ ListAssert.assertEquals(expected, conf.getColorList("color.list4"));
+
+ // list of Color objects
+ ListAssert.assertEquals(expected, conf.getColorList("color.list6"));
+
+ // list of interpolated values
+ ListAssert.assertEquals(expected, conf.getColorList("color.list.interpolated"));
+
+ // single Color values
+ expected = new ArrayList<Object>();
+ expected.add(Color.red);
+ ListAssert.assertEquals(expected, conf.getColorList("color.string"));
+ ListAssert.assertEquals(expected, conf.getColorList("color.object"));
+
+ // empty list
+ ListAssert.assertEquals(new ArrayList<Object>(), conf.getColorList("empty"));
+ }
+
+ @Test
+ public void testGetDate() throws Exception
+ {
+ DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
+
+ // missing Date
+ Date defaultValue = new Date();
+ assertEquals(defaultValue, conf.getDate("date", defaultValue));
+ assertNull("non null object for a missing key", conf.getDate("unknownkey", "yyyy-MM-dd"));
+
+ conf.setThrowExceptionOnMissing(true);
+
+ try
+ {
+ conf.getDate("unknownkey", "yyyy-MM-dd");
+ fail("NoSuchElementException should be thrown for missing properties");
+ }
+ catch (NoSuchElementException e)
+ {
+ // expected
+ }
+
+ Date expected = format.parse("2004-01-01");
+
+ // Date string
+ assertEquals(expected, conf.getDate("date.string"));
+ assertEquals(expected, conf.getDate("date.string", "yyyy-MM-dd"));
+
+ // Date object
+ assertEquals(expected, conf.getDate("date.object"));
+
+ // Calendar object
+ assertEquals(expected, conf.getDate("calendar.object"));
+
+ // interpolated value
+ assertEquals(expected, conf.getDate("date.string.interpolated"));
+ }
+
+ @Test
+ public void testGetDateArray() throws Exception
+ {
+ DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
+ Date date1 = format.parse("2004-01-01");
+ Date date2 = format.parse("2004-12-31");
+
+ // missing list
+ Date[] defaultValue = new Date[] { date2, date1 };
+ ArrayAssert.assertEquals(defaultValue, conf.getDateArray("date.list", defaultValue));
+
+ Date[] expected = new Date[] { date1, date2 };
+
+ // list of strings
+ ArrayAssert.assertEquals(expected, conf.getDateArray("date.list1"));
+
+ // list of strings, comma separated
+ ArrayAssert.assertEquals(expected, conf.getDateArray("date.list2"));
+
+ // list of Date objects
+ ArrayAssert.assertEquals(expected, conf.getDateArray("date.list3"));
+
+ // array of Date objects
+ ArrayAssert.assertEquals(expected, conf.getDateArray("date.list4"));
+
+ // list of Calendar objects
+ ArrayAssert.assertEquals(expected, conf.getDateArray("date.list5"));
+
+ // list of Date objects
+ ArrayAssert.assertEquals(expected, conf.getDateArray("date.list6"));
+
+ // list of interpolated values
+ ArrayAssert.assertEquals(expected, conf.getDateArray("date.list.interpolated"));
+
+ // single Date values
+ ArrayAssert.assertEquals(new Date[] { date1 }, conf.getDateArray("date.string"));
+ ArrayAssert.assertEquals(new Date[] { date1 }, conf.getDateArray("date.object"));
+
+ // empty array
+ ArrayAssert.assertEquals(new Date[] { }, conf.getDateArray("empty"));
+ }
+
+ @Test
+ public void testGetDateArrayWithFormat() throws Exception
+ {
+ DateFormat format = new SimpleDateFormat("MM/dd/yyyy");
+ Date date1 = format.parse("01/01/2004");
+ Date date2 = format.parse("12/31/2004");
+ Date[] expected = new Date[] { date1, date2 };
+
+ conf.addProperty("date.format", "01/01/2004");
+ conf.addProperty("date.format", "12/31/2004");
+ ArrayAssert.assertEquals("Wrong dates with format", expected, conf.getDateArray("date.format", "MM/dd/yyyy"));
+ }
+
+ @Test
+ public void testGetDateList() throws Exception
+ {
+ DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
+ Date date1 = format.parse("2004-01-01");
+ Date date2 = format.parse("2004-12-31");
+
+ // missing list
+ List<Date> nullList = null;
+ ListAssert.assertEquals(null, conf.getDateList("date.list", nullList));
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add(date1);
+ expected.add(date2);
+
+ // list of strings
+ ListAssert.assertEquals(expected, conf.getDateList("date.list1"));
+ ListAssert.assertEquals(expected, conf.getList(Date.class, "date.list1"));
+
+ // list of strings, comma separated
+ ListAssert.assertEquals(expected, conf.getDateList("date.list2"));
+
+ // list of Date objects
+ ListAssert.assertEquals(expected, conf.getDateList("date.list3"));
+
+ // array of Date objects
+ ListAssert.assertEquals(expected, conf.getDateList("date.list4"));
+
+ // list of Calendar objects
+ ListAssert.assertEquals(expected, conf.getDateList("date.list5"));
+
+ // list of Date objects
+ ListAssert.assertEquals(expected, conf.getDateList("date.list6"));
+
+ // array of strings
+ ListAssert.assertEquals(expected, conf.getList(Date.class, "date.list7"));
+
+ // list of interpolated values
+ ListAssert.assertEquals(expected, conf.getDateList("date.list.interpolated"));
+
+ // single Date values
+ expected = new ArrayList<Object>();
+ expected.add(date1);
+ ListAssert.assertEquals(expected, conf.getDateList("date.string"));
+ ListAssert.assertEquals(expected, conf.getDateList("date.object"));
+
+ // empty list
+ ListAssert.assertEquals(new ArrayList<Object>(), conf.getDateList("empty"));
+ }
+
+ @Test
+ public void testGetCalendar() throws Exception
+ {
+ DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
+
+ // missing Date
+ Calendar defaultValue = Calendar.getInstance();
+ defaultValue.setTime(new Date());
+ assertEquals(defaultValue, conf.getCalendar("calendar", defaultValue));
+ assertNull("non null object for a missing key", conf.getCalendar("unknownkey", "yyyy-MM-dd"));
+
+ conf.setThrowExceptionOnMissing(true);
+
+ try
+ {
+ conf.getCalendar("unknownkey", "yyyy-MM-dd");
+ fail("NoSuchElementException should be thrown for missing properties");
+ }
+ catch (NoSuchElementException e)
+ {
+ // expected
+ }
+
+ Calendar expected = Calendar.getInstance();
+ expected.setTime(format.parse("2004-01-01"));
+
+ // Calendar string
+ assertEquals(expected, conf.getCalendar("calendar.string"));
+ assertEquals(expected, conf.getCalendar("calendar.string", "yyyy-MM-dd"));
+
+ // Calendar object
+ assertEquals(expected, conf.getCalendar("calendar.object"));
+
+ // Date object
+ assertEquals(expected, conf.getCalendar("date.object"));
+
+ // interpolated value
+ assertEquals(expected, conf.getCalendar("calendar.string.interpolated"));
+ }
+
+ @Test
+ public void testGetCalendarArray() throws Exception
+ {
+ DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
+ Date date1 = format.parse("2004-01-01");
+ Date date2 = format.parse("2004-12-31");
+ Calendar calendar1 = Calendar.getInstance();
+ calendar1.setTime(date1);
+ Calendar calendar2 = Calendar.getInstance();
+ calendar2.setTime(date2);
+
+ // missing list
+ Calendar[] defaultValue = new Calendar[] { calendar2, calendar1 };
+ ArrayAssert.assertEquals(defaultValue, conf.getCalendarArray("calendar.list", defaultValue));
+
+ Calendar[] expected = new Calendar[] { calendar1, calendar2 };
+
+ // list of strings
+ ArrayAssert.assertEquals(expected, conf.getCalendarArray("calendar.list1"));
+
+ // list of strings, comma separated
+ ArrayAssert.assertEquals(expected, conf.getCalendarArray("calendar.list2"));
+
+ // list of Calendar objects
+ ArrayAssert.assertEquals(expected, conf.getCalendarArray("calendar.list3"));
+
+ // array of Calendar objects
+ ArrayAssert.assertEquals(expected, conf.getCalendarArray("calendar.list4"));
+
+ // list of Date objects
+ ArrayAssert.assertEquals(expected, conf.getCalendarArray("calendar.list5"));
+
+ // list of Calendar objects
+ ArrayAssert.assertEquals(expected, conf.getCalendarArray("calendar.list6"));
+
+ // list of interpolated values
+ ArrayAssert.assertEquals(expected, conf.getCalendarArray("calendar.list.interpolated"));
+
+ // single Calendar values
+ ArrayAssert.assertEquals(new Calendar[] { calendar1 }, conf.getCalendarArray("calendar.string"));
+ ArrayAssert.assertEquals(new Calendar[] { calendar1 }, conf.getCalendarArray("calendar.object"));
+
+ // empty array
+ ArrayAssert.assertEquals(new Calendar[] { }, conf.getCalendarArray("empty"));
+ }
+
+ @Test
+ public void testGetCalendarArrayWithFormat() throws Exception
+ {
+ DateFormat format = new SimpleDateFormat("MM/dd/yyyy");
+ Date date1 = format.parse("01/01/2004");
+ Date date2 = format.parse("12/31/2004");
+
+ Calendar calendar1 = Calendar.getInstance();
+ calendar1.setTime(date1);
+ Calendar calendar2 = Calendar.getInstance();
+ calendar2.setTime(date2);
+ Calendar[] expected = new Calendar[] { calendar1, calendar2 };
+
+ conf.addProperty("calendar.format", "01/01/2004");
+ conf.addProperty("calendar.format", "12/31/2004");
+ ArrayAssert.assertEquals("Wrong calendars with format", expected, conf.getCalendarArray("calendar.format", "MM/dd/yyyy"));
+ }
+
+ @Test
+ public void testGetCalendarList() throws Exception
+ {
+ DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
+ Date date1 = format.parse("2004-01-01");
+ Date date2 = format.parse("2004-12-31");
+ Calendar calendar1 = Calendar.getInstance();
+ calendar1.setTime(date1);
+ Calendar calendar2 = Calendar.getInstance();
+ calendar2.setTime(date2);
+
+ // missing list
+ List<Calendar> nullList = null;
+ ListAssert.assertEquals(null, conf.getCalendarList("calendar.list", nullList));
+
+ List<Object> expected = new ArrayList<Object>();
+ expected.add(calendar1);
+ expected.add(calendar2);
+
+ // list of strings
+ ListAssert.assertEquals(expected, conf.getCalendarList("calendar.list1"));
+ ListAssert.assertEquals(expected, conf.getList(Calendar.class, "calendar.list1"));
+
+ // list of strings, comma separated
+ ListAssert.assertEquals(expected, conf.getCalendarList("calendar.list2"));
+
+ // list of Calendar objects
+ ListAssert.assertEquals(expected, conf.getCalendarList("calendar.list3"));
+
+ // array of Calendar objects
+ ListAssert.assertEquals(expected, conf.getCalendarList("calendar.list4"));
+
+ // list of Date objects
+ ListAssert.assertEquals(expected, conf.getCalendarList("calendar.list5"));
+
+ // list of Calendar objects
+ ListAssert.assertEquals(expected, conf.getCalendarList("calendar.list6"));
+
+ // array of strings
+ ListAssert.assertEquals(expected, conf.getList(Calendar.class, "calendar.list7"));
+
+ // list of interpolated values
+ ListAssert.assertEquals(expected, conf.getCalendarList("calendar.list.interpolated"));
+
+ // single Calendar values
+ expected = new ArrayList<Object>();
+ expected.add(calendar1);
+ ListAssert.assertEquals(expected, conf.getCalendarList("date.string"));
+ ListAssert.assertEquals(expected, conf.getCalendarList("date.object"));
+
+ // empty list
+ ListAssert.assertEquals(new ArrayList<Object>(), conf.getCalendarList("empty"));
+ }
+
+ @Test
+ public void testGetInetAddress() throws Exception
+ {
+ InetAddress expected = InetAddress.getByName("127.0.0.1");
+
+ // address as string
+ assertEquals(expected, conf.get(InetAddress.class, "ip.string"));
+
+ // address object
+ assertEquals(expected, conf.get(InetAddress.class, "ip.object"));
+
+ // interpolated value
+ assertEquals(expected, conf.get(InetAddress.class, "ip.string.interpolated"));
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetInetAddressInvalidType()
+ {
+ conf.setProperty("ip.unknownhost", "random-foo-that-does-not-exist.ever");
+ conf.get(InetAddress.class, "ip.unknownhost");
+ }
+
+ @Test
+ public void testGetInternetAddress() throws Exception
+ {
+ Object expected = createInternetAddress("ebourg at apache.org");
+
+ // address as string
+ assertEquals(expected, conf.get(expected.getClass(), "email.string"));
+
+ // address object
+ assertEquals(expected, conf.get(expected.getClass(), "email.object"));
+
+ // interpolated value
+ assertEquals(expected, conf.get(expected.getClass(), "email.string.interpolated"));
+
+ conf.setProperty("email.invalid", "ebourg at apache@org");
+ try
+ {
+ conf.get(expected.getClass(), "email.invalid");
+ fail("ConversionException should be thrown for invalid emails");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testGetInternetAddressInvalidType() throws Exception
+ {
+ Object expected = createInternetAddress("ebourg at apache.org");
+ conf.setProperty("email.invalid", "ebourg at apache@org");
+ conf.get(expected.getClass(), "email.invalid");
+ }
+
+ /**
+ * Create an instance of InternetAddress. This trick is necessary to
+ * compile and run the test with Java 1.3 and the javamail-1.4 which
+ * is not compatible with Java 1.3
+ */
+ private Object createInternetAddress(String email) throws Exception
+ {
+ Class<?> cls = Class.forName("javax.mail.internet.InternetAddress");
+ return cls.getConstructor(new Class[]{String.class}).newInstance(new Object[]{email});
+ }
+
+ @Test
+ public void testConversionException() throws Exception
+ {
+ conf.addProperty("key1", new Object());
+ conf.addProperty("key2", "xxxxxx");
+
+ try
+ {
+ conf.getBooleanArray("key1");
+ fail("getBooleanArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getBooleanArray("key2");
+ fail("getBooleanArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getBooleanList("key1");
+ fail("getBooleanList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getBooleanList("key2");
+ fail("getBooleanList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getByteArray("key1");
+ fail("getByteArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getByteArray("key2");
+ fail("getByteArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getByteList("key1");
+ fail("getByteList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getByteList("key2");
+ fail("getByteList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getShortArray("key1");
+ fail("getShortArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getShortArray("key2");
+ fail("getShortArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getShortList("key1");
+ fail("getShortList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getShortList("key2");
+ fail("getShortList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getIntArray("key1");
+ fail("getIntArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getIntArray("key2");
+ fail("getIntArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getIntegerList("key1");
+ fail("getIntegerList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getIntegerList("key2");
+ fail("getIntegerList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getLongArray("key1");
+ fail("getLongArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getLongArray("key2");
+ fail("getLongArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getLongList("key1");
+ fail("getLongList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getLongList("key2");
+ fail("getLongList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getFloatArray("key1");
+ fail("getFloatArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getFloatArray("key2");
+ fail("getFloatArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getFloatList("key1");
+ fail("getFloatList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getFloatList("key2");
+ fail("getFloatList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getDoubleArray("key1");
+ fail("getDoubleArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getDoubleArray("key2");
+ fail("getDoubleArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getDoubleList("key1");
+ fail("getDoubleList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getDoubleList("key2");
+ fail("getDoubleList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getBigIntegerArray("key1");
+ fail("getBigIntegerArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getBigIntegerArray("key2");
+ fail("getBigIntegerArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getBigIntegerList("key1");
+ fail("getBigIntegerList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getBigIntegerList("key2");
+ fail("getBigIntegerList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getBigDecimalArray("key1");
+ fail("getBigDecimalArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getBigDecimalArray("key2");
+ fail("getBigDecimalArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getBigDecimalList("key1");
+ fail("getBigDecimalList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getBigDecimalList("key2");
+ fail("getBigDecimalList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getURLArray("key1");
+ fail("getURLArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getURLArray("key2");
+ fail("getURLArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getURLList("key1");
+ fail("getURLList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getURLList("key2");
+ fail("getURLList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getLocaleArray("key1");
+ fail("getLocaleArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getLocaleArray("key2");
+ fail("getLocaleArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getLocaleList("key1");
+ fail("getLocaleList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getLocaleList("key2");
+ fail("getLocaleList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getColorArray("key1");
+ fail("getColorArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getColorArray("key2");
+ fail("getColorArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getColorList("key1");
+ fail("getColorList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getColorList("key2");
+ fail("getColorList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getDateArray("key1");
+ fail("getDateArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getDate("key1", "yyyy-MM-dd");
+ fail("getDate didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getDate("key2", "yyyy-MM-dd");
+ fail("getDate didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getDateArray("key2");
+ fail("getDateArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getDateList("key1");
+ fail("getDateList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getDateList("key2");
+ fail("getDateList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getCalendar("key1", "yyyy-MM-dd");
+ fail("getCalendar didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getCalendar("key2","yyyy-MM-dd");
+ fail("getCalendar didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getCalendarArray("key1");
+ fail("getCalendarArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getCalendarArray("key2");
+ fail("getCalendarArray didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getCalendarList("key1");
+ fail("getCalendarList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.getCalendarList("key2");
+ fail("getCalendarList didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.get(InetAddress.class, "key1");
+ fail("getInetAddress didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ conf.get(Class.forName("javax.mail.internet.InternetAddress"), "key1");
+ fail("getInternetAddress didn't throw a ConversionException");
+ }
+ catch (ConversionException e)
+ {
+ // expected
+ }
+ }
+
+ /**
+ * Tests whether a string property can be obtained through get() if no type
+ * conversion is required.
+ */
+ @Test
+ public void testGetPropertyWithoutConversion()
+ {
+ String key = "test.str";
+ String value = "someTestValue";
+ conf.addProperty(key, value);
+ assertEquals("Wrong result", value, conf.get(String.class, key));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestDatabaseConfiguration.java b/src/test/java/org/apache/commons/configuration/TestDatabaseConfiguration.java
new file mode 100644
index 0000000..1add7cb
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestDatabaseConfiguration.java
@@ -0,0 +1,551 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.sql.DataSource;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test for database stored configurations. Note, when running this Unit
+ * Test in Eclipse it sometimes takes a couple tries. Otherwise you may get
+ * database is already in use by another process errors.
+ *
+ * @version $Id: TestDatabaseConfiguration.java 1223016 2011-12-24 20:56:52Z oheger $
+ */
+public class TestDatabaseConfiguration
+{
+ /** Constant for another configuration name. */
+ private static final String CONFIG_NAME2 = "anotherTestConfig";
+
+ /** An error listener for testing whether internal errors occurred.*/
+ private ConfigurationErrorListenerImpl listener;
+
+ /** The test helper. */
+ private DatabaseConfigurationTestHelper helper;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ /*
+ * Thread.sleep may or may not help with the database is already in
+ * use exception.
+ */
+ //Thread.sleep(1000);
+
+ // set up the datasource
+
+ helper = new DatabaseConfigurationTestHelper();
+ helper.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception
+ {
+ // if an error listener is defined, we check whether an error occurred
+ if(listener != null)
+ {
+ assertEquals("An internal error occurred", 0, listener.getErrorCount());
+ }
+ helper.tearDown();
+ }
+
+ /**
+ * Creates a database configuration with default values.
+ *
+ * @return the configuration
+ */
+ private PotentialErrorDatabaseConfiguration setUpConfig()
+ {
+ return new PotentialErrorDatabaseConfiguration(helper.getDatasource(),
+ DatabaseConfigurationTestHelper.TABLE,
+ DatabaseConfigurationTestHelper.COL_KEY,
+ DatabaseConfigurationTestHelper.COL_VALUE);
+ }
+
+ /**
+ * Creates an error listener and adds it to the specified configuration.
+ *
+ * @param config the configuration
+ */
+ private void setUpErrorListener(PotentialErrorDatabaseConfiguration config)
+ {
+ // remove log listener to avoid exception longs
+ config.removeErrorListener(config.getErrorListeners().iterator().next());
+ listener = new ConfigurationErrorListenerImpl();
+ config.addErrorListener(listener);
+ config.failOnConnect = true;
+ }
+
+ /**
+ * Prepares a test for a database error. Sets up a config and registers an
+ * error listener.
+ *
+ * @return the initialized configuration
+ */
+ private PotentialErrorDatabaseConfiguration setUpErrorConfig()
+ {
+ PotentialErrorDatabaseConfiguration config = setUpConfig();
+ setUpErrorListener(config);
+ return config;
+ }
+
+ /**
+ * Checks the error listener for an expected error. The properties of the
+ * error event will be compared with the expected values.
+ *
+ * @param type the expected type of the error event
+ * @param key the expected property key
+ * @param value the expected property value
+ */
+ private void checkErrorListener(int type, String key, Object value)
+ {
+ listener.verify(type, key, value);
+ assertTrue(
+ "Wrong event source",
+ listener.getLastEvent().getSource() instanceof DatabaseConfiguration);
+ assertTrue("Wrong exception",
+ listener.getLastEvent().getCause() instanceof SQLException);
+ listener = null; // mark as checked
+ }
+
+ /**
+ * Tests the default value of the doCommits property.
+ */
+ @Test
+ public void testDoCommitsDefault()
+ {
+ DatabaseConfiguration config = new DatabaseConfiguration(helper
+ .getDatasource(), DatabaseConfigurationTestHelper.TABLE,
+ DatabaseConfigurationTestHelper.COL_KEY,
+ DatabaseConfigurationTestHelper.COL_VALUE);
+ assertFalse("Wrong commits flag", config.isDoCommits());
+ }
+
+ /**
+ * Tests the default value of the doCommits property for multiple
+ * configurations in a table.
+ */
+ @Test
+ public void testDoCommitsDefaultMulti()
+ {
+ DatabaseConfiguration config = new DatabaseConfiguration(helper
+ .getDatasource(), DatabaseConfigurationTestHelper.TABLE,
+ DatabaseConfigurationTestHelper.COL_NAME,
+ DatabaseConfigurationTestHelper.COL_KEY,
+ DatabaseConfigurationTestHelper.COL_VALUE,
+ DatabaseConfigurationTestHelper.CONFIG_NAME);
+ assertFalse("Wrong commits flag", config.isDoCommits());
+ }
+
+ @Test
+ public void testAddPropertyDirectSingle()
+ {
+ DatabaseConfiguration config = helper.setUpConfig();
+ config.addPropertyDirect("key", "value");
+
+ assertTrue("missing property", config.containsKey("key"));
+ }
+
+ /**
+ * Tests whether a commit is performed after a property was added.
+ */
+ @Test
+ public void testAddPropertyDirectCommit()
+ {
+ helper.setAutoCommit(false);
+ DatabaseConfiguration config = helper.setUpConfig();
+ config.addPropertyDirect("key", "value");
+ assertTrue("missing property", config.containsKey("key"));
+ }
+
+ @Test
+ public void testAddPropertyDirectMultiple()
+ {
+ DatabaseConfiguration config = helper.setUpMultiConfig();
+ config.addPropertyDirect("key", "value");
+
+ assertTrue("missing property", config.containsKey("key"));
+ }
+
+ @Test
+ public void testAddNonStringProperty()
+ {
+ DatabaseConfiguration config = helper.setUpConfig();
+ config.addPropertyDirect("boolean", Boolean.TRUE);
+
+ assertTrue("missing property", config.containsKey("boolean"));
+ }
+
+ @Test
+ public void testGetPropertyDirectSingle()
+ {
+ Configuration config = setUpConfig();
+
+ assertEquals("property1", "value1", config.getProperty("key1"));
+ assertEquals("property2", "value2", config.getProperty("key2"));
+ assertEquals("unknown property", null, config.getProperty("key3"));
+ }
+
+ @Test
+ public void testGetPropertyDirectMultiple()
+ {
+ Configuration config = helper.setUpMultiConfig();
+
+ assertEquals("property1", "value1", config.getProperty("key1"));
+ assertEquals("property2", "value2", config.getProperty("key2"));
+ assertEquals("unknown property", null, config.getProperty("key3"));
+ }
+
+ @Test
+ public void testClearPropertySingle()
+ {
+ Configuration config = helper.setUpConfig();
+ config.clearProperty("key1");
+
+ assertFalse("property not cleared", config.containsKey("key1"));
+ }
+
+ @Test
+ public void testClearPropertyMultiple()
+ {
+ Configuration config = helper.setUpMultiConfig();
+ config.clearProperty("key1");
+
+ assertFalse("property not cleared", config.containsKey("key1"));
+ }
+
+ /**
+ * Tests that another configuration is not affected when clearing
+ * properties.
+ */
+ @Test
+ public void testClearPropertyMultipleOtherConfig()
+ {
+ DatabaseConfiguration config = helper.setUpMultiConfig();
+ DatabaseConfiguration config2 = helper.setUpMultiConfig(CONFIG_NAME2);
+ config2.addProperty("key1", "some test");
+ config.clearProperty("key1");
+ assertFalse("property not cleared", config.containsKey("key1"));
+ assertTrue("Property cleared in other config", config2
+ .containsKey("key1"));
+ }
+
+ /**
+ * Tests whether a commit is performed after a property was cleared.
+ */
+ @Test
+ public void testClearPropertyCommit()
+ {
+ helper.setAutoCommit(false);
+ Configuration config = helper.setUpConfig();
+ config.clearProperty("key1");
+ assertFalse("property not cleared", config.containsKey("key1"));
+ }
+
+ @Test
+ public void testClearSingle()
+ {
+ Configuration config = helper.setUpConfig();
+ config.clear();
+
+ assertTrue("configuration is not cleared", config.isEmpty());
+ }
+
+ @Test
+ public void testClearMultiple()
+ {
+ Configuration config = helper.setUpMultiConfig();
+ config.clear();
+
+ assertTrue("configuration is not cleared", config.isEmpty());
+ }
+
+ /**
+ * Tests whether a commit is performed after a clear operation.
+ */
+ @Test
+ public void testClearCommit()
+ {
+ helper.setAutoCommit(false);
+ Configuration config = helper.setUpConfig();
+ config.clear();
+ assertTrue("configuration is not cleared", config.isEmpty());
+ }
+
+ @Test
+ public void testGetKeysSingle()
+ {
+ Configuration config = setUpConfig();
+ Iterator<String> it = config.getKeys();
+
+ assertEquals("1st key", "key1", it.next());
+ assertEquals("2nd key", "key2", it.next());
+ }
+
+ @Test
+ public void testGetKeysMultiple()
+ {
+ Configuration config = helper.setUpMultiConfig();
+ Iterator<String> it = config.getKeys();
+
+ assertEquals("1st key", "key1", it.next());
+ assertEquals("2nd key", "key2", it.next());
+ }
+
+ @Test
+ public void testContainsKeySingle()
+ {
+ Configuration config = setUpConfig();
+ assertTrue("missing key1", config.containsKey("key1"));
+ assertTrue("missing key2", config.containsKey("key2"));
+ }
+
+ @Test
+ public void testContainsKeyMultiple()
+ {
+ Configuration config = helper.setUpMultiConfig();
+ assertTrue("missing key1", config.containsKey("key1"));
+ assertTrue("missing key2", config.containsKey("key2"));
+ }
+
+ @Test
+ public void testIsEmptySingle()
+ {
+ Configuration config1 = setUpConfig();
+ assertFalse("The configuration is empty", config1.isEmpty());
+ }
+
+ @Test
+ public void testIsEmptyMultiple()
+ {
+ Configuration config1 = helper.setUpMultiConfig();
+ assertFalse("The configuration named 'test' is empty", config1.isEmpty());
+
+ Configuration config2 = new DatabaseConfiguration(helper.getDatasource(), DatabaseConfigurationTestHelper.TABLE_MULTI, DatabaseConfigurationTestHelper.COL_NAME, DatabaseConfigurationTestHelper.COL_KEY, DatabaseConfigurationTestHelper.COL_VALUE, "testIsEmpty");
+ assertTrue("The configuration named 'testIsEmpty' is not empty", config2.isEmpty());
+ }
+
+ @Test
+ public void testGetList()
+ {
+ Configuration config1 = new DatabaseConfiguration(helper.getDatasource(), "configurationList", DatabaseConfigurationTestHelper.COL_KEY, DatabaseConfigurationTestHelper.COL_VALUE);
+ List<Object> list = config1.getList("key3");
+ assertEquals(3,list.size());
+ }
+
+ @Test
+ public void testGetKeys()
+ {
+ Configuration config1 = new DatabaseConfiguration(helper.getDatasource(), "configurationList", DatabaseConfigurationTestHelper.COL_KEY, DatabaseConfigurationTestHelper.COL_VALUE);
+ Iterator<String> i = config1.getKeys();
+ assertTrue(i.hasNext());
+ Object key = i.next();
+ assertEquals("key3",key.toString());
+ assertFalse(i.hasNext());
+ }
+
+ @Test
+ public void testClearSubset()
+ {
+ Configuration config = setUpConfig();
+
+ Configuration subset = config.subset("key1");
+ subset.clear();
+
+ assertTrue("the subset is not empty", subset.isEmpty());
+ assertFalse("the parent configuration is empty", config.isEmpty());
+ }
+
+ /**
+ * Tests whether the configuration has already an error listener registered
+ * that is used for logging.
+ */
+ @Test
+ public void testLogErrorListener()
+ {
+ DatabaseConfiguration config = new DatabaseConfiguration(helper.getDatasource(), DatabaseConfigurationTestHelper.TABLE, DatabaseConfigurationTestHelper.COL_KEY, DatabaseConfigurationTestHelper.COL_VALUE);
+ assertEquals("No error listener registered", 1, config.getErrorListeners().size());
+ }
+
+ /**
+ * Tests handling of errors in getProperty().
+ */
+ @Test
+ public void testGetPropertyError()
+ {
+ setUpErrorConfig().getProperty("key1");
+ checkErrorListener(AbstractConfiguration.EVENT_READ_PROPERTY, "key1", null);
+ }
+
+ /**
+ * Tests handling of errors in addPropertyDirect().
+ */
+ @Test
+ public void testAddPropertyError()
+ {
+ setUpErrorConfig().addProperty("key1", "value");
+ checkErrorListener(AbstractConfiguration.EVENT_ADD_PROPERTY, "key1", "value");
+ }
+
+ /**
+ * Tests handling of errors in isEmpty().
+ */
+ @Test
+ public void testIsEmptyError()
+ {
+ assertTrue("Wrong return value for failure", setUpErrorConfig().isEmpty());
+ checkErrorListener(AbstractConfiguration.EVENT_READ_PROPERTY, null, null);
+ }
+
+ /**
+ * Tests handling of errors in containsKey().
+ */
+ @Test
+ public void testContainsKeyError()
+ {
+ assertFalse("Wrong return value for failure", setUpErrorConfig().containsKey("key1"));
+ checkErrorListener(AbstractConfiguration.EVENT_READ_PROPERTY, "key1", null);
+ }
+
+ /**
+ * Tests handling of errors in clearProperty().
+ */
+ @Test
+ public void testClearPropertyError()
+ {
+ setUpErrorConfig().clearProperty("key1");
+ checkErrorListener(AbstractConfiguration.EVENT_CLEAR_PROPERTY, "key1", null);
+ }
+
+ /**
+ * Tests handling of errors in clear().
+ */
+ @Test
+ public void testClearError()
+ {
+ setUpErrorConfig().clear();
+ checkErrorListener(AbstractConfiguration.EVENT_CLEAR, null, null);
+ }
+
+ /**
+ * Tests handling of errors in getKeys().
+ */
+ @Test
+ public void testGetKeysError()
+ {
+ Iterator<String> it = setUpErrorConfig().getKeys();
+ checkErrorListener(AbstractConfiguration.EVENT_READ_PROPERTY, null, null);
+ assertFalse("Iteration is not empty", it.hasNext());
+ }
+
+ /**
+ * Tests obtaining a property as list whose value contains the list
+ * delimiter. Multiple values should be returned.
+ */
+ @Test
+ public void testGetListWithDelimiter()
+ {
+ DatabaseConfiguration config = setUpConfig();
+ config.setListDelimiter(';');
+ List<Object> values = config.getList("keyMulti");
+ assertEquals("Wrong number of list elements", 3, values.size());
+ assertEquals("Wrong list element 0", "a", values.get(0));
+ assertEquals("Wrong list element 2", "c", values.get(2));
+ }
+
+ /**
+ * Tests obtaining a property whose value contains the list delimiter when
+ * delimiter parsing is disabled.
+ */
+ @Test
+ public void testGetListWithDelimiterParsingDisabled()
+ {
+ DatabaseConfiguration config = setUpConfig();
+ config.setListDelimiter(';');
+ config.setDelimiterParsingDisabled(true);
+ assertEquals("Wrong value of property", "a;b;c", config.getString("keyMulti"));
+ }
+
+ /**
+ * Tests adding a property containing the list delimiter. When this property
+ * is queried multiple values should be returned.
+ */
+ @Test
+ public void testAddWithDelimiter()
+ {
+ DatabaseConfiguration config = setUpConfig();
+ config.setListDelimiter(';');
+ config.addProperty("keyList", "1;2;3");
+ String[] values = config.getStringArray("keyList");
+ assertEquals("Wrong number of property values", 3, values.length);
+ assertEquals("Wrong value at index 1", "2", values[1]);
+ }
+
+ /**
+ * Tests setProperty() if the property value contains the list delimiter.
+ */
+ @Test
+ public void testSetPropertyWithDelimiter()
+ {
+ DatabaseConfiguration config = helper.setUpMultiConfig();
+ config.setListDelimiter(';');
+ config.setProperty("keyList", "1;2;3");
+ String[] values = config.getStringArray("keyList");
+ assertEquals("Wrong number of property values", 3, values.length);
+ assertEquals("Wrong value at index 1", "2", values[1]);
+ }
+
+ /**
+ * A specialized database configuration implementation that can be
+ * configured to throw an exception when obtaining a connection. This way
+ * database exceptions can be simulated.
+ */
+ static class PotentialErrorDatabaseConfiguration extends DatabaseConfiguration
+ {
+ /** A flag whether a getConnection() call should fail. */
+ boolean failOnConnect;
+
+ public PotentialErrorDatabaseConfiguration(DataSource datasource,
+ String table, String keyColumn, String valueColumn)
+ {
+ super(datasource, table, keyColumn, valueColumn);
+ }
+
+ @Override
+ protected Connection getConnection() throws SQLException
+ {
+ if (failOnConnect)
+ {
+ throw new SQLException("Simulated DB error");
+ }
+ return super.getConnection();
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestDefaultConfigurationBuilder.java b/src/test/java/org/apache/commons/configuration/TestDefaultConfigurationBuilder.java
new file mode 100644
index 0000000..3947778
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestDefaultConfigurationBuilder.java
@@ -0,0 +1,1279 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.io.StringWriter;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.net.URL;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.configuration.beanutils.BeanHelper;
+import org.apache.commons.configuration.event.ConfigurationListenerTestImpl;
+import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.DefaultConfigurationNode;
+import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
+import org.apache.commons.lang.SystemUtils;
+import org.apache.commons.lang.text.StrLookup;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.logging.impl.Log4JLogger;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.SimpleLayout;
+import org.apache.log4j.WriterAppender;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for DefaultConfigurationBuilder.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestDefaultConfigurationBuilder.java 1301997 2012-03-17 20:32:02Z sebb $
+ */
+public class TestDefaultConfigurationBuilder
+{
+ /** Test configuration definition file. */
+ private static final File TEST_FILE = ConfigurationAssert
+ .getTestFile("testDigesterConfiguration.xml");
+
+ private static final File ADDITIONAL_FILE = ConfigurationAssert
+ .getTestFile("testDigesterConfiguration2.xml");
+
+ private static final File OPTIONAL_FILE = ConfigurationAssert
+ .getTestFile("testDigesterOptionalConfiguration.xml");
+
+ private static final File OPTIONALEX_FILE = ConfigurationAssert
+ .getTestFile("testDigesterOptionalConfigurationEx.xml");
+
+ private static final File MULTI_FILE = ConfigurationAssert
+ .getTestFile("testDigesterConfiguration3.xml");
+
+ private static final File INIT_FILE = ConfigurationAssert
+ .getTestFile("testComplexInitialization.xml");
+
+ private static final File CLASS_FILE = ConfigurationAssert
+ .getTestFile("testExtendedClass.xml");
+
+ private static final File PROVIDER_FILE = ConfigurationAssert
+ .getTestFile("testConfigurationProvider.xml");
+
+ private static final File EXTENDED_PROVIDER_FILE = ConfigurationAssert
+ .getTestFile("testExtendedXMLConfigurationProvider.xml");
+
+ private static final File GLOBAL_LOOKUP_FILE = ConfigurationAssert
+ .getTestFile("testGlobalLookup.xml");
+
+ private static final File SYSTEM_PROPS_FILE = ConfigurationAssert
+ .getTestFile("testSystemProperties.xml");
+
+ private static final File VALIDATION_FILE = ConfigurationAssert
+ .getTestFile("testValidation.xml");
+
+ private static final File VALIDATION3_FILE = ConfigurationAssert
+ .getTestFile("testValidation3.xml");
+
+ private static final File MULTI_TENENT_FILE = ConfigurationAssert
+ .getTestFile("testMultiTenentConfigurationBuilder.xml");
+
+ private static final File EXPRESSION_FILE = ConfigurationAssert
+ .getTestFile("testExpression.xml");
+
+ /** Constant for the name of an optional configuration.*/
+ private static final String OPTIONAL_NAME = "optionalConfig";
+
+ /** Stores the object to be tested. */
+ DefaultConfigurationBuilder factory;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ System
+ .setProperty("java.naming.factory.initial",
+ "org.apache.commons.configuration.MockInitialContextFactory");
+ System.setProperty("test_file_xml", "test.xml");
+ System.setProperty("test_file_combine", "testcombine1.xml");
+ factory = new DefaultConfigurationBuilder();
+ factory.clearErrorListeners(); // avoid exception messages
+ }
+
+ /**
+ * Tests the isReservedNode() method of ConfigurationDeclaration.
+ */
+ @Test
+ public void testConfigurationDeclarationIsReserved()
+ {
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory);
+ DefaultConfigurationNode parent = new DefaultConfigurationNode();
+ DefaultConfigurationNode nd = new DefaultConfigurationNode("at");
+ parent.addAttribute(nd);
+ assertTrue("Attribute at not recognized", decl.isReservedNode(nd));
+ nd = new DefaultConfigurationNode("optional");
+ parent.addAttribute(nd);
+ assertTrue("Attribute optional not recognized", decl.isReservedNode(nd));
+ nd = new DefaultConfigurationNode("config-class");
+ parent.addAttribute(nd);
+ assertTrue("Inherited attribute not recognized", decl
+ .isReservedNode(nd));
+ nd = new DefaultConfigurationNode("different");
+ parent.addAttribute(nd);
+ assertFalse("Wrong reserved attribute", decl.isReservedNode(nd));
+ nd = new DefaultConfigurationNode("at");
+ parent.addChild(nd);
+ assertFalse("Node type not evaluated", decl.isReservedNode(nd));
+ }
+
+ /**
+ * Tests if the at attribute is correctly detected as reserved attribute.
+ */
+ @Test
+ public void testConfigurationDeclarationIsReservedAt()
+ {
+ checkOldReservedAttribute("at");
+ }
+
+ /**
+ * Tests if the optional attribute is correctly detected as reserved
+ * attribute.
+ */
+ @Test
+ public void testConfigurationDeclarationIsReservedOptional()
+ {
+ checkOldReservedAttribute("optional");
+ }
+
+ /**
+ * Tests if special reserved attributes are recognized by the
+ * isReservedNode() method. For compatibility reasons the attributes "at"
+ * and "optional" are also treated as reserved attributes, but only if there
+ * are no corresponding attributes with the "config-" prefix.
+ *
+ * @param name the attribute name
+ */
+ private void checkOldReservedAttribute(String name)
+ {
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory);
+ DefaultConfigurationNode parent = new DefaultConfigurationNode();
+ DefaultConfigurationNode nd = new DefaultConfigurationNode("config-"
+ + name);
+ parent.addAttribute(nd);
+ assertTrue("config-" + name + " attribute not recognized", decl
+ .isReservedNode(nd));
+ DefaultConfigurationNode nd2 = new DefaultConfigurationNode(name);
+ parent.addAttribute(nd2);
+ assertFalse(name + " is reserved though config- exists", decl
+ .isReservedNode(nd2));
+ assertTrue("config- attribute not recognized when " + name + " exists",
+ decl.isReservedNode(nd));
+ }
+
+ /**
+ * Tests access to certain reserved attributes of a
+ * ConfigurationDeclaration.
+ */
+ public void testConfigurationDeclarationGetAttributes()
+ {
+ factory.addProperty("xml.fileName", "test.xml");
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory.configurationAt("xml"));
+ assertNull("Found an at attribute", decl.getAt());
+ assertFalse("Found an optional attribute", decl.isOptional());
+ factory.addProperty("xml[@config-at]", "test1");
+ assertEquals("Wrong value of at attribute", "test1", decl.getAt());
+ factory.addProperty("xml[@at]", "test2");
+ assertEquals("Wrong value of config-at attribute", "test1", decl.getAt());
+ factory.clearProperty("xml[@config-at]");
+ assertEquals("Old at attribute not detected", "test2", decl.getAt());
+ factory.addProperty("xml[@config-optional]", "true");
+ assertTrue("Wrong value of optional attribute", decl.isOptional());
+ factory.addProperty("xml[@optional]", "false");
+ assertTrue("Wrong value of config-optional attribute", decl.isOptional());
+ factory.clearProperty("xml[@config-optional]");
+ factory.setProperty("xml[@optional]", Boolean.TRUE);
+ assertTrue("Old optional attribute not detected", decl.isOptional());
+ }
+
+ /**
+ * Tests whether an invalid value of an optional attribute is detected.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testConfigurationDeclarationOptionalAttributeInvalid()
+ {
+ factory.addProperty("xml.fileName", "test.xml");
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory.configurationAt("xml"));
+ factory.setProperty("xml[@optional]", "invalid value");
+ decl.isOptional();
+ }
+
+ /**
+ * Tests adding a new configuration provider.
+ */
+ @Test
+ public void testAddConfigurationProvider()
+ {
+ DefaultConfigurationBuilder.ConfigurationProvider provider = new DefaultConfigurationBuilder.ConfigurationProvider();
+ assertNull("Provider already registered", factory
+ .providerForTag("test"));
+ factory.addConfigurationProvider("test", provider);
+ assertSame("Provider not registered", provider, factory
+ .providerForTag("test"));
+ }
+
+ /**
+ * Tries to register a null configuration provider. This should cause an
+ * exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddConfigurationProviderNull()
+ {
+ factory.addConfigurationProvider("test", null);
+ }
+
+ /**
+ * Tries to register a configuration provider for a null tag. This should
+ * cause an exception to be thrown.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddConfigurationProviderNullTag()
+ {
+ factory.addConfigurationProvider(null,
+ new DefaultConfigurationBuilder.ConfigurationProvider());
+ }
+
+ /**
+ * Tests removing configuration providers.
+ */
+ @Test
+ public void testRemoveConfigurationProvider()
+ {
+ assertNull("Removing unknown provider", factory
+ .removeConfigurationProvider("test"));
+ assertNull("Removing provider for null tag", factory
+ .removeConfigurationProvider(null));
+ DefaultConfigurationBuilder.ConfigurationProvider provider = new DefaultConfigurationBuilder.ConfigurationProvider();
+ factory.addConfigurationProvider("test", provider);
+ assertSame("Failed to remove provider", provider, factory
+ .removeConfigurationProvider("test"));
+ assertNull("Provider still registered", factory.providerForTag("test"));
+ }
+
+ /**
+ * Tests creating a configuration object from a configuration declaration.
+ */
+ @Test
+ public void testConfigurationBeanFactoryCreateBean()
+ {
+ factory.addConfigurationProvider("test",
+ new DefaultConfigurationBuilder.ConfigurationProvider(
+ PropertiesConfiguration.class));
+ factory.addProperty("test[@throwExceptionOnMissing]", "true");
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory.configurationAt("test"));
+ PropertiesConfiguration conf = (PropertiesConfiguration) BeanHelper
+ .createBean(decl);
+ assertTrue("Property was not initialized", conf
+ .isThrowExceptionOnMissing());
+ }
+
+ /**
+ * Tests creating a configuration object from an unknown tag. This should
+ * cause an exception.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testConfigurationBeanFactoryCreateUnknownTag()
+ {
+ factory.addProperty("test[@throwExceptionOnMissing]", "true");
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory.configurationAt("test"));
+ BeanHelper.createBean(decl);
+ }
+
+ /**
+ * Tests loading a simple configuration definition file.
+ */
+ @Test
+ public void testLoadConfiguration() throws ConfigurationException
+ {
+ factory.setFile(TEST_FILE);
+ checkConfiguration();
+ }
+
+ /**
+ * Tests the file constructor.
+ */
+ @Test
+ public void testLoadConfigurationFromFile() throws ConfigurationException
+ {
+ factory = new DefaultConfigurationBuilder(TEST_FILE);
+ checkConfiguration();
+ }
+
+ /**
+ * Tests the file name constructor.
+ */
+ @Test
+ public void testLoadConfigurationFromFileName()
+ throws ConfigurationException
+ {
+ factory = new DefaultConfigurationBuilder(TEST_FILE.getAbsolutePath());
+ checkConfiguration();
+ }
+
+ /**
+ * Tests the URL constructor.
+ */
+ @Test
+ public void testLoadConfigurationFromURL() throws Exception
+ {
+ factory = new DefaultConfigurationBuilder(TEST_FILE.toURI().toURL());
+ checkConfiguration();
+ }
+
+ /**
+ * Tests if the configuration was correctly created by the factory.
+ */
+ private void checkConfiguration() throws ConfigurationException
+ {
+ CombinedConfiguration compositeConfiguration = (CombinedConfiguration) factory
+ .getConfiguration();
+
+ assertEquals("Number of configurations", 3, compositeConfiguration
+ .getNumberOfConfigurations());
+ assertEquals(PropertiesConfiguration.class, compositeConfiguration
+ .getConfiguration(0).getClass());
+ assertEquals(XMLPropertiesConfiguration.class, compositeConfiguration
+ .getConfiguration(1).getClass());
+ assertEquals(XMLConfiguration.class, compositeConfiguration
+ .getConfiguration(2).getClass());
+
+ // check the first configuration
+ PropertiesConfiguration pc = (PropertiesConfiguration) compositeConfiguration
+ .getConfiguration(0);
+ assertNotNull("Make sure we have a fileName: " + pc.getFileName(), pc
+ .getFileName());
+
+ // check some properties
+ checkProperties(compositeConfiguration);
+ }
+
+ /**
+ * Checks if the passed in configuration contains the expected properties.
+ *
+ * @param compositeConfiguration the configuration to check
+ */
+ private void checkProperties(Configuration compositeConfiguration)
+ {
+ assertTrue("Make sure we have loaded our key", compositeConfiguration
+ .getBoolean("test.boolean"));
+ assertEquals("I'm complex!", compositeConfiguration
+ .getProperty("element2.subelement.subsubelement"));
+ assertEquals("property in the XMLPropertiesConfiguration", "value1",
+ compositeConfiguration.getProperty("key1"));
+ }
+
+ /**
+ * Tests loading a configuration definition file with an additional section.
+ */
+ @Test
+ public void testLoadAdditional() throws ConfigurationException
+ {
+ factory.setFile(ADDITIONAL_FILE);
+ CombinedConfiguration compositeConfiguration = (CombinedConfiguration) factory
+ .getConfiguration();
+ assertEquals("Verify how many configs", 2, compositeConfiguration
+ .getNumberOfConfigurations());
+
+ // Test if union was constructed correctly
+ Object prop = compositeConfiguration.getProperty("tables.table.name");
+ assertTrue(prop instanceof Collection);
+ assertEquals(3, ((Collection<?>) prop).size());
+ assertEquals("users", compositeConfiguration
+ .getProperty("tables.table(0).name"));
+ assertEquals("documents", compositeConfiguration
+ .getProperty("tables.table(1).name"));
+ assertEquals("tasks", compositeConfiguration
+ .getProperty("tables.table(2).name"));
+
+ prop = compositeConfiguration
+ .getProperty("tables.table.fields.field.name");
+ assertTrue(prop instanceof Collection);
+ assertEquals(17, ((Collection<?>) prop).size());
+
+ assertEquals("smtp.mydomain.org", compositeConfiguration
+ .getString("mail.host.smtp"));
+ assertEquals("pop3.mydomain.org", compositeConfiguration
+ .getString("mail.host.pop"));
+
+ // This was overriden
+ assertEquals("masterOfPost", compositeConfiguration
+ .getString("mail.account.user"));
+ assertEquals("topsecret", compositeConfiguration
+ .getString("mail.account.psswd"));
+
+ // This was overriden, too, but not in additional section
+ assertEquals("enhanced factory", compositeConfiguration
+ .getString("test.configuration"));
+ }
+
+ /**
+ * Tests whether a default log error listener is registered at the builder
+ * instance.
+ */
+ @Test
+ public void testLogErrorListener()
+ {
+ assertEquals("No default error listener registered", 1,
+ new DefaultConfigurationBuilder().getErrorListeners().size());
+ }
+
+ /**
+ * Tests loading a definition file that contains optional configurations.
+ */
+ @Test
+ public void testLoadOptional() throws Exception
+ {
+ factory.setURL(OPTIONAL_FILE.toURI().toURL());
+ Configuration config = factory.getConfiguration();
+ assertTrue(config.getBoolean("test.boolean"));
+ assertEquals("value", config.getProperty("element"));
+ }
+
+ /**
+ * Tests whether loading a failing optional configuration causes an error
+ * event.
+ */
+ @Test
+ public void testLoadOptionalErrorEvent() throws Exception
+ {
+ factory.clearErrorListeners();
+ ConfigurationErrorListenerImpl listener = new ConfigurationErrorListenerImpl();
+ factory.addErrorListener(listener);
+ prepareOptionalTest("configuration", false);
+ listener.verify(DefaultConfigurationBuilder.EVENT_ERR_LOAD_OPTIONAL,
+ OPTIONAL_NAME, null);
+ }
+
+ /**
+ * Tests loading a definition file with optional and non optional
+ * configuration sources. One non optional does not exist, so this should
+ * cause an exception.
+ */
+ @Test(expected = ConfigurationException.class)
+ public void testLoadOptionalWithException() throws ConfigurationException
+ {
+ factory.setFile(OPTIONALEX_FILE);
+ factory.getConfiguration();
+ }
+
+ /**
+ * Tries to load a configuration file with an optional, non file-based
+ * configuration. The optional attribute should work for other configuration
+ * classes, too.
+ */
+ @Test
+ public void testLoadOptionalNonFileBased() throws ConfigurationException
+ {
+ CombinedConfiguration config = prepareOptionalTest("configuration", false);
+ assertTrue("Configuration not empty", config.isEmpty());
+ assertEquals("Wrong number of configurations", 0, config
+ .getNumberOfConfigurations());
+ }
+
+ /**
+ * Tests an optional, non existing configuration with the forceCreate
+ * attribute. This configuration should be added to the resulting
+ * configuration.
+ */
+ @Test
+ public void testLoadOptionalForceCreate() throws ConfigurationException
+ {
+ factory.setBasePath(TEST_FILE.getParent());
+ CombinedConfiguration config = prepareOptionalTest("xml", true);
+ assertEquals("Wrong number of configurations", 1, config
+ .getNumberOfConfigurations());
+ FileConfiguration fc = (FileConfiguration) config
+ .getConfiguration(OPTIONAL_NAME);
+ assertNotNull("Optional config not found", fc);
+ assertEquals("File name was not set", "nonExisting.xml", fc
+ .getFileName());
+ assertNotNull("Base path was not set", fc.getBasePath());
+ }
+
+ /**
+ * Tests loading an embedded optional configuration builder with the force
+ * create attribute.
+ */
+ @Test
+ public void testLoadOptionalBuilderForceCreate()
+ throws ConfigurationException
+ {
+ CombinedConfiguration config = prepareOptionalTest("configuration",
+ true);
+ assertEquals("Wrong number of configurations", 1, config
+ .getNumberOfConfigurations());
+ assertTrue(
+ "Wrong optional configuration type",
+ config.getConfiguration(OPTIONAL_NAME) instanceof CombinedConfiguration);
+ }
+
+ /**
+ * Tests loading an optional configuration with the force create attribute
+ * set. The provider will always throw an exception. In this case the
+ * configuration will not be added to the resulting combined configuration.
+ */
+ @Test
+ public void testLoadOptionalForceCreateWithException()
+ throws ConfigurationException
+ {
+ factory.addConfigurationProvider("test",
+ new DefaultConfigurationBuilder.ConfigurationBuilderProvider()
+ {
+ // Throw an exception here, too
+ @Override
+ public AbstractConfiguration getEmptyConfiguration(
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl) throws Exception
+ {
+ throw new Exception("Unable to create configuration!");
+ }
+ });
+ CombinedConfiguration config = prepareOptionalTest("test", true);
+ assertEquals("Optional configuration could be created", 0, config
+ .getNumberOfConfigurations());
+ }
+
+ /**
+ * Prepares a test for loading a configuration definition file with an
+ * optional configuration declaration.
+ *
+ * @param tag the tag name with the optional configuration
+ * @param force the forceCreate attribute
+ * @return the combined configuration obtained from the builder
+ * @throws ConfigurationException if an error occurs
+ */
+ private CombinedConfiguration prepareOptionalTest(String tag, boolean force)
+ throws ConfigurationException
+ {
+ String prefix = "override." + tag;
+ factory.addProperty(prefix + "[@fileName]", "nonExisting.xml");
+ factory.addProperty(prefix + "[@config-optional]", Boolean.TRUE);
+ factory.addProperty(prefix + "[@config-name]", OPTIONAL_NAME);
+ if (force)
+ {
+ factory.addProperty(prefix + "[@config-forceCreate]", Boolean.TRUE);
+ }
+ return factory.getConfiguration(false);
+ }
+
+ /**
+ * Tests whether the error log message caused by an optional configuration
+ * can be suppressed if a child builder is involved.
+ */
+ @Test
+ public void testLoadOptionalChildBuilderSuppressErrorLog()
+ throws ConfigurationException
+ {
+ factory.addProperty("override.configuration[@fileName]",
+ OPTIONAL_FILE.getAbsolutePath());
+ // a special invocation handler which checks that the warn() method of
+ // a logger is not called
+ InvocationHandler handler = new InvocationHandler()
+ {
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable
+ {
+ String methodName = method.getName();
+ if (methodName.startsWith("is"))
+ {
+ return Boolean.TRUE;
+ }
+ if ("warn".equals(methodName))
+ {
+ fail("Unexpected log output!");
+ }
+ return null;
+ }
+ };
+ factory.setLogger((Log) Proxy.newProxyInstance(getClass()
+ .getClassLoader(), new Class[] {
+ Log.class
+ }, handler));
+ factory.getConfiguration(false);
+ }
+
+ /**
+ * Tests loading a definition file with multiple different sources.
+ */
+ @Test
+ public void testLoadDifferentSources() throws ConfigurationException
+ {
+ factory.setFile(MULTI_FILE);
+ Configuration config = factory.getConfiguration();
+ assertFalse(config.isEmpty());
+ assertTrue(config instanceof CombinedConfiguration);
+ CombinedConfiguration cc = (CombinedConfiguration) config;
+ assertEquals("Wrong number of configurations", 1, cc
+ .getNumberOfConfigurations());
+
+ assertNotNull(config
+ .getProperty("tables.table(0).fields.field(2).name"));
+ assertNotNull(config.getProperty("element2.subelement.subsubelement"));
+ assertEquals("value", config.getProperty("element3"));
+ assertEquals("foo", config.getProperty("element3[@name]"));
+ assertNotNull(config.getProperty("mail.account.user"));
+
+ // test JNDIConfiguration
+ assertNotNull(config.getProperty("test.onlyinjndi"));
+ assertTrue(config.getBoolean("test.onlyinjndi"));
+
+ Configuration subset = config.subset("test");
+ assertNotNull(subset.getProperty("onlyinjndi"));
+ assertTrue(subset.getBoolean("onlyinjndi"));
+
+ // test SystemConfiguration
+ assertNotNull(config.getProperty("java.version"));
+ assertEquals(System.getProperty("java.version"), config
+ .getString("java.version"));
+
+ // test INIConfiguration
+ assertEquals("Property from ini file not found", "yes",
+ config.getString("testini.loaded"));
+
+ // test environment configuration
+ EnvironmentConfiguration envConf = new EnvironmentConfiguration();
+ for (Iterator<String> it = envConf.getKeys(); it.hasNext();)
+ {
+ String key = it.next();
+ String combinedKey = "env." + key;
+ assertEquals("Wrong value for env property " + key,
+ envConf.getString(key), config.getString(combinedKey));
+ }
+ }
+
+ /**
+ * Tests if the base path is correctly evaluated.
+ */
+ @Test
+ public void testSetConfigurationBasePath() throws ConfigurationException
+ {
+ factory.addProperty("properties[@fileName]", "test.properties");
+ File deepDir = new File(ConfigurationAssert.TEST_DIR, "config/deep");
+ factory.setConfigurationBasePath(deepDir.getAbsolutePath());
+
+ Configuration config = factory.getConfiguration(false);
+ assertEquals("Wrong property value", "somevalue", config
+ .getString("somekey"));
+ }
+
+ /**
+ * Tests reading a configuration definition file that contains complex
+ * initialization of properties of the declared configuration sources.
+ */
+ @Test
+ public void testComplexInitialization() throws ConfigurationException
+ {
+ factory.setFile(INIT_FILE);
+ CombinedConfiguration cc = (CombinedConfiguration) factory
+ .getConfiguration();
+
+ assertEquals("System property not found", "test.xml",
+ cc.getString("test_file_xml"));
+ PropertiesConfiguration c1 = (PropertiesConfiguration) cc
+ .getConfiguration(1);
+ assertTrue(
+ "Reloading strategy was not set",
+ c1.getReloadingStrategy() instanceof FileChangedReloadingStrategy);
+ assertEquals("Refresh delay was not set", 10000,
+ ((FileChangedReloadingStrategy) c1.getReloadingStrategy())
+ .getRefreshDelay());
+
+ Configuration xmlConf = cc.getConfiguration("xml");
+ assertEquals("Property not found", "I'm complex!", xmlConf
+ .getString("element2/subelement/subsubelement"));
+ assertEquals("List index not found", "two", xmlConf
+ .getString("list[0]/item[1]"));
+ assertEquals("Property in combiner file not found", "yellow", cc
+ .getString("/gui/selcolor"));
+
+ assertTrue("Delimiter flag was not set", cc
+ .isDelimiterParsingDisabled());
+ assertTrue("Expression engine was not set",
+ cc.getExpressionEngine() instanceof XPathExpressionEngine);
+ }
+
+ /**
+ * Tests if the returned combined configuration has the expected structure.
+ */
+ @Test
+ public void testCombinedConfigurationStructure() throws ConfigurationException
+ {
+ factory.setFile(INIT_FILE);
+ CombinedConfiguration cc = (CombinedConfiguration) factory
+ .getConfiguration();
+ assertNotNull("Properties configuration not found", cc
+ .getConfiguration("properties"));
+ assertNotNull("XML configuration not found", cc.getConfiguration("xml"));
+ assertEquals("Wrong number of contained configs", 4, cc
+ .getNumberOfConfigurations());
+
+ CombinedConfiguration cc2 = (CombinedConfiguration) cc
+ .getConfiguration(DefaultConfigurationBuilder.ADDITIONAL_NAME);
+ assertNotNull("No additional configuration found", cc2);
+ Set<String> names = cc2.getConfigurationNames();
+ assertEquals("Wrong number of contained additional configs", 2, names
+ .size());
+ assertTrue("Config 1 not contained", names.contains("combiner1"));
+ assertTrue("Config 2 not contained", names.contains("combiner2"));
+ }
+
+ /**
+ * Helper method for testing the attributes of a combined configuration
+ * created by the builder.
+ *
+ * @param cc the configuration to be checked
+ */
+ private void checkCombinedConfigAttrs(CombinedConfiguration cc)
+ {
+ assertTrue("Wrong delimiter parsing flag",
+ cc.isDelimiterParsingDisabled());
+ assertTrue("Wrong reload check", cc.isForceReloadCheck());
+ assertTrue("Wrong ignore reload ex flag", cc.isIgnoreReloadExceptions());
+ }
+
+ /**
+ * Tests whether attributes are correctly set on the combined configurations
+ * for the override and additional sections.
+ */
+ @Test
+ public void testCombinedConfigurationAttributes() throws ConfigurationException
+ {
+ factory.setFile(INIT_FILE);
+ CombinedConfiguration cc = (CombinedConfiguration) factory
+ .getConfiguration();
+ checkCombinedConfigAttrs(cc);
+ CombinedConfiguration cc2 = (CombinedConfiguration) cc
+ .getConfiguration(DefaultConfigurationBuilder.ADDITIONAL_NAME);
+ checkCombinedConfigAttrs(cc2);
+ }
+
+ /**
+ * Tests the structure of the returned combined configuration if there is no
+ * additional section.
+ */
+ @Test
+ public void testCombinedConfigurationNoAdditional()
+ throws ConfigurationException
+ {
+ factory.setFile(TEST_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ assertNull("Additional configuration was found", cc
+ .getConfiguration(DefaultConfigurationBuilder.ADDITIONAL_NAME));
+ }
+
+ /**
+ * Tests whether the list node definition was correctly processed.
+ */
+ @Test
+ public void testCombinedConfigurationListNodes()
+ throws ConfigurationException
+ {
+ factory.setFile(INIT_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ Set<String> listNodes = cc.getNodeCombiner().getListNodes();
+ assertEquals("Wrong number of list nodes", 2, listNodes.size());
+ assertTrue("table node not a list node", listNodes.contains("table"));
+ assertTrue("list node not a list node", listNodes.contains("list"));
+
+ CombinedConfiguration cca = (CombinedConfiguration) cc
+ .getConfiguration(DefaultConfigurationBuilder.ADDITIONAL_NAME);
+ listNodes = cca.getNodeCombiner().getListNodes();
+ assertTrue("Found list nodes for additional combiner", listNodes
+ .isEmpty());
+ }
+
+ /**
+ * Tests whether a configuration builder can itself be declared in a
+ * configuration definition file.
+ */
+ @Test
+ public void testConfigurationBuilderProvider()
+ throws ConfigurationException
+ {
+ factory.addProperty("override.configuration[@fileName]", TEST_FILE
+ .getAbsolutePath());
+ CombinedConfiguration cc = factory.getConfiguration(false);
+ assertEquals("Wrong number of configurations", 1, cc
+ .getNumberOfConfigurations());
+ checkProperties(cc);
+ }
+
+ /**
+ * Tests whether settings of the builder are propagated to child builders.
+ */
+ @Test
+ public void testConfigurationBuilderProviderInheritProperties()
+ throws Exception
+ {
+ factory.addProperty("override.configuration[@fileName]",
+ TEST_FILE.getAbsolutePath());
+ factory.setBasePath("conf");
+ factory.setAttributeSplittingDisabled(true);
+ factory.setDelimiterParsingDisabled(true);
+ factory.setListDelimiter('/');
+ factory.setThrowExceptionOnMissing(true);
+ Log log = LogFactory.getLog(getClass());
+ factory.setLogger(log);
+ factory.clearErrorListeners();
+ factory.clearConfigurationListeners();
+ ConfigurationListenerTestImpl l =
+ new ConfigurationListenerTestImpl(factory);
+ factory.addConfigurationListener(l);
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl =
+ new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory,
+ factory.configurationAt("override.configuration"));
+ DefaultConfigurationBuilder.ConfigurationBuilderProvider provider =
+ new DefaultConfigurationBuilder.ConfigurationBuilderProvider();
+ DefaultConfigurationBuilder child =
+ (DefaultConfigurationBuilder) provider.createBean(
+ provider.fetchConfigurationClass(), decl, null);
+ assertEquals("Wrong base path", factory.getBasePath(),
+ child.getBasePath());
+ assertEquals("Wrong attribute splitting flag",
+ factory.isAttributeSplittingDisabled(),
+ child.isAttributeSplittingDisabled());
+ assertEquals("Wrong delimiter parsing flag",
+ factory.isDelimiterParsingDisabled(),
+ child.isDelimiterParsingDisabled());
+ assertEquals("Wrong list delimiter", factory.getListDelimiter(),
+ child.getListDelimiter());
+ assertEquals("Wrong exception flag",
+ factory.isThrowExceptionOnMissing(),
+ child.isThrowExceptionOnMissing());
+ assertSame("Wrong logger", log, child.getLogger());
+ assertTrue("Got error listeners", child.getErrorListeners().isEmpty());
+ assertEquals("Wrong number of listeners", 1, child
+ .getConfigurationListeners().size());
+ assertEquals("Wrong listener", l, child.getConfigurationListeners()
+ .iterator().next());
+ }
+
+ /**
+ * Tests whether properties of the parent configuration can be overridden.
+ */
+ @Test
+ public void testConfigurationBuilderProviderOverrideProperties()
+ throws Exception
+ {
+ factory.addProperty("override.configuration[@fileName]",
+ TEST_FILE.getAbsolutePath());
+ factory.addProperty("override.configuration[@basePath]", "base");
+ factory.addProperty("override.configuration[@throwExceptionOnMissing]",
+ "false");
+ factory.setBasePath("conf");
+ factory.setThrowExceptionOnMissing(true);
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl =
+ new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory,
+ factory.configurationAt("override.configuration"));
+ DefaultConfigurationBuilder.ConfigurationBuilderProvider provider =
+ new DefaultConfigurationBuilder.ConfigurationBuilderProvider();
+ DefaultConfigurationBuilder child =
+ (DefaultConfigurationBuilder) provider.createBean(
+ provider.fetchConfigurationClass(), decl, null);
+ assertEquals("Wrong base path", "base", child.getBasePath());
+ assertFalse("Wrong exception flag", child.isThrowExceptionOnMissing());
+ }
+
+ /**
+ * Tests whether XML settings can be inherited.
+ */
+ @Test
+ public void testLoadXMLWithSettings() throws Exception
+ {
+ File confDir = new File("conf");
+ File targetDir = new File("target");
+ File testXMLValidationSource = new File(confDir,
+ "testValidateInvalid.xml");
+ File testSavedXML = new File(targetDir, "testSave.xml");
+ File testSavedFactory = new File(targetDir, "testSaveFactory.xml");
+ URL dtdFile = getClass().getResource("/properties.dtd");
+ final String publicId = "http://commons.apache.org/test.dtd";
+
+ XMLConfiguration config = new XMLConfiguration("testDtd.xml");
+ config.setPublicID(publicId);
+ config.save(testSavedXML);
+ factory.addProperty("xml[@fileName]", testSavedXML.getAbsolutePath());
+ factory.addProperty("xml(0)[@validating]", "true");
+ factory.addProperty("xml(-1)[@fileName]", testXMLValidationSource
+ .getAbsolutePath());
+ factory.addProperty("xml(1)[@config-optional]", "true");
+ factory.addProperty("xml(1)[@validating]", "true");
+ factory.save(testSavedFactory);
+
+ factory = new DefaultConfigurationBuilder();
+ factory.setFile(testSavedFactory);
+ factory.registerEntityId(publicId, dtdFile);
+ factory.clearErrorListeners();
+ Configuration c = factory.getConfiguration();
+ assertEquals("Wrong property value", "value1", c.getString("entry(0)"));
+ assertFalse("Invalid XML source was loaded", c
+ .containsKey("table.name"));
+
+ testSavedXML.delete();
+ testSavedFactory.delete();
+ }
+
+ /**
+ * Tests loading a configuration definition file that defines a custom
+ * result class.
+ */
+ @Test
+ public void testExtendedClass() throws ConfigurationException
+ {
+ factory.setFile(CLASS_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ assertEquals("Extended", cc.getProperty("test"));
+ assertTrue("Wrong result class: " + cc.getClass(),
+ cc instanceof ExtendedCombinedConfiguration);
+ }
+
+ /**
+ * Tests loading a configuration definition file that defines new providers.
+ */
+ @Test
+ public void testConfigurationProvider() throws ConfigurationException
+ {
+ factory.setFile(PROVIDER_FILE);
+ factory.getConfiguration(true);
+ DefaultConfigurationBuilder.ConfigurationProvider provider = factory
+ .providerForTag("test");
+ assertNotNull("Provider 'test' not registered", provider);
+ }
+
+ /**
+ * Tests loading a configuration definition file that defines new providers.
+ */
+ @Test
+ public void testExtendedXMLConfigurationProvider() throws ConfigurationException
+ {
+ factory.setFile(EXTENDED_PROVIDER_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ DefaultConfigurationBuilder.ConfigurationProvider provider = factory
+ .providerForTag("test");
+ assertNotNull("Provider 'test' not registered", provider);
+ Configuration config = cc.getConfiguration("xml");
+ assertNotNull("Test configuration not present", config);
+ assertTrue("Configuration is not ExtendedXMLConfiguration, is " +
+ config.getClass().getName(), config instanceof ExtendedXMLConfiguration);
+ }
+
+ @Test
+ public void testGlobalLookup() throws Exception
+ {
+ factory.setFile(GLOBAL_LOOKUP_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ String value = cc.getInterpolator().lookup("test:test_key");
+ assertNotNull("The test key was not located", value);
+ assertEquals("Incorrect value retrieved","test.value",value);
+ }
+
+ @Test
+ public void testSystemProperties() throws Exception
+ {
+ factory.setFile(SYSTEM_PROPS_FILE);
+ factory.getConfiguration(true);
+ String value = System.getProperty("key1");
+ assertNotNull("The test key was not located", value);
+ assertEquals("Incorrect value retrieved","value1",value);
+ }
+
+ @Test
+ public void testValidation() throws Exception
+ {
+ factory.setFile(VALIDATION_FILE);
+ factory.getConfiguration(true);
+ String value = System.getProperty("key1");
+ assertNotNull("The test key was not located", value);
+ assertEquals("Incorrect value retrieved","value1",value);
+ }
+
+ @Test
+ public void testValidation3() throws Exception
+ {
+ System.getProperties().remove("Id");
+ factory.setFile(VALIDATION3_FILE);
+ CombinedConfiguration config = factory.getConfiguration(true);
+ String value = config.getString("Employee/Name");
+ assertNotNull("The test key was not located", value);
+ assertEquals("Incorrect value retrieved","John Doe",value);
+ System.setProperty("Id", "1001");
+ value = config.getString("Employee/Name");
+ assertNotNull("The test key was not located", value);
+ assertEquals("Incorrect value retrieved","Jane Doe",value);
+ }
+
+ @Test
+ public void testMultiTenentConfiguration() throws Exception
+ {
+ factory.setFile(MULTI_TENENT_FILE);
+ System.getProperties().remove("Id");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
+
+ verify("1001", config, 15);
+ verify("1002", config, 25);
+ verify("1003", config, 35);
+ verify("1004", config, 50);
+ verify("1005", config, 50);
+ }
+
+ @Test
+ public void testMultiTenentConfiguration2() throws Exception
+ {
+ factory.setFile(MULTI_TENENT_FILE);
+ System.setProperty("Id", "1004");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
+
+ verify("1001", config, 15);
+ verify("1002", config, 25);
+ verify("1003", config, 35);
+ verify("1004", config, 50);
+ verify("1005", config, 50);
+ }
+
+ @Test
+ public void testMultiTenentConfiguration3() throws Exception
+ {
+ factory.setFile(MULTI_TENENT_FILE);
+ StringWriter writer = new StringWriter();
+ WriterAppender app = new WriterAppender(new SimpleLayout(), writer);
+ Log log = LogFactory.getLog("TestLogger");
+ Logger logger = ((Log4JLogger)log).getLogger();
+ logger.addAppender(app);
+ logger.setLevel(Level.DEBUG);
+ logger.setAdditivity(false);
+
+ System.setProperty("Id", "1005");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
+
+ verify("1001", config, 15);
+ String xml = writer.getBuffer().toString();
+ assertNotNull("No XML returned", xml);
+ assertTrue("Incorect configuration data", xml.indexOf("<rowsPerPage>15</rowsPerPage>") >= 0);
+ logger.removeAppender(app);
+ logger.setLevel(Level.OFF);
+ verify("1002", config, 25);
+ verify("1003", config, 35);
+ verify("1004", config, 50);
+ verify("1005", config, 50);
+ }
+
+ @Test
+ public void testMultiTenantConfigurationAt() throws Exception
+ {
+ factory.setFile(MULTI_TENENT_FILE);
+ System.setProperty("Id", "1001");
+ CombinedConfiguration config = factory.getConfiguration(true);
+ HierarchicalConfiguration sub1 = config.configurationAt("Channels/Channel[@id='1']");
+ assertEquals("My Channel", sub1.getString("Name"));
+ assertEquals("test 1 data", sub1.getString("ChannelData"));
+ HierarchicalConfiguration sub2 = config.configurationAt("Channels/Channel[@id='2']");
+ assertEquals("Channel 2", sub2.getString("Name"));
+ assertEquals("more test 2 data", sub2.getString("MoreChannelData"));
+ }
+
+ @Test
+ public void testMerge() throws Exception
+ {
+ factory.setFile(MULTI_TENENT_FILE);
+ System.setProperty("Id", "1004");
+ Map<String, String> map = new HashMap<String, String>();
+ map.put("default", "${colors.header4}");
+ map.put("background", "#40404040");
+ map.put("text", "#000000");
+ map.put("header", "#444444");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
+
+ List<HierarchicalConfiguration> list = config.configurationsAt("colors/*");
+ Iterator<HierarchicalConfiguration> iter = list.iterator();
+ while (iter.hasNext())
+ {
+ SubnodeConfiguration sub = (SubnodeConfiguration)iter.next();
+ ConfigurationNode node = sub.getRootNode();
+ String value = (node.getValue() == null) ? "null" : node.getValue().toString();
+ if (map.containsKey(node.getName()))
+ {
+ assertEquals(map.get(node.getName()), value);
+ }
+ }
+
+ }
+
+ @Test
+ public void testDelimiterParsingDisabled() throws Exception
+ {
+ factory.setFile(MULTI_TENENT_FILE);
+ System.setProperty("Id", "1004");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
+
+ assertEquals("a,b,c", config.getString("split/list3/@values"));
+ assertEquals(0, config.getMaxIndex("split/list3/@values"));
+ assertEquals("a\\,b\\,c", config.getString("split/list4/@values"));
+ assertEquals("a,b,c", config.getString("split/list1"));
+ assertEquals(0, config.getMaxIndex("split/list1"));
+ assertEquals("a\\,b\\,c", config.getString("split/list2"));
+ }
+
+ @Test
+ public void testExpression() throws Exception
+ {
+ if (SystemUtils.isJavaVersionAtLeast(150))
+ {
+ factory.setFile(EXPRESSION_FILE);
+ factory.setAttributeSplittingDisabled(true);
+ System.getProperties().remove("Id");
+ org.slf4j.MDC.clear();
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertTrue("Incorrect configuration",
+ config instanceof DynamicCombinedConfiguration);
+
+ verify("1001", config, 15);
+ }
+ }
+
+ /**
+ * Tests whether variable substitution works across multiple child
+ * configurations. This test is related to CONFIGURATION-481.
+ */
+ @Test
+ public void testInterpolationOverMultipleSources()
+ throws ConfigurationException
+ {
+ File testFile =
+ ConfigurationAssert.getTestFile("testInterpolationBuilder.xml");
+ factory.setFile(testFile);
+ CombinedConfiguration combConfig = factory.getConfiguration(true);
+ assertEquals("Wrong value", "abc-product",
+ combConfig.getString("products.product.desc"));
+ XMLConfiguration xmlConfig =
+ (XMLConfiguration) combConfig.getConfiguration("test");
+ assertEquals("Wrong value from XML config", "abc-product",
+ xmlConfig.getString("products/product/desc"));
+ SubnodeConfiguration subConfig =
+ xmlConfig
+ .configurationAt("products/product[@name='abc']", true);
+ assertEquals("Wrong value from sub config", "abc-product",
+ subConfig.getString("desc"));
+ }
+
+ private void verify(String key, CombinedConfiguration config, int rows)
+ {
+ System.setProperty("Id", key);
+ org.slf4j.MDC.put("Id", key);
+ int actual = config.getInt("rowsPerPage");
+ assertTrue("expected: " + rows + " actual: " + actual, actual == rows);
+ }
+
+
+ /**
+ * A specialized combined configuration implementation used for testing
+ * custom result classes.
+ */
+ public static class ExtendedCombinedConfiguration extends
+ CombinedConfiguration
+ {
+ /**
+ * The serial version UID.
+ */
+ private static final long serialVersionUID = 4678031745085083392L;
+
+ @Override
+ public Object getProperty(String key)
+ {
+ if (key.equals("test"))
+ {
+ return "Extended";
+ }
+ return super.getProperty(key);
+ }
+ }
+
+ public static class ExtendedXMLConfiguration extends XMLConfiguration
+ {
+ private static final long serialVersionUID = 1L;
+
+ public ExtendedXMLConfiguration()
+ {
+ }
+
+ }
+
+ public static class TestLookup extends StrLookup
+ {
+ Map<String, String> map = new HashMap<String, String>();
+
+ public TestLookup()
+ {
+ map.put("test_file_xml", "test.xml");
+ map.put("test_file_combine", "testcombine1.xml");
+ map.put("test_key", "test.value");
+ }
+
+ @Override
+ public String lookup(String key)
+ {
+ if (key == null)
+ {
+ return null;
+ }
+ return map.get(key);
+
+ }
+ }
+}
+
diff --git a/src/test/java/org/apache/commons/configuration/TestDynamicCombinedConfiguration.java b/src/test/java/org/apache/commons/configuration/TestDynamicCombinedConfiguration.java
new file mode 100644
index 0000000..97a4289
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestDynamicCombinedConfiguration.java
@@ -0,0 +1,371 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+
+import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
+import org.apache.commons.lang.text.StrLookup;
+import org.junit.Test;
+
+public class TestDynamicCombinedConfiguration
+{
+ private static String PATTERN = "${sys:Id}";
+ private static String PATTERN1 = "target/test-classes/testMultiConfiguration_${sys:Id}.xml";
+ private static String DEFAULT_FILE = "target/test-classes/testMultiConfiguration_default.xml";
+ private static final File MULTI_TENENT_FILE = new File(
+ "conf/testMultiTenentConfigurationBuilder4.xml");
+ private static final File MULTI_DYNAMIC_FILE = new File(
+ "conf/testMultiTenentConfigurationBuilder5.xml");
+
+ /** Constant for the number of test threads. */
+ private static final int THREAD_COUNT = 3;
+
+ /** Constant for the number of loops in the multi-thread tests. */
+ private static final int LOOP_COUNT = 100;
+
+ @Test
+ public void testConfiguration() throws Exception
+ {
+ DynamicCombinedConfiguration config = new DynamicCombinedConfiguration();
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ config.setExpressionEngine(engine);
+ config.setKeyPattern(PATTERN);
+ config.setDelimiterParsingDisabled(true);
+ MultiFileHierarchicalConfiguration multi = new MultiFileHierarchicalConfiguration(PATTERN1);
+ multi.setExpressionEngine(engine);
+ config.addConfiguration(multi, "Multi");
+ XMLConfiguration xml = new XMLConfiguration();
+ xml.setExpressionEngine(engine);
+ xml.setDelimiterParsingDisabled(true);
+ xml.setFile(new File(DEFAULT_FILE));
+ xml.load();
+ config.addConfiguration(xml, "Default");
+
+ verify("1001", config, 15);
+ verify("1002", config, 25);
+ verify("1003", config, 35);
+ verify("1004", config, 50);
+ assertEquals("a,b,c", config.getString("split/list3/@values"));
+ assertEquals(0, config.getMaxIndex("split/list3/@values"));
+ assertEquals("a\\,b\\,c", config.getString("split/list4/@values"));
+ assertEquals("a,b,c", config.getString("split/list1"));
+ assertEquals(0, config.getMaxIndex("split/list1"));
+ assertEquals("a\\,b\\,c", config.getString("split/list2"));
+ }
+
+ @Test
+ public void testConcurrentGetAndReload() throws Exception
+ {
+ System.getProperties().remove("Id");
+ DefaultConfigurationBuilder factory = new DefaultConfigurationBuilder();
+ factory.setFile(MULTI_TENENT_FILE);
+ CombinedConfiguration config = factory.getConfiguration(true);
+
+ assertEquals(config.getString("rowsPerPage"), "50");
+ Thread testThreads[] = new Thread[THREAD_COUNT];
+ int failures[] = new int[THREAD_COUNT];
+
+ for (int i = 0; i < testThreads.length; ++i)
+ {
+ testThreads[i] = new ReloadThread(config, failures, i, LOOP_COUNT, false, null, "50");
+ testThreads[i].start();
+ }
+
+ int totalFailures = 0;
+ for (int i = 0; i < testThreads.length; ++i)
+ {
+ testThreads[i].join();
+ totalFailures += failures[i];
+ }
+ assertTrue(totalFailures + " failures Occurred", totalFailures == 0);
+ }
+
+ @Test
+ public void testConcurrentGetAndReload2() throws Exception
+ {
+ System.getProperties().remove("Id");
+ DefaultConfigurationBuilder factory = new DefaultConfigurationBuilder();
+ factory.setFile(MULTI_TENENT_FILE);
+ CombinedConfiguration config = factory.getConfiguration(true);
+
+ assertEquals(config.getString("rowsPerPage"), "50");
+
+ Thread testThreads[] = new Thread[THREAD_COUNT];
+ int failures[] = new int[THREAD_COUNT];
+ System.setProperty("Id", "2002");
+ assertEquals(config.getString("rowsPerPage"), "25");
+ for (int i = 0; i < testThreads.length; ++i)
+ {
+ testThreads[i] = new ReloadThread(config, failures, i, LOOP_COUNT, false, null, "25");
+ testThreads[i].start();
+ }
+
+ int totalFailures = 0;
+ for (int i = 0; i < testThreads.length; ++i)
+ {
+ testThreads[i].join();
+ totalFailures += failures[i];
+ }
+ System.getProperties().remove("Id");
+ assertTrue(totalFailures + " failures Occurred", totalFailures == 0);
+ }
+
+ @Test
+ public void testConcurrentGetAndReloadMultipleClients() throws Exception
+ {
+ System.getProperties().remove("Id");
+ DefaultConfigurationBuilder factory = new DefaultConfigurationBuilder();
+ factory.setFile(MULTI_TENENT_FILE);
+ CombinedConfiguration config = factory.getConfiguration(true);
+
+ assertEquals(config.getString("rowsPerPage"), "50");
+
+ Thread testThreads[] = new Thread[THREAD_COUNT];
+ int failures[] = new int[THREAD_COUNT];
+ String[] ids = new String[] {null, "2002", "3001", "3002", "3003"};
+ String[] expected = new String[] {"50", "25", "15", "25", "50"};
+ for (int i = 0; i < testThreads.length; ++i)
+ {
+ testThreads[i] = new ReloadThread(config, failures, i, LOOP_COUNT, true, ids[i], expected[i]);
+ testThreads[i].start();
+ }
+
+ int totalFailures = 0;
+ for (int i = 0; i < testThreads.length; ++i)
+ {
+ testThreads[i].join();
+ totalFailures += failures[i];
+ }
+ System.getProperties().remove("Id");
+ if (totalFailures != 0)
+ {
+ System.out.println("Failures:");
+ for (int i = 0; i < testThreads.length; ++i)
+ {
+ System.out.println("Thread " + i + " " + failures[i]);
+ }
+ }
+ assertTrue(totalFailures + " failures Occurred", totalFailures == 0);
+ }
+
+ @Test
+ public void testConcurrentGetAndReloadFile() throws Exception
+ {
+ final int threadCount = 25;
+ System.getProperties().remove("Id");
+ // create a new configuration
+ File input = new File("target/test-classes/testMultiDynamic_default.xml");
+ File output = new File("target/test-classes/testwrite/testMultiDynamic_default.xml");
+ output.delete();
+ output.getParentFile().mkdir();
+ copyFile(input, output);
+
+ DefaultConfigurationBuilder factory = new DefaultConfigurationBuilder();
+ factory.setFile(MULTI_DYNAMIC_FILE);
+ CombinedConfiguration config = factory.getConfiguration(true);
+
+ assertEquals(config.getString("Product/FIIndex/FI[@id='123456781']"), "ID0001");
+
+ ReaderThread testThreads[] = new ReaderThread[threadCount];
+ for (int i = 0; i < testThreads.length; ++i)
+ {
+ testThreads[i] = new ReaderThread(config);
+ testThreads[i].start();
+ }
+
+ Thread.sleep(2000);
+
+ input = new File("target/test-classes/testMultiDynamic_default2.xml");
+ copyFile(input, output);
+
+ Thread.sleep(2000);
+ String id = config.getString("Product/FIIndex/FI[@id='123456782']");
+ assertNotNull("File did not reload, id is null", id);
+ String rows = config.getString("rowsPerPage");
+ assertTrue("Incorrect value for rowsPerPage", "25".equals(rows));
+
+ for (int i = 0; i < testThreads.length; ++i)
+ {
+ testThreads[i].shutdown();
+ testThreads[i].join();
+ }
+ for (int i = 0; i < testThreads.length; ++i)
+ {
+ assertFalse(testThreads[i].failed());
+ }
+ assertEquals("ID0002", config.getString("Product/FIIndex/FI[@id='123456782']"));
+ output.delete();
+ }
+
+
+ private class ReloadThread extends Thread
+ {
+ CombinedConfiguration combined;
+ int[] failures;
+ int index;
+ int count;
+ String expected;
+ String id;
+ boolean useId;
+
+ ReloadThread(CombinedConfiguration config, int[] failures, int index, int count,
+ boolean useId, String id, String expected)
+ {
+ combined = config;
+ this.failures = failures;
+ this.index = index;
+ this.count = count;
+ this.expected = expected;
+ this.id = id;
+ this.useId = useId;
+ }
+ @Override
+ public void run()
+ {
+ failures[index] = 0;
+
+ if (useId)
+ {
+ ThreadLookup.setId(id);
+ }
+ for (int i = 0; i < count; i++)
+ {
+ try
+ {
+ String value = combined.getString("rowsPerPage", null);
+ if (value == null || !value.equals(expected))
+ {
+ ++failures[index];
+ }
+ }
+ catch (Exception ex)
+ {
+ ++failures[index];
+ }
+ }
+ }
+ }
+
+ private class ReaderThread extends Thread
+ {
+ private boolean running = true;
+ private boolean failed = false;
+ CombinedConfiguration combined;
+
+ public ReaderThread(CombinedConfiguration c)
+ {
+ combined = c;
+ }
+
+ @Override
+ public void run()
+ {
+ while (running)
+ {
+ String bcId = combined.getString("Product/FIIndex/FI[@id='123456781']");
+ if ("ID0001".equalsIgnoreCase(bcId))
+ {
+ if (failed)
+ {
+ System.out.println("Thread failed, but recovered");
+ }
+ failed = false;
+ }
+ else
+ {
+ failed = true;
+ }
+ }
+ }
+
+ public boolean failed()
+ {
+ return failed;
+ }
+
+ public void shutdown()
+ {
+ running = false;
+ }
+
+ }
+
+ private void verify(String key, DynamicCombinedConfiguration config, int rows)
+ {
+ System.setProperty("Id", key);
+ assertTrue(config.getInt("rowsPerPage") == rows);
+ }
+
+ private void copyFile(File input, File output) throws IOException
+ {
+ Reader reader = new FileReader(input);
+ Writer writer = new FileWriter(output);
+ char[] buffer = new char[4096];
+ int n = 0;
+ while (-1 != (n = reader.read(buffer)))
+ {
+ writer.write(buffer, 0, n);
+ }
+ reader.close();
+ writer.close();
+ }
+
+ public static class ThreadLookup extends StrLookup
+ {
+ private static ThreadLocal<String> id = new ThreadLocal<String>();
+
+
+
+ public ThreadLookup()
+ {
+
+ }
+
+ public static void setId(String value)
+ {
+ id.set(value);
+ }
+
+ @Override
+ public String lookup(String key)
+ {
+ if (key == null || !key.equals("Id"))
+ {
+ return null;
+ }
+ String value = System.getProperty("Id");
+ if (value != null)
+ {
+ return value;
+ }
+ return id.get();
+
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestEnvironmentConfiguration.java b/src/test/java/org/apache/commons/configuration/TestEnvironmentConfiguration.java
new file mode 100644
index 0000000..be54b05
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestEnvironmentConfiguration.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for EnvironmentConfiguration.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestEnvironmentConfiguration.java 1224637 2011-12-25 19:47:21Z oheger $
+ */
+public class TestEnvironmentConfiguration
+{
+ /** Stores the configuration to be tested. */
+ private EnvironmentConfiguration config;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ config = new EnvironmentConfiguration();
+ }
+
+ /**
+ * Tests whether a newly created configuration contains some properties. (We
+ * expect that at least some properties are set in each environment.)
+ */
+ @Test
+ public void testInit()
+ {
+ boolean found = false;
+ assertFalse("No properties found", config.isEmpty());
+ for (Iterator<String> it = config.getKeys(); it.hasNext();)
+ {
+ String key = it.next();
+ assertTrue("Key not found: " + key, config.containsKey(key));
+ assertNotNull("No value for property " + key, config.getString(key));
+ found = true;
+ }
+ assertTrue("No property keys returned", found);
+ }
+
+ /**
+ * Tests removing properties. This should not be possible.
+ */
+ @Test(expected = UnsupportedOperationException.class)
+ public void testClearProperty()
+ {
+ String key = config.getKeys().next();
+ config.clearProperty(key);
+ }
+
+ /**
+ * Tests removing all properties. This should not be possible.
+ */
+ @Test(expected = UnsupportedOperationException.class)
+ public void testClear()
+ {
+ config.clear();
+ }
+
+ /**
+ * Tries to add another property. This should cause an exception.
+ */
+ @Test(expected = UnsupportedOperationException.class)
+ public void testAddProperty()
+ {
+ config.addProperty("JAVA_HOME", "C:\\java");
+ }
+
+ /**
+ * Tries to set the value of a property. This should cause an exception.
+ */
+ @Test(expected = UnsupportedOperationException.class)
+ public void testSetProperty()
+ {
+ config.setProperty("JAVA_HOME", "C:\\java");
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestEqualBehaviour.java b/src/test/java/org/apache/commons/configuration/TestEqualBehaviour.java
new file mode 100644
index 0000000..9f8f2e8
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestEqualBehaviour.java
@@ -0,0 +1,258 @@
+package org.apache.commons.configuration;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.junit.Test;
+
+/**
+ * Compare the behavior of various methods between CompositeConfiguration
+ * and normal (Properties) Configuration
+ *
+ * @version $Id: TestEqualBehaviour.java 1224638 2011-12-25 19:51:09Z oheger $
+ */
+public class TestEqualBehaviour
+{
+ private Configuration setupSimpleConfiguration()
+ throws Exception
+ {
+ String simpleConfigurationFile = ConfigurationAssert.getTestFile("testEqual.properties").getAbsolutePath();
+ return new PropertiesConfiguration(simpleConfigurationFile);
+ }
+
+ @SuppressWarnings("deprecation")
+ private Configuration setupCompositeConfiguration()
+ throws Exception
+ {
+ String compositeConfigurationFile = ConfigurationAssert.getTestFile("testEqualDigester.xml").getAbsolutePath();
+
+ ConfigurationFactory configurationFactory = new ConfigurationFactory();
+ configurationFactory.setConfigurationFileName(compositeConfigurationFile);
+ return configurationFactory.getConfiguration();
+ }
+
+ /**
+ * Checks whether two configurations have the same size,
+ * the same key sequence and contain the same key -> value mappings
+ */
+ private void checkEquality(String msg, Configuration c1, Configuration c2)
+ {
+ Iterator<String> it1 = c1.getKeys();
+ Iterator<String> it2 = c2.getKeys();
+
+ while(it1.hasNext() && it2.hasNext())
+ {
+ String key1 = it1.next();
+ String key2 = it2.next();
+ assertEquals(msg + ", Keys: ", key1, key2);
+ assertEquals(msg + ", Contains: ", c1.containsKey(key1), c2.containsKey(key2));
+ }
+ assertEquals(msg + ", Iterator: ", it1.hasNext(), it2.hasNext());
+ }
+
+ /**
+ * Checks whether two configurations have the same key -> value mapping
+ */
+ private void checkSameKey(String msg, String key, Configuration c1, Configuration c2)
+ {
+ String [] s1 = c1.getStringArray(key);
+ String [] s2 = c2.getStringArray(key);
+
+ assertEquals(msg + ", length: ", s1.length, s2.length);
+
+ for (int i = 0; i < s1.length ; i++)
+ {
+ assertEquals(msg + ", String Array: ", s1[i], s2[i]);
+ }
+
+ List<Object> list1 = c1.getList(key);
+ List<Object> list2 = c2.getList(key);
+
+ assertEquals(msg + ", Size: ", list1.size(), list2.size());
+
+ Iterator<Object> it1 = list1.iterator();
+ Iterator<Object> it2 = list2.iterator();
+
+ while(it1.hasNext() && it2.hasNext())
+ {
+ String val1 = (String) it1.next();
+ String val2 = (String) it2.next();
+ assertEquals(msg + ", List: ", val1, val2);
+ }
+ assertEquals(msg + ", Iterator End: ", it1.hasNext(), it2.hasNext());
+ }
+
+ /**
+ * Are both configurations equal after loading?
+ */
+ @Test
+ public void testLoading() throws Exception
+ {
+ Configuration simple = setupSimpleConfiguration();
+ Configuration composite = setupCompositeConfiguration();
+
+ checkEquality("testLoading", simple, composite);
+ }
+
+ /**
+ * If we delete a key, does it vanish? Does it leave all
+ * the other keys unchanged? How about an unset key?
+ */
+ @Test
+ public void testDeletingExisting() throws Exception
+ {
+ Configuration simple = setupSimpleConfiguration();
+ Configuration composite = setupCompositeConfiguration();
+
+ String key = "clear.property";
+
+ assertTrue(simple.containsKey(key));
+ assertEquals(simple.containsKey(key), composite.containsKey(key));
+
+ simple.clearProperty(key);
+ composite.clearProperty(key);
+
+ assertFalse(simple.containsKey(key));
+ assertEquals(simple.containsKey(key), composite.containsKey(key));
+
+ checkEquality("testDeletingExisting", simple, composite);
+ }
+
+ @Test
+ public void testDeletingNonExisting() throws Exception
+ {
+ Configuration simple = setupSimpleConfiguration();
+ Configuration composite = setupCompositeConfiguration();
+
+ String key = "nonexisting.clear.property";
+
+ assertFalse(simple.containsKey(key));
+ assertEquals(simple.containsKey(key), composite.containsKey(key));
+
+ simple.clearProperty(key);
+ composite.clearProperty(key);
+
+ assertFalse(simple.containsKey(key));
+ assertEquals(simple.containsKey(key), composite.containsKey(key));
+
+ checkEquality("testDeletingNonExisting", simple, composite);
+ }
+
+ /**
+ * If we set a key, does it work? How about an existing
+ * key? Can we change it?
+ */
+ @Test
+ public void testSettingNonExisting() throws Exception
+ {
+ Configuration simple = setupSimpleConfiguration();
+ Configuration composite = setupCompositeConfiguration();
+
+ String key = "nonexisting.property";
+ String value = "new value";
+
+ assertFalse(simple.containsKey(key));
+ assertEquals(simple.containsKey(key), composite.containsKey(key));
+
+ simple.setProperty(key, value);
+ composite.setProperty(key, value);
+
+ assertTrue(simple.containsKey(key));
+ assertEquals(simple.containsKey(key), composite.containsKey(key));
+
+ checkSameKey("testSettingNonExisting", key, simple, composite);
+ checkEquality("testSettingNonExisting", simple, composite);
+ }
+
+ @Test
+ public void testSettingExisting() throws Exception
+ {
+ Configuration simple = setupSimpleConfiguration();
+ Configuration composite = setupCompositeConfiguration();
+
+ String key = "existing.property";
+ String value = "new value";
+
+ assertTrue(simple.containsKey(key));
+ assertFalse(simple.getString(key).equals(value));
+ assertEquals(simple.containsKey(key), composite.containsKey(key));
+
+ simple.setProperty(key, value);
+ composite.setProperty(key, value);
+
+ assertTrue(simple.containsKey(key));
+ assertEquals(simple.getString(key), value);
+ assertEquals(simple.containsKey(key), composite.containsKey(key));
+
+ checkSameKey("testSettingExisting", key, simple, composite);
+ checkEquality("testSettingExisting", simple, composite);
+ }
+
+ /**
+ * If we add a key, does it work?
+ */
+ @Test
+ public void testAddingUnset() throws Exception
+ {
+ Configuration simple = setupSimpleConfiguration();
+ Configuration composite = setupCompositeConfiguration();
+
+ String key = "nonexisting.property";
+ String value = "new value";
+
+ assertFalse(simple.containsKey(key));
+ assertEquals(simple.containsKey(key), composite.containsKey(key));
+
+ simple.addProperty(key, value);
+ composite.addProperty(key, value);
+
+ checkSameKey("testAddingUnset", key, simple, composite);
+ checkEquality("testAddingUnset", simple, composite);
+ }
+
+ /**
+ * If we add a to an existing key, does it work?
+ */
+ @Test
+ public void testAddingSet() throws Exception
+ {
+ Configuration simple = setupSimpleConfiguration();
+ Configuration composite = setupCompositeConfiguration();
+
+ String key = "existing.property";
+ String value = "new value";
+
+ assertTrue(simple.containsKey(key));
+ assertEquals(simple.containsKey(key), composite.containsKey(key));
+
+ simple.addProperty(key, value);
+ composite.addProperty(key, value);
+
+ assertTrue(simple.containsKey(key));
+ assertEquals(simple.containsKey(key), composite.containsKey(key));
+
+ checkSameKey("testAddingSet", key, simple, composite);
+ checkEquality("testAddingSet", simple, composite);
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestEqualsProperty.java b/src/test/java/org/apache/commons/configuration/TestEqualsProperty.java
new file mode 100644
index 0000000..9830aeb
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestEqualsProperty.java
@@ -0,0 +1,42 @@
+package org.apache.commons.configuration;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+/**
+ * test if properties that contain a "=" will be loaded correctly.
+ *
+ * @version $Id: TestEqualsProperty.java 1224639 2011-12-25 19:52:46Z oheger $
+ */
+public class TestEqualsProperty
+{
+ /** The File that we test with */
+ private String testProperties = ConfigurationAssert.getTestFile("test.properties").getAbsolutePath();
+
+ @Test
+ public void testEquals() throws Exception
+ {
+ PropertiesConfiguration conf = new PropertiesConfiguration(testProperties);
+
+ String equals = conf.getString("test.equals");
+ assertEquals("value=one", equals);
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestFileConfiguration.java b/src/test/java/org/apache/commons/configuration/TestFileConfiguration.java
new file mode 100644
index 0000000..60a1191
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestFileConfiguration.java
@@ -0,0 +1,652 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.util.Iterator;
+import java.util.Properties;
+
+import org.apache.commons.configuration.reloading.FileAlwaysReloadingStrategy;
+import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * @author Emmanuel Bourg
+ * @version $Id: TestFileConfiguration.java 1231721 2012-01-15 18:32:07Z oheger $
+ */
+public class TestFileConfiguration
+{
+ /** Constant for the name of a test file.*/
+ private static final String TEST_FILENAME = "test.properties";
+
+ /** Constant for a test file.*/
+ private static final File TEST_FILE = ConfigurationAssert.getTestFile(TEST_FILENAME);
+
+ /** Constant for a test output file. */
+ private static final File OUT_FILE = new File(
+ "target/test-resources/foo/bar/test.properties");
+
+ /** Constant for the name of a resource to be resolved.*/
+ private static final String RESOURCE_NAME = "config/deep/deeptest.properties";
+
+ /** Helper object for managing temporary files. */
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ /**
+ * Initializes the test environment. This implementation ensures that the
+ * test output file does not exist.
+ */
+ @Before
+ public void setUp() throws Exception
+ {
+ removeOutFile();
+ }
+
+ /**
+ * Performs cleanup after a test case. This implementation removes temporary
+ * files that have been created.
+ */
+ protected void tearDown() throws Exception
+ {
+ removeOutFile();
+ }
+
+ /**
+ * Removes a file if it exists.
+ *
+ * @param file the file to be removed
+ */
+ private static void removeFile(File file)
+ {
+ if (file.exists())
+ {
+ assertTrue("Cannot remove file: " + file, file.delete());
+ }
+ }
+
+ /**
+ * Removes the test output file if it exists. Its parent directories are
+ * also removed.
+ */
+ private static void removeOutFile()
+ {
+ removeFile(OUT_FILE);
+ File parent = OUT_FILE.getParentFile();
+ removeFile(parent);
+ parent = parent.getParentFile();
+ removeFile(parent);
+ }
+
+ @Test
+ public void testSetURL() throws Exception
+ {
+ // http URL
+ FileConfiguration config = new PropertiesConfiguration();
+ config.setURL(new URL("http://commons.apache.org/configuration/index.html"));
+
+ assertEquals("base path", "http://commons.apache.org/configuration/", config.getBasePath());
+ assertEquals("file name", "index.html", config.getFileName());
+
+ // file URL - This url is invalid, a valid url would be file:///temp/test.properties.
+ config.setURL(new URL("file:/temp/test.properties"));
+ assertEquals("base path", "file:///temp/", config.getBasePath());
+ assertEquals("file name", "test.properties", config.getFileName());
+ }
+
+ @Test
+ public void testSetURLWithParams() throws Exception
+ {
+ FileConfiguration config = new PropertiesConfiguration();
+ URL url = new URL("http://issues.apache.org/bugzilla/show_bug.cgi?id=37886");
+ config.setURL(url);
+ assertEquals("Base path incorrect", "http://issues.apache.org/bugzilla/", config.getBasePath());
+ assertEquals("File name incorrect", "show_bug.cgi", config.getFileName());
+ assertEquals("URL was not correctly stored", url, config.getURL());
+ }
+
+ @Test
+ public void testLocations() throws Exception
+ {
+ PropertiesConfiguration config = new PropertiesConfiguration();
+
+ File directory = ConfigurationAssert.TEST_DIR;
+ File file = TEST_FILE;
+ config.setFile(file);
+ assertEquals(directory.getAbsolutePath(), config.getBasePath());
+ assertEquals(TEST_FILENAME, config.getFileName());
+ assertEquals(file.getAbsolutePath(), config.getPath());
+
+ config.setPath(ConfigurationAssert.TEST_DIR_NAME + File.separator + TEST_FILENAME);
+ assertEquals(TEST_FILENAME, config.getFileName());
+ assertEquals(directory.getAbsolutePath(), config.getBasePath());
+ assertEquals(file.getAbsolutePath(), config.getPath());
+ assertEquals(file.toURI().toURL(), config.getURL());
+
+ config.setBasePath(null);
+ config.setFileName(TEST_FILENAME);
+ assertNull(config.getBasePath());
+ assertEquals(TEST_FILENAME, config.getFileName());
+ }
+
+ @Test
+ public void testCreateFile1() throws Exception
+ {
+ assertFalse("The file should not exist", OUT_FILE.exists());
+
+ FileConfiguration config = new PropertiesConfiguration(OUT_FILE);
+ config.save();
+
+ assertTrue("The file doesn't exist", OUT_FILE.exists());
+ }
+
+ @Test
+ public void testCreateFile2() throws Exception
+ {
+ FileConfiguration config = new PropertiesConfiguration();
+ config.setFile(OUT_FILE);
+ config.save();
+
+ assertTrue("The file doesn't exist", OUT_FILE.exists());
+ }
+
+ @Test
+ public void testCreateFile3() throws Exception
+ {
+ FileConfiguration config = new PropertiesConfiguration();
+ config.save(OUT_FILE);
+
+ assertTrue("The file doesn't exist", OUT_FILE.exists());
+ }
+
+ /**
+ * Tests collaboration with ConfigurationFactory: Is the base path set on
+ * loading is valid in file based configurations?
+ *
+ * @throws Exception if an error occurs
+ */
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testWithConfigurationFactory() throws Exception
+ {
+ File file = folder.newFile();
+
+ ConfigurationFactory factory = new ConfigurationFactory();
+ factory.setConfigurationURL(ConfigurationAssert.getTestURL(
+ "testDigesterConfiguration2.xml"));
+ CompositeConfiguration cc =
+ (CompositeConfiguration) factory.getConfiguration();
+ PropertiesConfiguration config = null;
+ for (int i = 0; config == null; i++)
+ {
+ if (cc.getConfiguration(i) instanceof PropertiesConfiguration)
+ {
+ config = (PropertiesConfiguration) cc.getConfiguration(i);
+ }
+ }
+
+ config.setProperty("test", "yes");
+ config.save(file);
+ assertTrue(file.exists());
+ config = new PropertiesConfiguration();
+ config.setFile(file);
+ config.load();
+
+ assertEquals("yes", config.getProperty("test"));
+ assertEquals("masterOfPost", config.getProperty("mail.account.user"));
+ }
+
+ /**
+ * Tests if invalid URLs cause an exception.
+ */
+ @Test(expected = ConfigurationException.class)
+ public void testSaveInvalidURL() throws Exception
+ {
+ FileConfiguration config = new PropertiesConfiguration();
+ config.save(new URL("http://jakarta.apache.org/test.properties"));
+ }
+
+ /**
+ * Tests if an invalid URL string causes an exception.
+ */
+ @Test(expected = ConfigurationException.class)
+ public void testSaveInvalidURLString() throws ConfigurationException
+ {
+ FileConfiguration config = new PropertiesConfiguration();
+ config.save("http://www.apache.org/test.properties");
+ }
+
+ /**
+ * Tests if the URL used by the load() method is also used by save().
+ */
+ @Test
+ public void testFileOverwrite() throws Exception
+ {
+ FileOutputStream out = null;
+ FileInputStream in = null;
+ File tempFile = null;
+ try
+ {
+ tempFile = folder.newFile();
+ Properties props = new Properties();
+ props.setProperty("1", "one");
+ out = new FileOutputStream(tempFile);
+ props.store(out, "TestFileOverwrite");
+ out.close();
+ out = null;
+ FileConfiguration config = new PropertiesConfiguration(tempFile);
+ config.load();
+ String value = config.getString("1");
+ assertTrue("one".equals(value));
+ config.setProperty("1", "two");
+ config.save();
+ props = new Properties();
+ in = new FileInputStream(tempFile);
+ props.load(in);
+ String value2 = props.getProperty("1");
+ assertTrue("two".equals(value2));
+ }
+ finally
+ {
+ if (out != null)
+ {
+ try
+ {
+ out.close();
+ }
+ catch (IOException ioex)
+ {
+ ioex.printStackTrace();
+ }
+ }
+ if (in != null)
+ {
+ try
+ {
+ in.close();
+ }
+ catch (IOException ioex)
+ {
+ ioex.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /**
+ * Tests setting a file changed reloading strategy together with the auto
+ * save feature.
+ */
+ @Test
+ public void testReloadingWithAutoSave() throws Exception
+ {
+ File configFile = folder.newFile();
+ PrintWriter out = null;
+
+ try
+ {
+ out = new PrintWriter(new FileWriter(configFile));
+ out.println("a = one");
+ out.close();
+ out = null;
+
+ PropertiesConfiguration config = new PropertiesConfiguration(
+ configFile);
+ config.setReloadingStrategy(new FileChangedReloadingStrategy());
+ config.setAutoSave(true);
+
+ assertEquals("one", config.getProperty("a"));
+ config.setProperty("b", "two");
+ assertEquals("one", config.getProperty("a"));
+ }
+ finally
+ {
+ if (out != null)
+ {
+ out.close();
+ }
+ }
+ }
+
+ /**
+ * Tests loading and saving a configuration file with a complicated path
+ * name including spaces. (related to issue 35210)
+ */
+ @Test
+ public void testPathWithSpaces() throws Exception
+ {
+ File path = folder.newFolder("path with spaces");
+ File confFile = new File(path, "config-test.properties");
+ PrintWriter out = null;
+
+ try
+ {
+ out = new PrintWriter(new FileWriter(confFile));
+ out.println("saved = false");
+ out.close();
+ out = null;
+
+ URL url = confFile.toURI().toURL();
+ PropertiesConfiguration config = new PropertiesConfiguration(url);
+ config.load();
+ assertFalse(config.getBoolean("saved"));
+
+ config.setProperty("saved", Boolean.TRUE);
+ config.save();
+ config = new PropertiesConfiguration();
+ config.setFile(confFile);
+ config.load();
+ assertTrue(config.getBoolean("saved"));
+ }
+ finally
+ {
+ if (out != null)
+ {
+ out.close();
+ }
+ }
+ }
+
+ /**
+ * Tests whether file names containing a "+" character are handled
+ * correctly. This test is related to CONFIGURATION-415.
+ */
+ @Test
+ public void testPathWithPlus() throws ConfigurationException, IOException
+ {
+ File saveFile = folder.newFile("test+config.properties");
+ FileConfiguration config = new PropertiesConfiguration(saveFile);
+ config.addProperty("test", Boolean.TRUE);
+ config.save();
+ File configFile = config.getFile();
+ assertEquals("Wrong configuration file", saveFile, configFile);
+ }
+
+ /**
+ * Tests the getFile() method.
+ */
+ @Test
+ public void testGetFile() throws ConfigurationException
+ {
+ FileConfiguration config = new PropertiesConfiguration();
+ assertNull(config.getFile());
+ File file = TEST_FILE.getAbsoluteFile();
+ config.setFile(file);
+ assertEquals(file, config.getFile());
+ config.load();
+ assertEquals(file, config.getFile());
+ }
+
+ /**
+ * Tests whether getFile() returns a valid file after a configuration has
+ * been loaded.
+ */
+ @Test
+ public void testGetFileAfterLoad() throws ConfigurationException,
+ IOException
+ {
+ FileConfiguration config = new PropertiesConfiguration();
+ config.load(TEST_FILE.getAbsolutePath());
+ assertNotNull("No source URL set", config.getURL());
+ assertEquals("Wrong source file", TEST_FILE.getCanonicalFile(), config
+ .getFile().getCanonicalFile());
+ }
+
+ /**
+ * Tests whether calling load() multiple times changes the source. This
+ * should not be the case.
+ */
+ @Test
+ public void testLoadMultiple() throws ConfigurationException
+ {
+ FileConfiguration config = new PropertiesConfiguration();
+ config.load(TEST_FILE.getAbsolutePath());
+ URL srcUrl = config.getURL();
+ File srcFile = config.getFile();
+ File file2 = ConfigurationAssert.getTestFile("testEqual.properties");
+ config.load(file2.getAbsolutePath());
+ assertEquals("Source URL was changed", srcUrl, config.getURL());
+ assertEquals("Source file was changed", srcFile, config.getFile());
+ }
+
+ @Test(expected = ConfigurationException.class)
+ public void testSaveWithoutFileNameFile() throws Exception
+ {
+ FileConfiguration config = new PropertiesConfiguration();
+ config.load(TEST_FILE);
+ config.save();
+ }
+
+ @Test(expected = ConfigurationException.class)
+ public void testSaveWithoutFileNameURL() throws Exception
+ {
+ FileConfiguration config = new PropertiesConfiguration();
+ config.load(TEST_FILE.toURI().toURL());
+ config.save();
+ }
+
+ /**
+ * Checks that loading a directory instead of a file throws an exception.
+ */
+ @Test(expected = ConfigurationException.class)
+ public void testLoadDirectoryString() throws ConfigurationException
+ {
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ config.load("target");
+ }
+
+ /**
+ * Tests that it is not possible to load a directory using the load() method
+ * which expects a File.
+ */
+ @Test(expected = ConfigurationException.class)
+ public void testLoadDirectoryFile() throws ConfigurationException
+ {
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ config.load(new File("target"));
+ }
+
+ /**
+ * Tests that it is not possible to load a directory using the String constructor.
+ */
+ @Test(expected = ConfigurationException.class)
+ public void testLoadDirectoryConstrString() throws ConfigurationException
+ {
+ new PropertiesConfiguration("target");
+ }
+
+ /**
+ * Tests that it is not possible to load a directory using the File constructor.
+ */
+ @Test(expected = ConfigurationException.class)
+ public void testLoadDirectoryConstrFile() throws ConfigurationException
+ {
+ new PropertiesConfiguration(new File("target"));
+ }
+
+ /**
+ * Tests whether the constructor behaves the same as setFileName() when the
+ * configuration source is in the classpath.
+ */
+ @Test
+ public void testInitFromClassPath() throws ConfigurationException
+ {
+ PropertiesConfiguration config1 = new PropertiesConfiguration();
+ config1.setFileName(RESOURCE_NAME);
+ config1.load();
+ PropertiesConfiguration config2 = new PropertiesConfiguration(
+ RESOURCE_NAME);
+ compare(config1, config2);
+ }
+
+ /**
+ * Tests the loading of configuration file in a Combined configuration
+ * when the configuration source is in the classpath.
+ */
+ @Test
+ public void testLoadFromClassPath() throws ConfigurationException
+ {
+ DefaultConfigurationBuilder cf =
+ new DefaultConfigurationBuilder("config/deep/testFileFromClasspath.xml");
+ CombinedConfiguration config = cf.getConfiguration(true);
+ Configuration config1 = config.getConfiguration("propConf");
+ Configuration config2 = config.getConfiguration("propConfDeep");
+ compare(config1, config2);
+ }
+
+ /**
+ * Tests cloning a file based configuration.
+ */
+ @Test
+ public void testClone() throws ConfigurationException
+ {
+ PropertiesConfiguration config = new PropertiesConfiguration(
+ RESOURCE_NAME);
+ PropertiesConfiguration copy = (PropertiesConfiguration) config.clone();
+ compare(config, copy);
+ assertNull("URL was not reset", copy.getURL());
+ assertNull("Base path was not reset", copy.getBasePath());
+ assertNull("File name was not reset", copy.getFileName());
+ assertNotSame("Reloading strategy was not reset", config
+ .getReloadingStrategy(), copy.getReloadingStrategy());
+ }
+
+ /**
+ * Tests whether an error log listener was registered at the configuration.
+ */
+ @Test
+ public void testLogErrorListener()
+ {
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ assertEquals("No error log listener registered", 1, config
+ .getErrorListeners().size());
+ }
+
+ /**
+ * Tests handling of errors in the reload() method.
+ */
+ @Test
+ public void testReloadError() throws ConfigurationException
+ {
+ ConfigurationErrorListenerImpl l = new ConfigurationErrorListenerImpl();
+ PropertiesConfiguration config = new PropertiesConfiguration(
+ RESOURCE_NAME);
+ config.clearErrorListeners();
+ config.addErrorListener(l);
+ config.setReloadingStrategy(new FileAlwaysReloadingStrategy());
+ config.getString("test");
+ config.setFileName("Not existing file");
+ config.getString("test");
+ l.verify(AbstractFileConfiguration.EVENT_RELOAD, null, null);
+ assertNotNull("Exception is not set", l.getLastEvent().getCause());
+ }
+
+ /**
+ * Tests iterating over the keys of a non hierarchical file-based
+ * configuration while a reload happens. This test is related to
+ * CONFIGURATION-347.
+ */
+ @Test
+ public void testIterationWithReloadFlat() throws ConfigurationException
+ {
+ PropertiesConfiguration config = new PropertiesConfiguration(TEST_FILE);
+ checkIterationWithReload(config);
+ }
+
+ /**
+ * Tests iterating over the keys of a hierarchical file-based configuration
+ * while a reload happens. This test is related to CONFIGURATION-347.
+ */
+ @Test
+ public void testIterationWithReloadHierarchical()
+ throws ConfigurationException
+ {
+ XMLConfiguration config = new XMLConfiguration("test.xml");
+ checkIterationWithReload(config);
+ }
+
+ /**
+ * Tests whether a configuration can be refreshed.
+ */
+ @Test
+ public void testRefresh() throws ConfigurationException
+ {
+ PropertiesConfiguration config = new PropertiesConfiguration(TEST_FILE);
+ assertEquals("Wrong value", 10, config.getInt("test.integer"));
+ config.setProperty("test.integer", new Integer(42));
+ assertEquals("Wrong value after update", 42,
+ config.getInt("test.integer"));
+ config.refresh();
+ assertEquals("Wrong value after refresh", 10,
+ config.getInt("test.integer"));
+ }
+
+ /**
+ * Tests refresh if the configuration is not associated with a file.
+ */
+ @Test(expected = ConfigurationException.class)
+ public void testRefreshNoFile() throws ConfigurationException
+ {
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ config.refresh();
+ }
+
+ /**
+ * Helper method for testing an iteration over the keys of a file-based
+ * configuration while a reload happens.
+ *
+ * @param config the configuration to test
+ */
+ private void checkIterationWithReload(FileConfiguration config)
+ {
+ config.setReloadingStrategy(new FileAlwaysReloadingStrategy());
+ for (Iterator<String> it = config.getKeys(); it.hasNext();)
+ {
+ String key = it.next();
+ assertNotNull("No value for key " + key, config.getProperty(key));
+ }
+ }
+
+ /**
+ * Helper method for comparing the content of two configuration objects.
+ *
+ * @param config1 the first configuration
+ * @param config2 the second configuration
+ */
+ private void compare(Configuration config1, Configuration config2)
+ {
+ StrictConfigurationComparator cc = new StrictConfigurationComparator();
+ assertTrue("Configurations are different", cc.compare(config1, config2));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/commons/configuration/TestHierarchicalConfiguration.java b/src/test/java/org/apache/commons/configuration/TestHierarchicalConfiguration.java
new file mode 100644
index 0000000..60c101a
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestHierarchicalConfiguration.java
@@ -0,0 +1,1252 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.configuration.HierarchicalConfiguration.Node;
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.DefaultConfigurationKey;
+import org.apache.commons.configuration.tree.DefaultConfigurationNode;
+import org.apache.commons.configuration.tree.DefaultExpressionEngine;
+import org.apache.commons.configuration.tree.ExpressionEngine;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for HierarchicalConfiguration.
+ *
+ * @version $Id: TestHierarchicalConfiguration.java 1231749 2012-01-15 20:48:56Z oheger $
+ */
+public class TestHierarchicalConfiguration
+{
+ private static String[] tables = { "users", "documents" };
+
+ private static String[][] fields =
+ {
+ { "uid", "uname", "firstName", "lastName", "email" },
+ { "docid", "name", "creationDate", "authorID", "version" }
+ };
+
+ private HierarchicalConfiguration config;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ /**
+ * Initialize the configuration with the following structure:
+ *
+ * tables
+ * table
+ * name
+ * fields
+ * field
+ * name
+ * field
+ * name
+ */
+ config = new HierarchicalConfiguration();
+ HierarchicalConfiguration.Node nodeTables = createNode("tables", null);
+ for(int i = 0; i < tables.length; i++)
+ {
+ HierarchicalConfiguration.Node nodeTable = createNode("table", null);
+ nodeTables.addChild(nodeTable);
+ HierarchicalConfiguration.Node nodeName = createNode("name", tables[i]);
+ nodeTable.addChild(nodeName);
+ HierarchicalConfiguration.Node nodeFields = createNode("fields", null);
+ nodeTable.addChild(nodeFields);
+
+ for (int j = 0; j < fields[i].length; j++)
+ {
+ nodeFields.addChild(createFieldNode(fields[i][j]));
+ }
+ }
+
+ config.getRoot().addChild(nodeTables);
+ }
+
+ @Test
+ public void testSetRoot()
+ {
+ config.setRoot(new HierarchicalConfiguration.Node("test"));
+ assertTrue(config.isEmpty());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetRootNull()
+ {
+ config.setRoot(null);
+ }
+
+ @Test
+ public void testSetRootNode()
+ {
+ config.setRootNode(new DefaultConfigurationNode("testNode"));
+ assertNotSame("Same root node", config.getRootNode(), config.getRoot());
+ assertEquals("Wrong name of root node", "testNode", config.getRoot().getName());
+
+ config.setRootNode(new HierarchicalConfiguration.Node("test"));
+ assertSame("Wrong root node", config.getRootNode(), config.getRoot());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetRootNodeNull()
+ {
+ config.setRootNode(null);
+ }
+
+ @Test
+ public void testIsEmpty()
+ {
+ assertFalse(config.isEmpty());
+ HierarchicalConfiguration conf2 = new HierarchicalConfiguration();
+ assertTrue(conf2.isEmpty());
+ HierarchicalConfiguration.Node child1 = new HierarchicalConfiguration.Node("child1");
+ HierarchicalConfiguration.Node child2 = new HierarchicalConfiguration.Node("child2");
+ child1.addChild(child2);
+ conf2.getRoot().addChild(child1);
+ assertTrue(conf2.isEmpty());
+ }
+
+ @Test
+ public void testGetProperty()
+ {
+ assertNull(config.getProperty("tables.table.resultset"));
+ assertNull(config.getProperty("tables.table.fields.field"));
+
+ Object prop = config.getProperty("tables.table(0).fields.field.name");
+ assertNotNull(prop);
+ assertTrue(prop instanceof Collection);
+ assertEquals(5, ((Collection<?>) prop).size());
+
+ prop = config.getProperty("tables.table.fields.field.name");
+ assertNotNull(prop);
+ assertTrue(prop instanceof Collection);
+ assertEquals(10, ((Collection<?>) prop).size());
+
+ prop = config.getProperty("tables.table.fields.field(3).name");
+ assertNotNull(prop);
+ assertTrue(prop instanceof Collection);
+ assertEquals(2, ((Collection<?>) prop).size());
+
+ prop = config.getProperty("tables.table(1).fields.field(2).name");
+ assertNotNull(prop);
+ assertEquals("creationDate", prop.toString());
+ }
+
+ @Test
+ public void testSetProperty()
+ {
+ config.setProperty("tables.table(0).name", "resources");
+ assertEquals("resources", config.getString("tables.table(0).name"));
+ config.setProperty("tables.table.name", "tab1,tab2");
+ assertEquals("tab1", config.getString("tables.table(0).name"));
+ assertEquals("tab2", config.getString("tables.table(1).name"));
+
+ config.setProperty("test.items.item", new int[] { 2, 4, 8, 16 });
+ assertEquals(3, config.getMaxIndex("test.items.item"));
+ assertEquals(8, config.getInt("test.items.item(2)"));
+ config.setProperty("test.items.item(2)", new Integer(6));
+ assertEquals(6, config.getInt("test.items.item(2)"));
+ config.setProperty("test.items.item(2)", new int[] { 7, 9, 11 });
+ assertEquals(5, config.getMaxIndex("test.items.item"));
+
+ config.setProperty("test", Boolean.TRUE);
+ config.setProperty("test.items", "01/01/05");
+ assertEquals(5, config.getMaxIndex("test.items.item"));
+ assertTrue(config.getBoolean("test"));
+ assertEquals("01/01/05", config.getProperty("test.items"));
+
+ config.setProperty("test.items.item", new Integer(42));
+ assertEquals(0, config.getMaxIndex("test.items.item"));
+ assertEquals(42, config.getInt("test.items.item"));
+ }
+
+ @Test
+ public void testClear()
+ {
+ config.setProperty(null, "value");
+ config.addProperty("[@attr]", "defined");
+ config.clear();
+ assertTrue("Configuration not empty", config.isEmpty());
+ }
+
+ @Test
+ public void testClearProperty()
+ {
+ config.clearProperty("tables.table(0).fields.field(0).name");
+ assertEquals("uname", config.getProperty("tables.table(0).fields.field(0).name"));
+ config.clearProperty("tables.table(0).name");
+ assertFalse(config.containsKey("tables.table(0).name"));
+ assertEquals("firstName", config.getProperty("tables.table(0).fields.field(1).name"));
+ assertEquals("documents", config.getProperty("tables.table.name"));
+ config.clearProperty("tables.table");
+ assertEquals("documents", config.getProperty("tables.table.name"));
+
+ config.addProperty("test", "first");
+ config.addProperty("test.level", "second");
+ config.clearProperty("test");
+ assertEquals("second", config.getString("test.level"));
+ assertFalse(config.containsKey("test"));
+ }
+
+ @Test
+ public void testClearTree()
+ {
+ Object prop = config.getProperty("tables.table(0).fields.field.name");
+ assertNotNull(prop);
+ config.clearTree("tables.table(0).fields.field(3)");
+ prop = config.getProperty("tables.table(0).fields.field.name");
+ assertNotNull(prop);
+ assertTrue(prop instanceof Collection);
+ assertEquals(4, ((Collection<?>) prop).size());
+
+ config.clearTree("tables.table(0).fields");
+ assertNull(config.getProperty("tables.table(0).fields.field.name"));
+ prop = config.getProperty("tables.table.fields.field.name");
+ assertNotNull(prop);
+ assertTrue(prop instanceof Collection);
+ assertEquals(5, ((Collection<?>) prop).size());
+
+ config.clearTree("tables.table(1)");
+ assertNull(config.getProperty("tables.table.fields.field.name"));
+ }
+
+ /**
+ * Tests removing more complex node structures.
+ */
+ @Test
+ public void testClearTreeComplex()
+ {
+ final int count = 5;
+ // create the structure
+ for (int idx = 0; idx < count; idx++)
+ {
+ config.addProperty("indexList.index(-1)[@default]", Boolean.FALSE);
+ config.addProperty("indexList.index[@name]", "test" + idx);
+ config.addProperty("indexList.index.dir", "testDir" + idx);
+ }
+ assertEquals("Wrong number of nodes", count - 1, config
+ .getMaxIndex("indexList.index[@name]"));
+
+ // Remove a sub tree
+ boolean found = false;
+ for (int idx = 0; true; idx++)
+ {
+ String name = config.getString("indexList.index(" + idx
+ + ")[@name]");
+ if (name == null)
+ {
+ break;
+ }
+ if ("test3".equals(name))
+ {
+ assertEquals("Wrong dir", "testDir3", config
+ .getString("indexList.index(" + idx + ").dir"));
+ config.clearTree("indexList.index(" + idx + ")");
+ found = true;
+ }
+ }
+ assertTrue("Key to remove not found", found);
+ assertEquals("Wrong number of nodes after remove", count - 2, config
+ .getMaxIndex("indexList.index[@name]"));
+ assertEquals("Wrong number of dir nodes after remove", count - 2,
+ config.getMaxIndex("indexList.index.dir"));
+
+ // Verify
+ for (int idx = 0; true; idx++)
+ {
+ String name = config.getString("indexList.index(" + idx
+ + ")[@name]");
+ if (name == null)
+ {
+ break;
+ }
+ if ("test3".equals(name))
+ {
+ fail("Key was not removed!");
+ }
+ }
+ }
+
+ /**
+ * Tests the clearTree() method on a hierarchical structure of nodes. This
+ * is a test case for CONFIGURATION-293.
+ */
+ @Test
+ public void testClearTreeHierarchy()
+ {
+ config.addProperty("a.b.c", "c");
+ config.addProperty("a.b.c.d", "d");
+ config.addProperty("a.b.c.d.e", "e");
+ config.clearTree("a.b.c");
+ assertFalse("Property not removed", config.containsKey("a.b.c"));
+ assertFalse("Sub property not removed", config.containsKey("a.b.c.d"));
+ }
+
+ @Test
+ public void testContainsKey()
+ {
+ assertTrue(config.containsKey("tables.table(0).name"));
+ assertTrue(config.containsKey("tables.table(1).name"));
+ assertFalse(config.containsKey("tables.table(2).name"));
+
+ assertTrue(config.containsKey("tables.table(0).fields.field.name"));
+ assertFalse(config.containsKey("tables.table(0).fields.field"));
+ config.clearTree("tables.table(0).fields");
+ assertFalse(config.containsKey("tables.table(0).fields.field.name"));
+
+ assertTrue(config.containsKey("tables.table.fields.field.name"));
+ }
+
+ @Test
+ public void testGetKeys()
+ {
+ List<String> keys = new ArrayList<String>();
+ for (Iterator<String> it = config.getKeys(); it.hasNext();)
+ {
+ keys.add(it.next());
+ }
+
+ assertEquals(2, keys.size());
+ assertTrue(keys.contains("tables.table.name"));
+ assertTrue(keys.contains("tables.table.fields.field.name"));
+
+ // test the order of the keys returned
+ config.addProperty("order.key1", "value1");
+ config.addProperty("order.key2", "value2");
+ config.addProperty("order.key3", "value3");
+
+ Iterator<String> it = config.getKeys("order");
+ assertEquals("1st key", "order.key1", it.next());
+ assertEquals("2nd key", "order.key2", it.next());
+ assertEquals("3rd key", "order.key3", it.next());
+ }
+
+ @Test
+ public void testGetKeysString()
+ {
+ // add some more properties to make it more interesting
+ config.addProperty("tables.table(0).fields.field(1).type", "VARCHAR");
+ config.addProperty("tables.table(0)[@type]", "system");
+ config.addProperty("tables.table(0).size", "42");
+ config.addProperty("tables.table(0).fields.field(0).size", "128");
+ config.addProperty("connections.connection.param.url", "url1");
+ config.addProperty("connections.connection.param.user", "me");
+ config.addProperty("connections.connection.param.pwd", "secret");
+ config.addProperty("connections.connection(-1).param.url", "url2");
+ config.addProperty("connections.connection(1).param.user", "guest");
+
+ checkKeys("tables.table(1)", new String[] { "name", "fields.field.name" });
+ checkKeys("tables.table(0)",
+ new String[] { "name", "fields.field.name", "tables.table(0)[@type]", "size", "fields.field.type", "fields.field.size" });
+ checkKeys("connections.connection(0).param",
+ new String[] {"url", "user", "pwd" });
+ checkKeys("connections.connection(1).param",
+ new String[] {"url", "user" });
+ }
+
+ /**
+ * Tests getKeys() with a prefix when the prefix matches exactly a key.
+ */
+ @Test
+ public void testGetKeysWithKeyAsPrefix()
+ {
+ config.addProperty("order.key1", "value1");
+ config.addProperty("order.key2", "value2");
+ Iterator<String> it = config.getKeys("order.key1");
+ assertTrue("no key found", it.hasNext());
+ assertEquals("1st key", "order.key1", it.next());
+ assertFalse("more keys than expected", it.hasNext());
+ }
+
+ /**
+ * Tests getKeys() with a prefix when the prefix matches exactly a key, and
+ * there are multiple keys starting with this prefix.
+ */
+ @Test
+ public void testGetKeysWithKeyAsPrefixMultiple()
+ {
+ config.addProperty("order.key1", "value1");
+ config.addProperty("order.key1.test", "value2");
+ config.addProperty("order.key1.test.complex", "value2");
+ Iterator<String> it = config.getKeys("order.key1");
+ assertEquals("Wrong key 1", "order.key1", it.next());
+ assertEquals("Wrong key 2", "order.key1.test", it.next());
+ assertEquals("Wrong key 3", "order.key1.test.complex", it.next());
+ assertFalse("More keys than expected", it.hasNext());
+ }
+
+ @Test
+ public void testAddProperty()
+ {
+ config.addProperty("tables.table(0).fields.field(-1).name", "phone");
+ Object prop = config.getProperty("tables.table(0).fields.field.name");
+ assertNotNull(prop);
+ assertTrue(prop instanceof Collection);
+ assertEquals(6, ((Collection<?>) prop).size());
+
+ config.addProperty("tables.table(0).fields.field.name", "fax");
+ prop = config.getProperty("tables.table.fields.field(5).name");
+ assertNotNull(prop);
+ assertTrue(prop instanceof List);
+ List<?> list = (List<?>) prop;
+ assertEquals("phone", list.get(0));
+ assertEquals("fax", list.get(1));
+
+ config.addProperty("tables.table(-1).name", "config");
+ prop = config.getProperty("tables.table.name");
+ assertNotNull(prop);
+ assertTrue(prop instanceof Collection);
+ assertEquals(3, ((Collection<?>) prop).size());
+ config.addProperty("tables.table(2).fields.field(0).name", "cid");
+ config.addProperty("tables.table(2).fields.field(-1).name",
+ "confName");
+ prop = config.getProperty("tables.table(2).fields.field.name");
+ assertNotNull(prop);
+ assertTrue(prop instanceof Collection);
+ assertEquals(2, ((Collection<?>) prop).size());
+ assertEquals("confName",
+ config.getProperty("tables.table(2).fields.field(1).name"));
+
+ config.addProperty("connection.user", "scott");
+ config.addProperty("connection.passwd", "tiger");
+ assertEquals("tiger", config.getProperty("connection.passwd"));
+
+ DefaultConfigurationKey key = createConfigurationKey();
+ key.append("tables").append("table").appendIndex(0);
+ key.appendAttribute("tableType");
+ config.addProperty(key.toString(), "system");
+ assertEquals("system", config.getProperty(key.toString()));
+ }
+
+ /**
+ * Creates a {@code DefaultConfigurationKey} object.
+ *
+ * @return the new key object
+ */
+ private static DefaultConfigurationKey createConfigurationKey()
+ {
+ return new DefaultConfigurationKey(new DefaultExpressionEngine());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddPropertyInvalidKey()
+ {
+ config.addProperty(".", "InvalidKey");
+ }
+
+ @Test
+ public void testGetMaxIndex()
+ {
+ assertEquals(4, config.getMaxIndex("tables.table(0).fields.field"));
+ assertEquals(4, config.getMaxIndex("tables.table(1).fields.field"));
+ assertEquals(1, config.getMaxIndex("tables.table"));
+ assertEquals(1, config.getMaxIndex("tables.table.name"));
+ assertEquals(0, config.getMaxIndex("tables.table(0).name"));
+ assertEquals(0, config.getMaxIndex("tables.table(1).fields.field(1)"));
+ assertEquals(-1, config.getMaxIndex("tables.table(2).fields"));
+
+ int maxIdx = config.getMaxIndex("tables.table(0).fields.field.name");
+ for(int i = 0; i <= maxIdx; i++)
+ {
+ DefaultConfigurationKey key =
+ new DefaultConfigurationKey(new DefaultExpressionEngine(),
+ "tables.table(0).fields");
+ key.append("field").appendIndex(i).append("name");
+ assertNotNull(config.getProperty(key.toString()));
+ }
+ }
+
+ @Test
+ public void testSubset()
+ {
+ // test the subset on the first table
+ Configuration subset = config.subset("tables.table(0)");
+ assertEquals(tables[0], subset.getProperty("name"));
+
+ Object prop = subset.getProperty("fields.field.name");
+ assertNotNull(prop);
+ assertTrue(prop instanceof Collection);
+ assertEquals(5, ((Collection<?>) prop).size());
+
+ for (int i = 0; i < fields[0].length; i++)
+ {
+ DefaultConfigurationKey key = createConfigurationKey();
+ key.append("fields").append("field").appendIndex(i);
+ key.append("name");
+ assertEquals(fields[0][i], subset.getProperty(key.toString()));
+ }
+
+ // test the subset on the second table
+ assertTrue("subset is not empty", config.subset("tables.table(2)").isEmpty());
+
+ // test the subset on the fields
+ subset = config.subset("tables.table.fields.field");
+ prop = subset.getProperty("name");
+ assertTrue("prop is not a collection", prop instanceof Collection);
+ assertEquals(10, ((Collection<?>) prop).size());
+
+ assertEquals(fields[0][0], subset.getProperty("name(0)"));
+
+ // test the subset on the field names
+ subset = config.subset("tables.table.fields.field.name");
+ assertTrue("subset is not empty", subset.isEmpty());
+ }
+
+ /**
+ * Tests the subset() method when the specified node has a value. This value
+ * must be available in the subset, too. Related to CONFIGURATION-295.
+ */
+ @Test
+ public void testSubsetNodeWithValue()
+ {
+ config.setProperty("tables.table(0).fields", "My fields");
+ Configuration subset = config.subset("tables.table(0).fields");
+ assertEquals("Wrong field name", fields[0][0], subset
+ .getString("field(0).name"));
+ assertEquals("Wrong value of root", "My fields", subset.getString(""));
+ }
+
+ /**
+ * Tests the subset() method when the specified key selects multiple keys.
+ * The resulting root node should have a value only if exactly one of the
+ * selected nodes has a value. Related to CONFIGURATION-295.
+ */
+ @Test
+ public void testSubsetMultipleNodesWithValues()
+ {
+ config.setProperty("tables.table(0).fields", "My fields");
+ Configuration subset = config.subset("tables.table.fields");
+ assertEquals("Wrong value of root", "My fields", subset.getString(""));
+ config.setProperty("tables.table(1).fields", "My other fields");
+ subset = config.subset("tables.table.fields");
+ assertNull("Root value is not null though there are multiple values",
+ subset.getString(""));
+ }
+
+ /**
+ * Tests the configurationAt() method to obtain a configuration for a sub
+ * tree.
+ */
+ @Test
+ public void testConfigurationAt()
+ {
+ HierarchicalConfiguration subConfig = config
+ .configurationAt("tables.table(1)");
+ assertEquals("Wrong table name", tables[1], subConfig.getString("name"));
+ List<Object> lstFlds = subConfig.getList("fields.field.name");
+ assertEquals("Wrong number of fields", fields[1].length, lstFlds.size());
+ for (int i = 0; i < fields[1].length; i++)
+ {
+ assertEquals("Wrong field at position " + i, fields[1][i], lstFlds
+ .get(i));
+ }
+
+ subConfig.setProperty("name", "testTable");
+ assertEquals("Change not visible in parent", "testTable", config
+ .getString("tables.table(1).name"));
+ config.setProperty("tables.table(1).fields.field(2).name", "testField");
+ assertEquals("Change not visible in sub config", "testField", subConfig
+ .getString("fields.field(2).name"));
+ }
+
+ /**
+ * Tests the configurationAt() method when the passed in key does not exist.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testConfigurationAtUnknownSubTree()
+ {
+ config.configurationAt("non.existing.key");
+ }
+
+ /**
+ * Tests the configurationAt() method when the passed in key selects
+ * multiple nodes. This should cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testConfigurationAtMultipleNodes()
+ {
+ config.configurationAt("tables.table.name");
+ }
+
+ /**
+ * Tests whether a sub configuration obtained by configurationAt() can be
+ * cleared.
+ */
+ @Test
+ public void testConfigurationAtClear()
+ {
+ config.addProperty("test.sub.test", "fail");
+ assertEquals("Wrong index (1)", 0, config.getMaxIndex("test"));
+ SubnodeConfiguration sub = config.configurationAt("test.sub");
+ assertEquals("Wrong value", "fail", sub.getString("test"));
+ sub.clear();
+ assertNull("Key still found", config.getString("test.sub.key"));
+ sub.setProperty("test", "success");
+ assertEquals("Property not set", "success",
+ config.getString("test.sub.test"));
+ assertEquals("Wrong index (2)", 0, config.getMaxIndex("test"));
+ }
+
+ /**
+ * Tests the configurationsAt() method.
+ */
+ @Test
+ public void testConfigurationsAt()
+ {
+ List<HierarchicalConfiguration> lstFlds = config.configurationsAt("tables.table(1).fields.field");
+ assertEquals("Wrong size of fields", fields[1].length, lstFlds.size());
+ for (int i = 0; i < fields[1].length; i++)
+ {
+ HierarchicalConfiguration sub = lstFlds.get(i);
+ assertEquals("Wrong field at position " + i, fields[1][i], sub
+ .getString("name"));
+ }
+ }
+
+ /**
+ * Tests the configurationsAt() method when the passed in key does not
+ * select any sub nodes.
+ */
+ @Test
+ public void testConfigurationsAtEmpty()
+ {
+ assertTrue("List is not empty", config.configurationsAt("unknown.key")
+ .isEmpty());
+ }
+
+ @Test
+ public void testClone()
+ {
+ Configuration copy = (Configuration) config.clone();
+ assertTrue(copy instanceof HierarchicalConfiguration);
+ checkContent(copy);
+ }
+
+ /**
+ * Tests whether registered event handlers are handled correctly when a
+ * configuration is cloned. They should not be registered at the clone.
+ */
+ @Test
+ public void testCloneWithEventListeners()
+ {
+ config.addConfigurationListener(new ConfigurationListener()
+ {
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ // just a dummy
+ }
+ });
+ HierarchicalConfiguration copy = (HierarchicalConfiguration) config
+ .clone();
+ assertTrue("Event listener registered at clone", copy
+ .getConfigurationListeners().isEmpty());
+ }
+
+ @Test
+ public void testAddNodes()
+ {
+ Collection<ConfigurationNode> nodes = new ArrayList<ConfigurationNode>();
+ nodes.add(createFieldNode("birthDate"));
+ nodes.add(createFieldNode("lastLogin"));
+ nodes.add(createFieldNode("language"));
+ config.addNodes("tables.table(0).fields", nodes);
+ assertEquals(7, config.getMaxIndex("tables.table(0).fields.field"));
+ assertEquals("birthDate", config.getString("tables.table(0).fields.field(5).name"));
+ assertEquals("lastLogin", config.getString("tables.table(0).fields.field(6).name"));
+ assertEquals("language", config.getString("tables.table(0).fields.field(7).name"));
+ }
+
+ /**
+ * Tests the addNodes() method when the provided key does not exist. In
+ * this case, a new node (or even a complete new branch) will be created.
+ */
+ @Test
+ public void testAddNodesForNonExistingKey()
+ {
+ Collection<ConfigurationNode> nodes = new ArrayList<ConfigurationNode>();
+ nodes.add(createNode("usr", "scott"));
+ Node nd = createNode("pwd", "tiger");
+ nd.setAttribute(true);
+ nodes.add(nd);
+ config.addNodes("database.connection.settings", nodes);
+
+ assertEquals("Usr node not found", "scott", config.getString("database.connection.settings.usr"));
+ assertEquals("Pwd node not found", "tiger", config.getString("database.connection.settings[@pwd]"));
+ }
+
+ /**
+ * Tests the addNodes() method when the new nodes should be added to an
+ * attribute node. This is not allowed.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddNodesWithAttributeKey()
+ {
+ Collection<ConfigurationNode> nodes = new ArrayList<ConfigurationNode>();
+ nodes.add(createNode("testNode", "yes"));
+ config.addNodes("database.connection[@settings]", nodes);
+ }
+
+ /**
+ * Tests copying nodes from one configuration to another one.
+ */
+ @Test
+ public void testAddNodesCopy()
+ {
+ HierarchicalConfiguration configDest = new HierarchicalConfiguration();
+ configDest.addProperty("test", "TEST");
+ Collection<ConfigurationNode> nodes = config.getRootNode().getChildren();
+ assertEquals("Wrong number of children", 1, nodes.size());
+ configDest.addNodes("newNodes", nodes);
+ for (int i = 0; i < tables.length; i++)
+ {
+ String keyTab = "newNodes.tables.table(" + i + ").";
+ assertEquals("Table " + i + " not found", tables[i], configDest
+ .getString(keyTab + "name"));
+ for (int j = 0; j < fields[i].length; j++)
+ {
+ assertEquals("Invalid field " + j + " in table " + i,
+ fields[i][j], configDest.getString(keyTab
+ + "fields.field(" + j + ").name"));
+ }
+ }
+ }
+
+ /**
+ * Tests adding an attribute node with the addNodes() method.
+ */
+ @Test
+ public void testAddNodesAttributeNode()
+ {
+ Collection<ConfigurationNode> nodes = new ArrayList<ConfigurationNode>();
+ ConfigurationNode nd = createNode("length", "10");
+ nd.setAttribute(true);
+ nodes.add(nd);
+ config.addNodes("tables.table(0).fields.field(1)", nodes);
+ assertEquals("Attribute was not added", "10", config
+ .getString("tables.table(0).fields.field(1)[@length]"));
+ }
+
+ /**
+ * Tests removing children from a configuration node.
+ */
+ @Test
+ public void testNodeRemove()
+ {
+ HierarchicalConfiguration.Node node = new HierarchicalConfiguration.Node(
+ "parent", "test");
+ assertFalse(node.hasChildren());
+ node.removeChildren(); // should have no effect
+ assertFalse(node.remove("child"));
+
+ node.addChild(createNode("test", "test"));
+ assertTrue(node.hasChildren());
+ assertTrue(node.remove("test"));
+ assertFalse(node.hasChildren());
+
+ for (int i = 0; i < 10; i++)
+ {
+ node.addChild(createNode("child" + i, "test" + i));
+ }
+ assertTrue(node.hasChildren());
+ assertFalse(node.remove("child"));
+ assertTrue(node.remove("child2"));
+ assertTrue(node.getChildren("child2").isEmpty());
+
+ HierarchicalConfiguration.Node child = createNode("child0", "testChild");
+ assertFalse(node.remove(child));
+ node.addChild(child);
+ assertTrue(node.remove(child));
+ assertEquals(1, node.getChildren("child0").size());
+ assertEquals("test0", ((HierarchicalConfiguration.Node) node
+ .getChildren("child0").get(0)).getValue());
+
+ assertTrue(node.remove("child0"));
+ assertFalse(node.remove(child));
+
+ node.removeChildren();
+ assertTrue(node.getChildren().isEmpty());
+ assertFalse(node.remove(child));
+ }
+
+ /**
+ * Tests the visitor mechanism.
+ */
+ @Test
+ public void testNodeVisitor()
+ {
+ CountVisitor v = new CountVisitor();
+ config.getRoot().visit(v, null);
+ assertEquals("Wrong number of visits", 28, v.beforeCount);
+ assertEquals("Different number of before and after visits",
+ v.beforeCount, v.afterCount);
+ }
+
+ /**
+ * Tests the visitor mechanism if a ConfigurationKey is passed in.
+ */
+ @Test
+ public void testNodeVisitorKeys()
+ {
+ CountVisitor v = new CountVisitor();
+ @SuppressWarnings("deprecation")
+ ConfigurationKey configKey = new ConfigurationKey();
+ config.getRoot().visit(v, configKey);
+ for (Iterator<String> it = config.getKeys(); it.hasNext();)
+ {
+ String key = it.next();
+ assertTrue("Key not found in before keys: " + key, v.beforeKeys
+ .contains(key));
+ assertTrue("Key not found in after keys: " + key, v.afterKeys
+ .contains(key));
+ }
+ }
+
+ /**
+ * Tests setting a custom expression engine, which uses a slightly different
+ * syntax.
+ */
+ @Test
+ public void testSetExpressionEngine()
+ {
+ config.setExpressionEngine(null);
+ assertNotNull("Expression engine is null", config.getExpressionEngine());
+ assertSame("Default engine is not used", HierarchicalConfiguration
+ .getDefaultExpressionEngine(), config.getExpressionEngine());
+
+ config.setExpressionEngine(createAlternativeExpressionEngine());
+ checkAlternativeSyntax();
+ }
+
+ /**
+ * Tests setting the default expression engine. This should impact all
+ * configuration instances that do not have their own engine.
+ */
+ @Test
+ public void testSetDefaultExpressionEngine()
+ {
+ ExpressionEngine engineOld = HierarchicalConfiguration.getDefaultExpressionEngine();
+ HierarchicalConfiguration
+ .setDefaultExpressionEngine(createAlternativeExpressionEngine());
+ checkAlternativeSyntax();
+ HierarchicalConfiguration.setDefaultExpressionEngine(engineOld);
+ }
+
+ /**
+ * Tests setting the default expression engine to null. This should not be
+ * allowed.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetDefaultExpressionEngineNull()
+ {
+ HierarchicalConfiguration.setDefaultExpressionEngine(null);
+ }
+
+ /**
+ * Tests the copy constructor.
+ */
+ @Test
+ public void testInitCopy()
+ {
+ HierarchicalConfiguration copy = new HierarchicalConfiguration(config);
+ checkContent(copy);
+ }
+
+ /**
+ * Tests whether the nodes of a copied configuration are independent from
+ * the source configuration.
+ */
+ @Test
+ public void testInitCopyUpdate()
+ {
+ HierarchicalConfiguration copy = new HierarchicalConfiguration(config);
+ config.setProperty("tables.table(0).name", "NewTable");
+ checkContent(copy);
+ }
+
+ /**
+ * Tests interpolation facilities.
+ */
+ @Test
+ public void testInterpolation()
+ {
+ config.addProperty("base.dir", "/home/foo");
+ config.addProperty("test.absolute.dir.dir1", "${base.dir}/path1");
+ config.addProperty("test.absolute.dir.dir2", "${base.dir}/path2");
+ config.addProperty("test.absolute.dir.dir3", "${base.dir}/path3");
+
+ Configuration sub = config.subset("test.absolute.dir");
+ for (int i = 1; i < 4; i++)
+ {
+ assertEquals("Wrong interpolation in parent", "/home/foo/path" + i,
+ config.getString("test.absolute.dir.dir" + i));
+ assertEquals("Wrong interpolation in subnode",
+ "/home/foo/path" + i, sub.getString("dir" + i));
+ }
+ }
+
+ /**
+ * Basic interpolation tests.
+ */
+ @Test
+ public void testInterpolationBasic()
+ {
+ InterpolationTestHelper.testInterpolation(config);
+ }
+
+ /**
+ * Tests multiple levels of interpolation.
+ */
+ @Test
+ public void testInterpolationMultipleLevels()
+ {
+ InterpolationTestHelper.testMultipleInterpolation(config);
+ }
+
+ /**
+ * Tests an invalid interpolation that causes an endless loop.
+ */
+ @Test
+ public void testInterpolationLoop()
+ {
+ InterpolationTestHelper.testInterpolationLoop(config);
+ }
+
+ /**
+ * Tests interpolation with a subset.
+ */
+ @Test
+ public void testInterpolationSubset()
+ {
+ InterpolationTestHelper.testInterpolationSubset(config);
+ }
+
+ /**
+ * Tests whether interpolation with a subset configuration works over
+ * multiple layers.
+ */
+ @Test
+ public void testInterpolationSubsetMultipleLayers()
+ {
+ config.clear();
+ config.addProperty("var", "value");
+ config.addProperty("prop2.prop[@attr]", "${var}");
+ Configuration sub1 = config.subset("prop2");
+ Configuration sub2 = sub1.subset("prop");
+ assertEquals("Wrong value", "value", sub2.getString("[@attr]"));
+ }
+
+ /**
+ * Tests interpolation of a variable, which cannot be resolved.
+ */
+ @Test
+ public void testInterpolationUnknownProperty()
+ {
+ InterpolationTestHelper.testInterpolationUnknownProperty(config);
+ }
+
+ /**
+ * Tests interpolation with system properties.
+ */
+ @Test
+ public void testInterpolationSysProperties()
+ {
+ InterpolationTestHelper.testInterpolationSystemProperties(config);
+ }
+
+ /**
+ * Tests interpolation with constant values.
+ */
+ @Test
+ public void testInterpolationConstants()
+ {
+ InterpolationTestHelper.testInterpolationConstants(config);
+ }
+
+ /**
+ * Tests escaping variables.
+ */
+ @Test
+ public void testInterpolationEscaped()
+ {
+ InterpolationTestHelper.testInterpolationEscaped(config);
+ }
+
+ /**
+ * Tests manipulating the interpolator.
+ */
+ @Test
+ public void testInterpolator()
+ {
+ InterpolationTestHelper.testGetInterpolator(config);
+ }
+
+ /**
+ * Tests obtaining a configuration with all variables substituted.
+ */
+ @Test
+ public void testInterpolatedConfiguration()
+ {
+ HierarchicalConfiguration c = (HierarchicalConfiguration) InterpolationTestHelper
+ .testInterpolatedConfiguration(config);
+
+ // tests whether the hierarchical structure has been maintained
+ config = c;
+ testGetProperty();
+ }
+
+ /**
+ * Tests the copy constructor when a null reference is passed.
+ */
+ @Test
+ public void testInitCopyNull()
+ {
+ HierarchicalConfiguration copy = new HierarchicalConfiguration(null);
+ assertTrue("Configuration not empty", copy.isEmpty());
+ }
+
+ /**
+ * Tests the parents of nodes when setRootNode() is involved. This is
+ * related to CONFIGURATION-334.
+ */
+ @Test
+ public void testNodeParentsAfterSetRootNode()
+ {
+ DefaultConfigurationNode root = new DefaultConfigurationNode();
+ DefaultConfigurationNode child1 = new DefaultConfigurationNode(
+ "child1", "test1");
+ root.addChild(child1);
+ config.setRootNode(root);
+ config.addProperty("child2", "test2");
+ List<ConfigurationNode> nodes = config.getExpressionEngine().query(config.getRootNode(),
+ "child2");
+ assertEquals("Wrong number of result nodes", 1, nodes.size());
+ ConfigurationNode child2 = nodes.get(0);
+ assertEquals("Different parent nodes", child1.getParentNode(), child2
+ .getParentNode());
+ }
+
+ /**
+ * Tests calling getRoot() after a root node was set using setRootNode() and
+ * further child nodes have been added. The newly add child nodes should be
+ * present in the root node returned.
+ */
+ @Test
+ public void testGetRootAfterSetRootNode()
+ {
+ DefaultConfigurationNode root = new DefaultConfigurationNode();
+ DefaultConfigurationNode child1 = new DefaultConfigurationNode(
+ "child1", "test1");
+ root.addChild(child1);
+ config.setRootNode(root);
+ config.addProperty("child2", "test2");
+ ConfigurationNode oldRoot = config.getRoot();
+ assertEquals("Wrong number of children", 2, oldRoot.getChildrenCount());
+ }
+
+ /**
+ * Tests whether keys that contains brackets can be used.
+ */
+ @Test
+ public void testGetPropertyKeyWithBrackets()
+ {
+ final String key = "test.directory.platform(x86)";
+ config.addProperty(key, "C:\\Temp");
+ assertEquals("Wrong property value", "C:\\Temp", config.getString(key));
+ }
+
+ /**
+ * Helper method for testing the getKeys(String) method.
+ *
+ * @param prefix the key to pass into getKeys()
+ * @param expected the expected result
+ */
+ private void checkKeys(String prefix, String[] expected)
+ {
+ Set<String> values = new HashSet<String>();
+ for(int i = 0; i < expected.length; i++)
+ {
+ values.add((expected[i].startsWith(prefix)) ? expected[i] : prefix + "." + expected[i]);
+ }
+
+ Iterator<String> itKeys = config.getKeys(prefix);
+ while(itKeys.hasNext())
+ {
+ String key = itKeys.next();
+ if(!values.contains(key))
+ {
+ fail("Found unexpected key: " + key);
+ }
+ else
+ {
+ values.remove(key);
+ }
+ }
+
+ assertTrue("Remaining keys " + values, values.isEmpty());
+ }
+
+ /**
+ * Helper method for checking keys using an alternative syntax.
+ */
+ private void checkAlternativeSyntax()
+ {
+ assertNull(config.getProperty("tables/table/resultset"));
+ assertNull(config.getProperty("tables/table/fields/field"));
+
+ Object prop = config.getProperty("tables/table[0]/fields/field/name");
+ assertNotNull(prop);
+ assertTrue(prop instanceof Collection);
+ assertEquals(5, ((Collection<?>) prop).size());
+
+ prop = config.getProperty("tables/table/fields/field/name");
+ assertNotNull(prop);
+ assertTrue(prop instanceof Collection);
+ assertEquals(10, ((Collection<?>) prop).size());
+
+ prop = config.getProperty("tables/table/fields/field[3]/name");
+ assertNotNull(prop);
+ assertTrue(prop instanceof Collection);
+ assertEquals(2, ((Collection<?>) prop).size());
+
+ prop = config.getProperty("tables/table[1]/fields/field[2]/name");
+ assertNotNull(prop);
+ assertEquals("creationDate", prop.toString());
+
+ Set<String> keys = new HashSet<String>();
+ CollectionUtils.addAll(keys, config.getKeys());
+ assertEquals("Wrong number of defined keys", 2, keys.size());
+ assertTrue("Key not found", keys.contains("tables/table/name"));
+ assertTrue("Key not found", keys
+ .contains("tables/table/fields/field/name"));
+ }
+
+ /**
+ * Checks the content of the passed in configuration object. Used by some
+ * tests that copy a configuration.
+ *
+ * @param c the configuration to check
+ */
+ private void checkContent(Configuration c)
+ {
+ for (int i = 0; i < tables.length; i++)
+ {
+ assertEquals(tables[i], c.getString("tables.table(" + i + ").name"));
+ for (int j = 0; j < fields[i].length; j++)
+ {
+ assertEquals(fields[i][j], c.getString("tables.table(" + i
+ + ").fields.field(" + j + ").name"));
+ }
+ }
+ }
+
+ private ExpressionEngine createAlternativeExpressionEngine()
+ {
+ DefaultExpressionEngine engine = new DefaultExpressionEngine();
+ engine.setPropertyDelimiter("/");
+ engine.setIndexStart("[");
+ engine.setIndexEnd("]");
+ return engine;
+ }
+
+ /**
+ * Helper method for creating a field node with its children.
+ *
+ * @param name the name of the field
+ * @return the field node
+ */
+ private static HierarchicalConfiguration.Node createFieldNode(String name)
+ {
+ HierarchicalConfiguration.Node fld = createNode("field", null);
+ fld.addChild(createNode("name", name));
+ return fld;
+ }
+
+ /**
+ * Helper method for creating a configuration node.
+ * @param name the node's name
+ * @param value the node's value
+ * @return the new node
+ */
+ private static HierarchicalConfiguration.Node createNode(String name, Object value)
+ {
+ HierarchicalConfiguration.Node node = new HierarchicalConfiguration.Node(name);
+ node.setValue(value);
+ return node;
+ }
+
+ /**
+ * A test visitor implementation for checking whether all visitor methods
+ * are correctly called.
+ */
+ @SuppressWarnings("deprecation")
+ static class CountVisitor extends HierarchicalConfiguration.NodeVisitor
+ {
+ /** The number of invocations of visitBeforeChildren(). */
+ int beforeCount;
+
+ /** The number of invocations of visitAfterChildren(). */
+ int afterCount;
+
+ /** A set with the keys passed to visitBeforeChildren(). */
+ final Set<String> beforeKeys = new HashSet<String>();
+
+ /** A set with the keys passed to visitAfterChildren(). */
+ final Set<String> afterKeys = new HashSet<String>();
+
+ @Override
+ public void visitAfterChildren(Node node, ConfigurationKey key)
+ {
+ super.visitAfterChildren(node, key);
+ afterCount++;
+ if (key != null)
+ {
+ afterKeys.add(key.toString());
+ }
+ }
+
+ @Override
+ public void visitBeforeChildren(Node node, ConfigurationKey key)
+ {
+ super.visitBeforeChildren(node, key);
+ beforeCount++;
+ if (key != null)
+ {
+ beforeKeys.add(key.toString());
+ }
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestHierarchicalConfigurationXMLReader.java b/src/test/java/org/apache/commons/configuration/TestHierarchicalConfigurationXMLReader.java
new file mode 100644
index 0000000..52a1639
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestHierarchicalConfigurationXMLReader.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMResult;
+import javax.xml.transform.sax.SAXSource;
+
+import org.apache.commons.jxpath.JXPathContext;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.xml.sax.InputSource;
+
+/**
+ * Test class for HierarchicalConfigurationXMLReader.
+ *
+ * @version $Id: TestHierarchicalConfigurationXMLReader.java 1224644 2011-12-25 20:34:22Z oheger $
+ */
+public class TestHierarchicalConfigurationXMLReader
+{
+ private static final String TEST_FILE = ConfigurationAssert.getTestFile(
+ "testHierarchicalXMLConfiguration.xml").getAbsolutePath();
+
+ private HierarchicalConfigurationXMLReader parser;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ XMLConfiguration config = new XMLConfiguration();
+ config.setFileName(TEST_FILE);
+ config.load();
+ parser = new HierarchicalConfigurationXMLReader(config);
+ }
+
+ @Test
+ public void testParse() throws Exception
+ {
+ SAXSource source = new SAXSource(parser, new InputSource());
+ DOMResult result = new DOMResult();
+ Transformer trans = TransformerFactory.newInstance().newTransformer();
+ trans.transform(source, result);
+ Node root = ((Document) result.getNode()).getDocumentElement();
+ JXPathContext ctx = JXPathContext.newContext(root);
+
+ assertEquals("Wrong name of root element", "database", root.getNodeName());
+ assertEquals("Wrong number of children of root", 1, ctx.selectNodes(
+ "/*").size());
+ assertEquals("Wrong number of tables", 2, ctx.selectNodes(
+ "/tables/table").size());
+ assertEquals("Wrong name of first table", "users", ctx
+ .getValue("/tables/table[1]/name"));
+ assertEquals("Wrong number of fields in first table", 5, ctx
+ .selectNodes("/tables/table[1]/fields/field").size());
+ assertEquals("Wrong attribute value", "system", ctx
+ .getValue("/tables/table[1]/@tableType"));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestHierarchicalINIConfiguration.java b/src/test/java/org/apache/commons/configuration/TestHierarchicalINIConfiguration.java
new file mode 100644
index 0000000..d232d85
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestHierarchicalINIConfiguration.java
@@ -0,0 +1,971 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.After;
+import org.junit.Test;
+
+/**
+ * Test class for HierarchicalINIConfiguration.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestHierarchicalINIConfiguration.java 1234362 2012-01-21 16:59:48Z oheger $
+ */
+public class TestHierarchicalINIConfiguration
+{
+ private static String LINE_SEPARATOR = System.getProperty("line.separator");
+
+ /** Constant for the content of an ini file. */
+ private static final String INI_DATA = "[section1]" + LINE_SEPARATOR
+ + "var1 = foo" + LINE_SEPARATOR + "var2 = 451" + LINE_SEPARATOR
+ + LINE_SEPARATOR + "[section2]" + LINE_SEPARATOR + "var1 = 123.45"
+ + LINE_SEPARATOR + "var2 = bar" + LINE_SEPARATOR + LINE_SEPARATOR
+ + "[section3]" + LINE_SEPARATOR + "var1 = true" + LINE_SEPARATOR
+ + "interpolated = ${section3.var1}" + LINE_SEPARATOR
+ + "multi = foo" + LINE_SEPARATOR + "multi = bar" + LINE_SEPARATOR
+ + LINE_SEPARATOR;
+
+ private static final String INI_DATA2 = "[section4]" + LINE_SEPARATOR
+ + "var1 = \"quoted value\"" + LINE_SEPARATOR
+ + "var2 = \"quoted value\\nwith \\\"quotes\\\"\"" + LINE_SEPARATOR
+ + "var3 = 123 ; comment" + LINE_SEPARATOR
+ + "var4 = \"1;2;3\" ; comment" + LINE_SEPARATOR
+ + "var5 = '\\'quoted\\' \"value\"' ; comment" + LINE_SEPARATOR
+ + "var6 = \"\"" + LINE_SEPARATOR;
+
+ private static final String INI_DATA3 = "[section5]" + LINE_SEPARATOR
+ + "multiLine = one \\" + LINE_SEPARATOR
+ + " two \\" + LINE_SEPARATOR
+ + " three" + LINE_SEPARATOR
+ + "singleLine = C:\\Temp\\" + LINE_SEPARATOR
+ + "multiQuoted = one \\" + LINE_SEPARATOR
+ + "\" two \" \\" + LINE_SEPARATOR
+ + " three" + LINE_SEPARATOR
+ + "multiComment = one \\ ; a comment" + LINE_SEPARATOR
+ + "two" + LINE_SEPARATOR
+ + "multiQuotedComment = \" one \" \\ ; comment" + LINE_SEPARATOR
+ + "two" + LINE_SEPARATOR
+ + "noFirstLine = \\" + LINE_SEPARATOR
+ + " line 2" + LINE_SEPARATOR
+ + "continueNoLine = one \\" + LINE_SEPARATOR;
+
+ private static final String INI_DATA_SEPARATORS = "[section]"
+ + LINE_SEPARATOR + "var1 = value1" + LINE_SEPARATOR
+ + "var2 : value2" + LINE_SEPARATOR
+ + "var3=value3" + LINE_SEPARATOR
+ + "var4:value4" + LINE_SEPARATOR
+ + "var5 : value=5" + LINE_SEPARATOR
+ + "var:6=value" + LINE_SEPARATOR
+ + "var:7=\"value7\"" + LINE_SEPARATOR
+ + "var:8 = \"value8\"" + LINE_SEPARATOR;
+
+ /** An ini file that contains only a property in the global section. */
+ private static final String INI_DATA_GLOBAL_ONLY = "globalVar = testGlobal"
+ + LINE_SEPARATOR + LINE_SEPARATOR;
+
+ /** An ini file with a global section. */
+ private static final String INI_DATA_GLOBAL = INI_DATA_GLOBAL_ONLY
+ + INI_DATA;
+
+ /** A test ini file. */
+ private static final File TEST_FILE = new File("target/test.ini");
+
+ @After
+ public void tearDown() throws Exception
+ {
+ if (TEST_FILE.exists())
+ {
+ assertTrue("Cannot remove test file: " + TEST_FILE, TEST_FILE
+ .delete());
+ }
+ }
+
+ /**
+ * Creates a HierarchicalINIConfiguration object that is initialized from
+ * the given data.
+ *
+ * @param data the data of the configuration (an ini file as string)
+ * @return the initialized configuration
+ * @throws ConfigurationException if an error occurs
+ */
+ private static HierarchicalINIConfiguration setUpConfig(String data)
+ throws ConfigurationException
+ {
+ HierarchicalINIConfiguration instance = new HierarchicalINIConfiguration();
+ load(instance, data);
+ return instance;
+ }
+
+ /**
+ * Loads the specified content into the given configuration instance.
+ *
+ * @param instance the configuration
+ * @param data the data to be loaded
+ * @throws ConfigurationException if an error occurs
+ */
+ private static void load(HierarchicalINIConfiguration instance, String data)
+ throws ConfigurationException
+ {
+ StringReader reader = new StringReader(data);
+ instance.load(reader);
+ reader.close();
+ }
+
+ /**
+ * Writes a test ini file.
+ *
+ * @param content the content of the file
+ * @throws IOException if an error occurs
+ */
+ private static void writeTestFile(String content) throws IOException
+ {
+ PrintWriter out = new PrintWriter(new FileWriter(TEST_FILE));
+ try
+ {
+ out.println(content);
+ }
+ finally
+ {
+ out.close();
+ }
+ }
+
+ /**
+ * Test of save method, of class {@link HierarchicalINIConfiguration}.
+ */
+ @Test
+ public void testSave() throws Exception
+ {
+ Writer writer = new StringWriter();
+ HierarchicalINIConfiguration instance = new HierarchicalINIConfiguration();
+ instance.addProperty("section1.var1", "foo");
+ instance.addProperty("section1.var2", "451");
+ instance.addProperty("section2.var1", "123.45");
+ instance.addProperty("section2.var2", "bar");
+ instance.addProperty("section3.var1", "true");
+ instance.addProperty("section3.interpolated", "${section3.var1}");
+ instance.addProperty("section3.multi", "foo");
+ instance.addProperty("section3.multi", "bar");
+ instance.save(writer);
+
+ assertEquals("Wrong content of ini file", INI_DATA, writer.toString());
+ }
+
+ /**
+ * Helper method for testing a save operation. This method constructs a
+ * configuration from the specified content string. Then it saves this
+ * configuration and checks whether the result matches the original content.
+ *
+ * @param content the content of the configuration
+ * @throws ConfigurationException if an error occurs
+ */
+ private void checkSave(String content) throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(content);
+ StringWriter writer = new StringWriter();
+ config.save(writer);
+ assertEquals("Wrong content of ini file", content, writer.toString());
+ }
+
+ /**
+ * Tests saving a configuration that contains a global section.
+ */
+ @Test
+ public void testSaveWithGlobalSection() throws ConfigurationException
+ {
+ checkSave(INI_DATA_GLOBAL);
+ }
+
+ /**
+ * Tests whether a configuration that contains only a global section can be
+ * saved correctly.
+ */
+ @Test
+ public void testSaveWithOnlyGlobalSection() throws ConfigurationException
+ {
+ checkSave(INI_DATA_GLOBAL_ONLY);
+ }
+
+ /**
+ * Test of load method, of class {@link HierarchicalINIConfiguration}.
+ */
+ @Test
+ public void testLoad() throws Exception
+ {
+ checkLoad(INI_DATA);
+ }
+
+ /**
+ * Tests the load() method when the alternative value separator is used (a
+ * ':' for '=').
+ */
+ @Test
+ public void testLoadAlternativeSeparator() throws Exception
+ {
+ checkLoad(INI_DATA.replace('=', ':'));
+ }
+
+ /**
+ * Tests loading a configuration from a File.
+ */
+ @Test
+ public void testLoadFile() throws ConfigurationException, IOException
+ {
+ writeTestFile(INI_DATA);
+ HierarchicalINIConfiguration config = new HierarchicalINIConfiguration(
+ TEST_FILE);
+ checkContent(config);
+ }
+
+ /**
+ * Tests loading a configuration from a file name.
+ */
+ @Test
+ public void testLoadFileName() throws ConfigurationException, IOException
+ {
+ writeTestFile(INI_DATA);
+ HierarchicalINIConfiguration config = new HierarchicalINIConfiguration(
+ TEST_FILE.getAbsolutePath());
+ checkContent(config);
+ }
+
+ /**
+ * Tests loading a configuration from a URL.
+ */
+ @Test
+ public void testLoadURL() throws ConfigurationException, IOException
+ {
+ writeTestFile(INI_DATA);
+ HierarchicalINIConfiguration config = new HierarchicalINIConfiguration(
+ TEST_FILE.toURI().toURL());
+ checkContent(config);
+ }
+
+ /**
+ * Tests the values of some properties to ensure that the configuration was
+ * correctly loaded.
+ *
+ * @param instance the configuration to check
+ */
+ private void checkContent(HierarchicalINIConfiguration instance)
+ {
+ assertTrue(instance.getString("section1.var1").equals("foo"));
+ assertTrue(instance.getInt("section1.var2") == 451);
+ assertTrue(instance.getDouble("section2.var1") == 123.45);
+ assertTrue(instance.getString("section2.var2").equals("bar"));
+ assertTrue(instance.getBoolean("section3.var1"));
+ assertTrue(instance.getSections().size() == 3);
+ }
+
+ /**
+ * Helper method for testing the load operation. Loads the specified content
+ * into a configuration and then checks some properties.
+ *
+ * @param data the data to load
+ */
+ private void checkLoad(String data) throws ConfigurationException
+ {
+ HierarchicalINIConfiguration instance = setUpConfig(data);
+ checkContent(instance);
+ }
+
+ /**
+ * Test of isCommentLine method, of class
+ * {@link HierarchicalINIConfiguration}.
+ */
+ @Test
+ public void testIsCommentLine()
+ {
+ HierarchicalINIConfiguration instance = new HierarchicalINIConfiguration();
+ assertTrue(instance.isCommentLine("#comment1"));
+ assertTrue(instance.isCommentLine(";comment1"));
+ assertFalse(instance.isCommentLine("nocomment=true"));
+ assertFalse(instance.isCommentLine(null));
+ }
+
+ /**
+ * Test of isSectionLine method, of class
+ * {@link HierarchicalINIConfiguration}.
+ */
+ @Test
+ public void testIsSectionLine()
+ {
+ HierarchicalINIConfiguration instance = new HierarchicalINIConfiguration();
+ assertTrue(instance.isSectionLine("[section]"));
+ assertFalse(instance.isSectionLine("nosection=true"));
+ assertFalse(instance.isSectionLine(null));
+ }
+
+ /**
+ * Test of getSections method, of class {@link HierarchicalINIConfiguration}
+ * .
+ */
+ @Test
+ public void testGetSections()
+ {
+ HierarchicalINIConfiguration instance = new HierarchicalINIConfiguration();
+ instance.addProperty("test1.foo", "bar");
+ instance.addProperty("test2.foo", "abc");
+ Set<String> expResult = new HashSet<String>();
+ expResult.add("test1");
+ expResult.add("test2");
+ Set<String> result = instance.getSections();
+ assertEquals(expResult, result);
+ }
+
+ @Test
+ public void testQuotedValue() throws Exception
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA2);
+ assertEquals("value", "quoted value", config.getString("section4.var1"));
+ }
+
+ @Test
+ public void testQuotedValueWithQuotes() throws Exception
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA2);
+ assertEquals("value", "quoted value\\nwith \"quotes\"", config
+ .getString("section4.var2"));
+ }
+
+ @Test
+ public void testValueWithComment() throws Exception
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA2);
+ assertEquals("value", "123", config.getString("section4.var3"));
+ }
+
+ @Test
+ public void testQuotedValueWithComment() throws Exception
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA2);
+ assertEquals("value", "1;2;3", config.getString("section4.var4"));
+ }
+
+ @Test
+ public void testQuotedValueWithSingleQuotes() throws Exception
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA2);
+ assertEquals("value", "'quoted' \"value\"", config
+ .getString("section4.var5"));
+ }
+
+ @Test
+ public void testWriteValueWithCommentChar() throws Exception
+ {
+ HierarchicalINIConfiguration config = new HierarchicalINIConfiguration();
+ config.setProperty("section.key1", "1;2;3");
+
+ StringWriter writer = new StringWriter();
+ config.save(writer);
+
+ HierarchicalINIConfiguration config2 = new HierarchicalINIConfiguration();
+ config2.load(new StringReader(writer.toString()));
+
+ assertEquals("value", "1;2;3", config2.getString("section.key1"));
+ }
+
+ /**
+ * Tests whether whitespace is left unchanged for quoted values.
+ */
+ @Test
+ public void testQuotedValueWithWhitespace() throws Exception
+ {
+ final String content = "CmdPrompt = \" [test at cmd ~]$ \"";
+ HierarchicalINIConfiguration config = setUpConfig(content);
+ assertEquals("Wrong propert value", " [test at cmd ~]$ ", config
+ .getString("CmdPrompt"));
+ }
+
+ /**
+ * Tests a quoted value with space and a comment.
+ */
+ @Test
+ public void testQuotedValueWithWhitespaceAndComment() throws Exception
+ {
+ final String content = "CmdPrompt = \" [test at cmd ~]$ \" ; a comment";
+ HierarchicalINIConfiguration config = setUpConfig(content);
+ assertEquals("Wrong propert value", " [test at cmd ~]$ ", config
+ .getString("CmdPrompt"));
+ }
+
+ /**
+ * Tests an empty quoted value.
+ */
+ @Test
+ public void testQuotedValueEmpty() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA2);
+ assertEquals("Wrong value for empty property", "", config
+ .getString("section4.var6"));
+ }
+
+ /**
+ * Tests a property that has no value.
+ */
+ @Test
+ public void testGetPropertyNoValue() throws ConfigurationException
+ {
+ final String data = INI_DATA2 + LINE_SEPARATOR + "noValue ="
+ + LINE_SEPARATOR;
+ HierarchicalINIConfiguration config = setUpConfig(data);
+ assertEquals("Wrong value of key", "", config
+ .getString("section4.noValue"));
+ }
+
+ /**
+ * Tests a property that has no key.
+ */
+ @Test
+ public void testGetPropertyNoKey() throws ConfigurationException
+ {
+ final String data = INI_DATA2 + LINE_SEPARATOR + "= noKey"
+ + LINE_SEPARATOR;
+ HierarchicalINIConfiguration config = setUpConfig(data);
+ assertEquals("Cannot find property with no key", "noKey", config
+ .getString("section4. "));
+ }
+
+ /**
+ * Tests reading a property from the global section.
+ */
+ @Test
+ public void testGlobalProperty() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
+ assertEquals("Wrong value of global property", "testGlobal", config
+ .getString("globalVar"));
+ }
+
+ /**
+ * Tests whether the specified configuration contains exactly the expected
+ * sections.
+ *
+ * @param config the configuration to check
+ * @param expected an array with the expected sections
+ */
+ private void checkSectionNames(HierarchicalINIConfiguration config,
+ String[] expected)
+ {
+ Set<String> sectionNames = config.getSections();
+ Iterator<String> it = sectionNames.iterator();
+ for (int idx = 0; idx < expected.length; idx++)
+ {
+ assertEquals("Wrong section at " + idx, expected[idx], it.next());
+ }
+ assertFalse("Too many sections", it.hasNext());
+ }
+
+ /**
+ * Tests the names of the sections returned by the configuration.
+ *
+ * @param data the data of the ini configuration
+ * @param expected the expected section names
+ * @return the configuration instance
+ */
+ private HierarchicalINIConfiguration checkSectionNames(String data,
+ String[] expected) throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(data);
+ checkSectionNames(config, expected);
+ return config;
+ }
+
+ /**
+ * Tests querying the sections if a global section if available.
+ */
+ @Test
+ public void testGetSectionsWithGlobal() throws ConfigurationException
+ {
+ checkSectionNames(INI_DATA_GLOBAL, new String[] {
+ null, "section1", "section2", "section3"
+ });
+ }
+
+ /**
+ * Tests querying the sections if there is no global section.
+ */
+ @Test
+ public void testGetSectionsNoGlobal() throws ConfigurationException
+ {
+ checkSectionNames(INI_DATA, new String[] {
+ "section1", "section2", "section3"
+ });
+ }
+
+ /**
+ * Tests whether the sections of a configuration can be queried that
+ * contains only a global section.
+ */
+ @Test
+ public void testGetSectionsGlobalOnly() throws ConfigurationException
+ {
+ checkSectionNames(INI_DATA_GLOBAL_ONLY, new String[] {
+ null
+ });
+ }
+
+ /**
+ * Tests whether variables containing a dot are not misinterpreted as
+ * sections. This test is related to CONFIGURATION-327.
+ */
+ @Test
+ public void testGetSectionsDottedVar() throws ConfigurationException
+ {
+ final String data = "dotted.var = 1" + LINE_SEPARATOR + INI_DATA_GLOBAL;
+ HierarchicalINIConfiguration config = checkSectionNames(data,
+ new String[] {
+ null, "section1", "section2", "section3"
+ });
+ assertEquals("Wrong value of dotted variable", 1, config
+ .getInt("dotted..var"));
+ }
+
+ /**
+ * Tests whether a section added later is also found by getSections().
+ */
+ @Test
+ public void testGetSectionsAdded() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA2);
+ config.addProperty("section5.test", Boolean.TRUE);
+ checkSectionNames(config, new String[] {
+ "section4", "section5"
+ });
+ }
+
+ /**
+ * Tests querying the properties of an existing section.
+ */
+ @Test
+ public void testGetSectionExisting() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA);
+ SubnodeConfiguration section = config.getSection("section1");
+ assertEquals("Wrong value of var1", "foo", section.getString("var1"));
+ assertEquals("Wrong value of var2", "451", section.getString("var2"));
+ }
+
+ /**
+ * Tests querying the properties of a section that was merged from two
+ * sections with the same name.
+ */
+ @Test
+ public void testGetSectionMerged() throws ConfigurationException
+ {
+ final String data = INI_DATA + "[section1]" + LINE_SEPARATOR
+ + "var3 = merged" + LINE_SEPARATOR;
+ HierarchicalINIConfiguration config = setUpConfig(data);
+ SubnodeConfiguration section = config.getSection("section1");
+ assertEquals("Wrong value of var1", "foo", section.getString("var1"));
+ assertEquals("Wrong value of var2", "451", section.getString("var2"));
+ assertEquals("Wrong value of var3", "merged", section.getString("var3"));
+ }
+
+ /**
+ * Tests querying the content of the global section.
+ */
+ @Test
+ public void testGetSectionGlobal() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
+ SubnodeConfiguration section = config.getSection(null);
+ assertEquals("Wrong value of global variable", "testGlobal", section
+ .getString("globalVar"));
+ }
+
+ /**
+ * Tests concurrent access to the global section.
+ */
+ @Test
+ public void testGetSectionGloabalMultiThreaded()
+ throws ConfigurationException, InterruptedException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
+ final int threadCount = 10;
+ GlobalSectionTestThread[] threads = new GlobalSectionTestThread[threadCount];
+ for (int i = 0; i < threadCount; i++)
+ {
+ threads[i] = new GlobalSectionTestThread(config);
+ threads[i].start();
+ }
+ for (int i = 0; i < threadCount; i++)
+ {
+ threads[i].join();
+ assertFalse("Exception occurred", threads[i].error);
+ }
+ }
+
+ /**
+ * Tests querying the content of the global section if there is none.
+ */
+ @Test
+ public void testGetSectionGlobalNonExisting() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA);
+ SubnodeConfiguration section = config.getSection(null);
+ assertTrue("Sub config not empty", section.isEmpty());
+ }
+
+ /**
+ * Tests querying a non existing section.
+ */
+ @Test
+ public void testGetSectionNonExisting() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA);
+ SubnodeConfiguration section = config
+ .getSection("Non existing section");
+ assertTrue("Sub config not empty", section.isEmpty());
+ }
+
+ /**
+ * Tests a property whose value spans multiple lines.
+ */
+ @Test
+ public void testLineContinuation() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA3);
+ assertEquals("Wrong value", "one" + LINE_SEPARATOR + "two"
+ + LINE_SEPARATOR + "three", config
+ .getString("section5.multiLine"));
+ }
+
+ /**
+ * Tests a property value that ends on a backslash, which is no line
+ * continuation character.
+ */
+ @Test
+ public void testLineContinuationNone() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA3);
+ assertEquals("Wrong value", "C:\\Temp\\", config
+ .getString("section5.singleLine"));
+ }
+
+ /**
+ * Tests a property whose value spans multiple lines when quoting is
+ * involved. In this case whitespace must not be trimmed.
+ */
+ @Test
+ public void testLineContinuationQuoted() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA3);
+ assertEquals("Wrong value", "one" + LINE_SEPARATOR + " two "
+ + LINE_SEPARATOR + "three", config
+ .getString("section5.multiQuoted"));
+ }
+
+ /**
+ * Tests a property whose value spans multiple lines with a comment.
+ */
+ @Test
+ public void testLineContinuationComment() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA3);
+ assertEquals("Wrong value", "one" + LINE_SEPARATOR + "two", config
+ .getString("section5.multiComment"));
+ }
+
+ /**
+ * Tests a property with a quoted value spanning multiple lines and a
+ * comment.
+ */
+ @Test
+ public void testLineContinuationQuotedComment()
+ throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA3);
+ assertEquals("Wrong value", " one " + LINE_SEPARATOR + "two", config
+ .getString("section5.multiQuotedComment"));
+ }
+
+ /**
+ * Tests a multi-line property value with an empty line.
+ */
+ @Test
+ public void testLineContinuationEmptyLine() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA3);
+ assertEquals("Wrong value", LINE_SEPARATOR + "line 2", config
+ .getString("section5.noFirstLine"));
+ }
+
+ /**
+ * Tests a line continuation at the end of the file.
+ */
+ @Test
+ public void testLineContinuationAtEnd() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA3);
+ assertEquals("Wrong value", "one" + LINE_SEPARATOR, config
+ .getString("section5.continueNoLine"));
+ }
+
+ /**
+ * Tests whether a configuration can be saved that contains section keys
+ * with delimiter characters. This test is related to CONFIGURATION-409.
+ */
+ @Test
+ public void testSaveKeysWithDelimiters() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration conf = new HierarchicalINIConfiguration();
+ final String section = "Section..with..dots";
+ conf.addProperty(section + ".test1", "test1");
+ conf.addProperty(section + ".test2", "test2");
+ conf.save(TEST_FILE);
+ conf = new HierarchicalINIConfiguration();
+ conf.load(TEST_FILE);
+ assertEquals("Wrong value (1)", "test1", conf.getString(section + ".test1"));
+ assertEquals("Wrong value (2)", "test2", conf.getString(section + ".test2"));
+ }
+
+ /**
+ * Tests whether a value which contains a semicolon can be loaded
+ * successfully. This test is related to CONFIGURATION-434.
+ */
+ @Test
+ public void testValueWithSemicolon() throws ConfigurationException
+ {
+ final String path =
+ "C:\\Program Files\\jar\\manage.jar;"
+ + "C:\\Program Files\\jar\\guiLauncher.jar";
+ final String content =
+ "[Environment]" + LINE_SEPARATOR + "Application Type=any"
+ + LINE_SEPARATOR + "Class Path=" + path + " ;comment"
+ + LINE_SEPARATOR + "Path=" + path
+ + "\t; another comment";
+ HierarchicalINIConfiguration config = setUpConfig(content);
+ assertEquals("Wrong class path", path,
+ config.getString("Environment.Class Path"));
+ assertEquals("Wrong path", path, config.getString("Environment.Path"));
+ }
+
+ /**
+ * Tests whether the different separators with or without whitespace are
+ * recognized.
+ */
+ @Test
+ public void testSeparators() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA_SEPARATORS);
+ for (int i = 1; i <= 4; i++)
+ {
+ assertEquals("Wrong value", "value" + i,
+ config.getString("section.var" + i));
+ }
+ }
+
+ /**
+ * Tests property definitions containing multiple separators.
+ */
+ @Test
+ public void testMultipleSeparators() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA_SEPARATORS);
+ assertEquals("Wrong value for var5", "value=5",
+ config.getString("section.var5"));
+ assertEquals("Wrong value for var6", "6=value",
+ config.getString("section.var"));
+ }
+
+ /**
+ * Tests property definitions containing multiple separators that are
+ * quoted.
+ */
+ @Test
+ public void testMultipleSeparatorsQuoted() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA_SEPARATORS);
+ assertEquals("Wrong value for var7", "value7",
+ config.getString("section.var:7"));
+ assertEquals("Wrong value for var8", "value8",
+ config.getString("section.var:8"));
+ }
+
+ /**
+ * Tests whether a section that has been cleared can be manipulated and
+ * saved later.
+ */
+ @Test
+ public void testSaveClearedSection() throws ConfigurationException
+ {
+ final String data = "[section]\ntest = failed\n";
+ HierarchicalINIConfiguration config = setUpConfig(data);
+ SubnodeConfiguration sub = config.getSection("section");
+ assertFalse("No content", sub.isEmpty());
+ sub.clear();
+ sub.setProperty("test", "success");
+ StringWriter writer = new StringWriter();
+ config.save(writer);
+ HierarchicalConfiguration config2 = setUpConfig(writer.toString());
+ assertEquals("Wrong value", "success",
+ config2.getString("section.test"));
+ }
+
+ /**
+ * Tests whether a duplicate session is merged.
+ */
+ @Test
+ public void testMergeDuplicateSection() throws ConfigurationException
+ {
+ final String data =
+ "[section]\nvar1 = sec1\n\n" + "[section]\nvar2 = sec2\n";
+ HierarchicalINIConfiguration config = setUpConfig(data);
+ assertEquals("Wrong value 1", "sec1", config.getString("section.var1"));
+ assertEquals("Wrong value 2", "sec2", config.getString("section.var2"));
+ SubnodeConfiguration sub = config.getSection("section");
+ assertEquals("Wrong sub value 1", "sec1", sub.getString("var1"));
+ assertEquals("Wrong sub value 2", "sec2", sub.getString("var2"));
+ StringWriter writer = new StringWriter();
+ config.save(writer);
+ String content = writer.toString();
+ int pos = content.indexOf("[section]");
+ assertTrue("Section not found: " + content, pos >= 0);
+ assertTrue("Section found multiple times: " + content,
+ content.indexOf("[section]", pos + 1) < 0);
+ }
+
+ /**
+ * Tests whether a section that was created by getSection() can be
+ * manipulated.
+ */
+ @Test
+ public void testGetSectionNonExistingManipulate()
+ throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config = setUpConfig(INI_DATA);
+ SubnodeConfiguration section = config.getSection("newSection");
+ section.addProperty("test", "success");
+ assertEquals("Main config not updated", "success",
+ config.getString("newSection.test"));
+ StringWriter writer = new StringWriter();
+ config.save(writer);
+ HierarchicalINIConfiguration config2 = setUpConfig(writer.toString());
+ section = config2.getSection("newSection");
+ assertEquals("Wrong value", "success", section.getString("test"));
+ }
+
+ /**
+ * Tests whether getSection() can deal with duplicate sections.
+ */
+ @Test
+ public void testGetSectionDuplicate()
+ {
+ HierarchicalINIConfiguration config =
+ new HierarchicalINIConfiguration();
+ config.addProperty("section.var1", "value1");
+ config.addProperty("section(-1).var2", "value2");
+ SubnodeConfiguration section = config.getSection("section");
+ Iterator<String> keys = section.getKeys();
+ assertEquals("Wrong key", "var1", keys.next());
+ assertFalse("Too many keys", keys.hasNext());
+ }
+
+ /**
+ * Tests whether the list delimiter character is recognized.
+ */
+ @Test
+ public void testValueWithDelimiters() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config =
+ setUpConfig("[test]" + LINE_SEPARATOR + "list=1,2,3"
+ + LINE_SEPARATOR);
+ List<Object> list = config.getList("test.list");
+ assertEquals("Wrong number of elements", 3, list.size());
+ assertEquals("Wrong element at 1", "1", list.get(0));
+ assertEquals("Wrong element at 2", "2", list.get(1));
+ assertEquals("Wrong element at 3", "3", list.get(2));
+ }
+
+ /**
+ * Tests whether parsing of lists can be disabled.
+ */
+ @Test
+ public void testListParsingDisabled() throws ConfigurationException
+ {
+ HierarchicalINIConfiguration config =
+ new HierarchicalINIConfiguration();
+ config.setDelimiterParsingDisabled(true);
+ load(config, "[test]" + LINE_SEPARATOR + "nolist=1,2,3");
+ assertEquals("Wrong value", "1,2,3", config.getString("test.nolist"));
+ }
+
+ /**
+ * A thread class for testing concurrent access to the global section.
+ */
+ private static class GlobalSectionTestThread extends Thread
+ {
+ /** The configuration. */
+ private final HierarchicalINIConfiguration config;
+
+ /** A flag whether an error was found. */
+ volatile boolean error;
+
+ /**
+ * Creates a new instance of <code>GlobalSectionTestThread</code> and
+ * initializes it.
+ *
+ * @param conf the configuration object
+ */
+ public GlobalSectionTestThread(HierarchicalINIConfiguration conf)
+ {
+ config = conf;
+ }
+
+ /**
+ * Accesses the global section in a loop. If there is no correct
+ * synchronization, this can cause an exception.
+ */
+ @Override
+ public void run()
+ {
+ final int loopCount = 250;
+
+ for (int i = 0; i < loopCount && !error; i++)
+ {
+ try
+ {
+ config.getSection(null);
+ }
+ catch (IllegalStateException istex)
+ {
+ error = true;
+ }
+ }
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestHierarchicalXMLConfiguration.java b/src/test/java/org/apache/commons/configuration/TestHierarchicalXMLConfiguration.java
new file mode 100644
index 0000000..c7a49ca
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestHierarchicalXMLConfiguration.java
@@ -0,0 +1,296 @@
+package org.apache.commons.configuration;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Test class for XMLConfiguration. In addition to TestXMLConfiguration this
+ * class especially tests the hierarchical nature of this class and structured
+ * data access.
+ *
+ * @author Emmanuel Bourg
+ * @author Mark Woodman
+ * @version $Id: TestHierarchicalXMLConfiguration.java 1224764 2011-12-26 17:14:49Z oheger $
+ */
+public class TestHierarchicalXMLConfiguration
+{
+ /** Test resources directory. */
+ private static final String TEST_DIR = "conf";
+
+ /** Test file #1 **/
+ private static final String TEST_FILENAME = "testHierarchicalXMLConfiguration.xml";
+
+ /** Test file #2 **/
+ private static final String TEST_FILENAME2 = "testHierarchicalXMLConfiguration2.xml";
+
+ /** Test file path #1 **/
+ private static final String TEST_FILE = ConfigurationAssert.getTestFile(TEST_FILENAME).getAbsolutePath();
+
+ /** Test file path #2 **/
+ private static final String TEST_FILE2 = ConfigurationAssert.getTestFile(TEST_FILENAME2).getAbsolutePath();
+
+ /** Test file path #3.*/
+ private static final String TEST_FILE3 = ConfigurationAssert.getTestFile("test.xml").getAbsolutePath();
+
+ /** File name for saving.*/
+ private static final String TEST_SAVENAME = "testhierarchicalsave.xml";
+
+ /** Helper object for creating temporary files. */
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ /** Instance config used for tests. */
+ private XMLConfiguration config;
+
+ /** Fixture setup. */
+ @Before
+ public void setUp() throws Exception
+ {
+ config = new XMLConfiguration();
+ }
+
+ private void configTest(XMLConfiguration config)
+ {
+ assertEquals(1, config.getMaxIndex("tables.table"));
+ assertEquals("system", config.getProperty("tables.table(0)[@tableType]"));
+ assertEquals("application", config.getProperty("tables.table(1)[@tableType]"));
+
+ assertEquals("users", config.getProperty("tables.table(0).name"));
+ assertEquals("documents", config.getProperty("tables.table(1).name"));
+
+ Object prop = config.getProperty("tables.table.fields.field.name");
+ assertTrue(prop instanceof Collection);
+ assertEquals(10, ((Collection<?>) prop).size());
+
+ prop = config.getProperty("tables.table(0).fields.field.type");
+ assertTrue(prop instanceof Collection);
+ assertEquals(5, ((Collection<?>) prop).size());
+
+ prop = config.getProperty("tables.table(1).fields.field.type");
+ assertTrue(prop instanceof Collection);
+ assertEquals(5, ((Collection<?>) prop).size());
+ }
+
+ @Test
+ public void testGetProperty() throws Exception
+ {
+ config.setFileName(TEST_FILE);
+ config.load();
+
+ configTest(config);
+ }
+
+ @Test
+ public void testLoadURL() throws Exception
+ {
+ config.load(new File(TEST_FILE).getAbsoluteFile().toURI().toURL());
+ configTest(config);
+ }
+
+ @Test
+ public void testLoadBasePath1() throws Exception
+ {
+ config.setBasePath(TEST_DIR);
+ config.setFileName(TEST_FILENAME);
+ config.load();
+ configTest(config);
+ }
+
+ @Test
+ public void testLoadBasePath2() throws Exception
+ {
+ config.setBasePath(new File(TEST_FILE).getAbsoluteFile().toURI().toURL().toString());
+ config.setFileName(TEST_FILENAME);
+ config.load();
+ configTest(config);
+ }
+
+ /**
+ * Ensure various node types are correctly processed in config.
+ * @throws Exception
+ */
+ @Test
+ public void testXmlNodeTypes() throws Exception
+ {
+ // Number of keys expected from test configuration file
+ final int KEY_COUNT = 5;
+
+ // Load the configuration file
+ config.load(new File(TEST_FILE2).getAbsoluteFile().toURI().toURL());
+
+ // Validate comment in element ignored
+ assertEquals("Comment in element must not change element value.", "Case1Text", config
+ .getString("case1"));
+
+ // Validate sibling comment ignored
+ assertEquals("Comment as sibling must not change element value.", "Case2Text", config
+ .getString("case2.child"));
+
+ // Validate comment ignored, CDATA processed
+ assertEquals("Comment and use of CDATA must not change element value.", "Case3Text", config
+ .getString("case3"));
+
+ // Validate comment and processing instruction ignored
+ assertEquals("Comment and use of PI must not change element value.", "Case4Text", config
+ .getString("case4"));
+
+ // Validate comment ignored in parent attribute
+ assertEquals("Comment must not change attribute node value.", "Case5Text", config
+ .getString("case5[@attr]"));
+
+ // Validate non-text nodes haven't snuck in as keys
+ Iterator<String> iter = config.getKeys();
+ int count = 0;
+ while (iter.hasNext())
+ {
+ iter.next();
+ count++;
+ }
+ assertEquals("Config must contain only " + KEY_COUNT + " keys.", KEY_COUNT, count);
+ }
+
+ @Test
+ public void testSave() throws Exception
+ {
+ config.setFileName(TEST_FILE3);
+ config.load();
+ File saveFile = folder.newFile(TEST_SAVENAME);
+ config.save(saveFile);
+
+ config = new XMLConfiguration();
+ config.load(saveFile.toURI().toURL());
+ assertEquals("value", config.getProperty("element"));
+ assertEquals("I'm complex!", config.getProperty("element2.subelement.subsubelement"));
+ assertEquals(8, config.getInt("test.short"));
+ assertEquals("one", config.getString("list(0).item(0)[@name]"));
+ assertEquals("two", config.getString("list(0).item(1)"));
+ assertEquals("six", config.getString("list(1).sublist.item(1)"));
+ }
+
+ /**
+ * Tests to save a newly created configuration.
+ */
+ @Test
+ public void testSaveNew() throws Exception
+ {
+ config.addProperty("connection.url", "jdbc://mydb:1234");
+ config.addProperty("connection.user", "scott");
+ config.addProperty("connection.passwd", "tiger");
+ config.addProperty("connection[@type]", "system");
+ config.addProperty("tables.table.name", "tests");
+ config.addProperty("tables.table(0).fields.field.name", "test_id");
+ config.addProperty("tables.table(0).fields.field(-1).name", "test_name");
+ config.addProperty("tables.table(-1).name", "results");
+ config.addProperty("tables.table(1).fields.field.name", "res_id");
+ config.addProperty("tables.table(1).fields.field(0).type", "int");
+ config.addProperty("tables.table(1).fields.field(-1).name", "value");
+ config.addProperty("tables.table(1).fields.field(1).type", "string");
+ config.addProperty("tables.table(1).fields.field(1)[@null]", "true");
+
+ File saveFile = folder.newFile(TEST_SAVENAME);
+ config.setFile(saveFile);
+ config.setRootElementName("myconfig");
+ config.save();
+
+ config = new XMLConfiguration();
+ config.load(saveFile);
+ assertEquals(1, config.getMaxIndex("tables.table.name"));
+ assertEquals("tests", config.getString("tables.table(0).name"));
+ assertEquals("test_name", config.getString("tables.table(0).fields.field(1).name"));
+ assertEquals("int", config.getString("tables.table(1).fields.field(0).type"));
+ assertTrue(config.getBoolean("tables.table(1).fields.field(1)[@null]"));
+ assertEquals("tiger", config.getString("connection.passwd"));
+ assertEquals("system", config.getProperty("connection[@type]"));
+ assertEquals("myconfig", config.getRootElementName());
+ }
+
+ /**
+ * Tests to save a modified configuration.
+ */
+ @Test
+ public void testSaveModified() throws Exception
+ {
+ config.setFile(new File(TEST_FILE3));
+ config.load();
+
+ assertTrue(config.getString("mean").startsWith("This is\n A long story..."));
+ assertTrue(config.getString("mean").indexOf("And even longer") > 0);
+ config.clearProperty("test.entity[@name]");
+ config.setProperty("element", "new value");
+ config.setProperty("test(0)", "A <new> value");
+ config.addProperty("test(1).int", new Integer(9));
+ config.addProperty("list(1).sublist.item", "seven");
+ config.setProperty("clear", "yes");
+ config.setProperty("mean", "now it's simple");
+ config.addProperty("[@topattr]", "available");
+ config.addProperty("[@topattr]", "successfull");
+
+ File saveFile = folder.newFile(TEST_SAVENAME);
+ config.save(saveFile);
+ config = new XMLConfiguration();
+ config.load(saveFile.getAbsolutePath());
+ assertFalse(config.containsKey("test.entity[@name]"));
+ assertEquals("1<2", config.getProperty("test.entity"));
+ assertEquals("new value", config.getString("element"));
+ assertEquals("A <new> value", config.getProperty("test(0)"));
+ assertEquals((short) 8, config.getShort("test(1).short"));
+ assertEquals(9, config.getInt("test(1).int"));
+ assertEquals("six", config.getProperty("list(1).sublist.item(1)"));
+ assertEquals("seven", config.getProperty("list(1).sublist.item(2)"));
+ assertEquals("yes", config.getProperty("clear"));
+ assertEquals("now it's simple", config.getString("mean"));
+ assertEquals("available", config.getString("[@topattr](0)"));
+ assertEquals("successfull", config.getString("[@topattr](1)"));
+ }
+
+ /**
+ * Tests manipulation of the root element's name.
+ */
+ @Test
+ public void testRootElement() throws Exception
+ {
+ assertEquals("configuration", config.getRootElementName());
+ config.setRootElementName("newRootName");
+ assertEquals("newRootName", config.getRootElementName());
+ }
+
+ /**
+ * Tests that it is not allowed to change the root element name when the
+ * configuration was loaded from a file.
+ */
+ @Test(expected = UnsupportedOperationException.class)
+ public void testSetRootElementNameWhenLoadedFromFile() throws Exception
+ {
+ config.setFile(new File(TEST_FILE3));
+ config.load();
+ assertEquals("testconfig", config.getRootElementName());
+ config.setRootElementName("anotherRootElement");
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestINIConfiguration.java b/src/test/java/org/apache/commons/configuration/TestINIConfiguration.java
new file mode 100644
index 0000000..cf71ea5
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestINIConfiguration.java
@@ -0,0 +1,256 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Test;
+
+/**
+ * Test class for INIConfiguration.
+ *
+ * @author Trevor Miller
+ * @version $Id: TestINIConfiguration.java 1224770 2011-12-26 17:18:36Z oheger $
+ */
+ at SuppressWarnings("deprecation")
+public class TestINIConfiguration
+{
+ private static String LINE_SEPARATOR = System.getProperty("line.separator");
+
+ /** Constant for the content of an ini file. */
+ private static final String INI_DATA =
+ "[section1]" + LINE_SEPARATOR
+ + "var1 = foo" + LINE_SEPARATOR
+ + "var2 = 451" + LINE_SEPARATOR
+ + LINE_SEPARATOR
+ + "[section2]" + LINE_SEPARATOR
+ + "var1 = 123.45" + LINE_SEPARATOR
+ + "var2 = bar" + LINE_SEPARATOR
+ + LINE_SEPARATOR
+ + "[section3]" + LINE_SEPARATOR
+ + "var1 = true" + LINE_SEPARATOR
+ + "interpolated = ${section3.var1}" + LINE_SEPARATOR
+ + "multi = foo" + LINE_SEPARATOR
+ + "multi = bar" + LINE_SEPARATOR
+ + LINE_SEPARATOR;
+
+ private static final String INI_DATA2 =
+ "[section4]" + LINE_SEPARATOR
+ + "var1 = \"quoted value\"" + LINE_SEPARATOR
+ + "var2 = \"quoted value\\nwith \\\"quotes\\\"\"" + LINE_SEPARATOR
+ + "var3 = 123 ; comment" + LINE_SEPARATOR
+ + "var4 = \"1;2;3\" ; comment" + LINE_SEPARATOR
+ + "var5 = '\\'quoted\\' \"value\"' ; comment";
+
+ /**
+ * Test of save method, of class {@link INIConfiguration}.
+ */
+ @Test
+ public void testSave() throws Exception
+ {
+ Writer writer = new StringWriter();
+ INIConfiguration instance = new INIConfiguration();
+ instance.addProperty("section1.var1", "foo");
+ instance.addProperty("section1.var2", "451");
+ instance.addProperty("section2.var1", "123.45");
+ instance.addProperty("section2.var2", "bar");
+ instance.addProperty("section3.var1", "true");
+ instance.addProperty("section3.interpolated", "${section3.var1}");
+ instance.addProperty("section3.multi", "foo");
+ instance.addProperty("section3.multi", "bar");
+ instance.save(writer);
+
+ assertEquals("Wrong content of ini file", INI_DATA, writer.toString());
+ }
+
+ /**
+ * Test of load method, of class {@link INIConfiguration}.
+ */
+ @Test
+ public void testLoad() throws Exception
+ {
+ checkLoad(INI_DATA);
+ }
+
+ /**
+ * Tests the load() method when the alternative value separator is used (a
+ * ':' for '=').
+ */
+ @Test
+ public void testLoadAlternativeSeparator() throws Exception
+ {
+ checkLoad(INI_DATA.replace('=', ':'));
+ }
+
+ /**
+ * Helper method for testing the load operation. Loads the specified content
+ * into a configuration and then checks some properties.
+ *
+ * @param data the data to load
+ */
+ private void checkLoad(String data) throws ConfigurationException, IOException
+ {
+ Reader reader = new StringReader(data);
+ INIConfiguration instance = new INIConfiguration();
+ instance.load(reader);
+ reader.close();
+ assertTrue(instance.getString("section1.var1").equals("foo"));
+ assertTrue(instance.getInt("section1.var2") == 451);
+ assertTrue(instance.getDouble("section2.var1") == 123.45);
+ assertTrue(instance.getString("section2.var2").equals("bar"));
+ assertTrue(instance.getBoolean("section3.var1"));
+ assertTrue(instance.getSections().size() == 3);
+ }
+
+ /**
+ * Test of isCommentLine method, of class {@link INIConfiguration}.
+ */
+ @Test
+ public void testIsCommentLine()
+ {
+ INIConfiguration instance = new INIConfiguration();
+ assertTrue(instance.isCommentLine("#comment1"));
+ assertTrue(instance.isCommentLine(";comment1"));
+ assertFalse(instance.isCommentLine("nocomment=true"));
+ assertFalse(instance.isCommentLine(null));
+ }
+
+ /**
+ * Test of isSectionLine method, of class {@link INIConfiguration}.
+ */
+ @Test
+ public void testIsSectionLine()
+ {
+ INIConfiguration instance = new INIConfiguration();
+ assertTrue(instance.isSectionLine("[section]"));
+ assertFalse(instance.isSectionLine("nosection=true"));
+ assertFalse(instance.isSectionLine(null));
+ }
+
+ /**
+ * Test of getSections method, of class {@link INIConfiguration}.
+ */
+ @Test
+ public void testGetSections()
+ {
+ INIConfiguration instance = new INIConfiguration();
+ instance.addProperty("test1.foo", "bar");
+ instance.addProperty("test2.foo", "abc");
+ Set<String> expResult = new HashSet<String>();
+ expResult.add("test1");
+ expResult.add("test2");
+ Set<String> result = instance.getSections();
+ assertEquals(expResult, result);
+ }
+
+ @Test
+ public void testQuotedValue() throws Exception
+ {
+ INIConfiguration config = new INIConfiguration();
+ config.load(new StringReader(INI_DATA2));
+
+ assertEquals("value", "quoted value", config.getString("section4.var1"));
+ }
+
+ @Test
+ public void testQuotedValueWithQuotes() throws Exception
+ {
+ INIConfiguration config = new INIConfiguration();
+ config.load(new StringReader(INI_DATA2));
+
+ assertEquals("value", "quoted value\\nwith \"quotes\"", config.getString("section4.var2"));
+ }
+
+ @Test
+ public void testValueWithComment() throws Exception
+ {
+ INIConfiguration config = new INIConfiguration();
+ config.load(new StringReader(INI_DATA2));
+
+ assertEquals("value", "123", config.getString("section4.var3"));
+ }
+
+ @Test
+ public void testQuotedValueWithComment() throws Exception
+ {
+ INIConfiguration config = new INIConfiguration();
+ config.load(new StringReader(INI_DATA2));
+
+ assertEquals("value", "1;2;3", config.getString("section4.var4"));
+ }
+
+ @Test
+ public void testQuotedValueWithSingleQuotes() throws Exception
+ {
+ INIConfiguration config = new INIConfiguration();
+ config.load(new StringReader(INI_DATA2));
+
+ assertEquals("value", "'quoted' \"value\"", config.getString("section4.var5"));
+ }
+
+ @Test
+ public void testWriteValueWithCommentChar() throws Exception
+ {
+ INIConfiguration config = new INIConfiguration();
+ config.setProperty("section.key1", "1;2;3");
+
+ StringWriter writer = new StringWriter();
+ config.save(writer);
+
+ INIConfiguration config2 = new INIConfiguration();
+ config2.load(new StringReader(writer.toString()));
+
+ assertEquals("value", "1;2;3", config2.getString("section.key1"));
+ }
+
+ /**
+ * Tests whether whitespace is left unchanged for quoted values.
+ */
+ @Test
+ public void testQuotedValueWithWhitespace() throws Exception
+ {
+ final String content = "CmdPrompt = \" [test at cmd ~]$ \"";
+ INIConfiguration config = new INIConfiguration();
+ config.load(new StringReader(content));
+ assertEquals("Wrong propert value", " [test at cmd ~]$ ", config
+ .getString("CmdPrompt"));
+ }
+
+ /**
+ * Tests a quoted value with space and a comment.
+ */
+ @Test
+ public void testQuotedValueWithWhitespaceAndComment() throws Exception
+ {
+ final String content = "CmdPrompt = \" [test at cmd ~]$ \" ; a comment";
+ INIConfiguration config = new INIConfiguration();
+ config.load(new StringReader(content));
+ assertEquals("Wrong propert value", " [test at cmd ~]$ ", config
+ .getString("CmdPrompt"));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestJNDIConfiguration.java b/src/test/java/org/apache/commons/configuration/TestJNDIConfiguration.java
new file mode 100644
index 0000000..3e28845
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestJNDIConfiguration.java
@@ -0,0 +1,375 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Hashtable;
+import java.util.Properties;
+
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test to see if the JNDIConfiguration works properly.
+ *
+ * @version $Id: TestJNDIConfiguration.java 1301996 2012-03-17 20:30:39Z sebb $
+ */
+public class TestJNDIConfiguration {
+
+ public static final String CONTEXT_FACTORY = MockInitialContextFactory.class.getName();
+
+ private PotentialErrorJNDIConfiguration conf;
+ private NonStringTestHolder nonStringTestHolder;
+
+ /** A test error listener for counting internal errors.*/
+ private ConfigurationErrorListenerImpl listener;
+
+ @Before
+ public void setUp() throws Exception {
+
+ System.setProperty("java.naming.factory.initial", CONTEXT_FACTORY);
+
+ Properties props = new Properties();
+ props.put("java.naming.factory.initial", CONTEXT_FACTORY);
+ Context ctx = new InitialContext(props);
+ conf = new PotentialErrorJNDIConfiguration(ctx);
+
+ nonStringTestHolder = new NonStringTestHolder();
+ nonStringTestHolder.setConfiguration(conf);
+
+ listener = new ConfigurationErrorListenerImpl();
+ conf.addErrorListener(listener);
+ }
+
+ /**
+ * Clears the test environment. If an error listener is defined, checks
+ * whether no error event was received.
+ */
+ @After
+ public void tearDown() throws Exception
+ {
+ if (listener != null)
+ {
+ listener.verify();
+ }
+ }
+
+ @Test
+ public void testBoolean() throws Exception {
+ nonStringTestHolder.testBoolean();
+ }
+
+ @Test
+ public void testBooleanDefaultValue() throws Exception {
+ nonStringTestHolder.testBooleanDefaultValue();
+ }
+
+ @Test
+ public void testByte() throws Exception {
+ nonStringTestHolder.testByte();
+ }
+
+ @Test
+ public void testDouble() throws Exception {
+ nonStringTestHolder.testDouble();
+ }
+
+ @Test
+ public void testDoubleDefaultValue() throws Exception {
+ nonStringTestHolder.testDoubleDefaultValue();
+ }
+
+ @Test
+ public void testFloat() throws Exception {
+ nonStringTestHolder.testFloat();
+ }
+
+ @Test
+ public void testFloatDefaultValue() throws Exception {
+ nonStringTestHolder.testFloatDefaultValue();
+ }
+
+ @Test
+ public void testInteger() throws Exception {
+ nonStringTestHolder.testInteger();
+ }
+
+ @Test
+ public void testIntegerDefaultValue() throws Exception {
+ nonStringTestHolder.testIntegerDefaultValue();
+ }
+
+ @Test
+ public void testLong() throws Exception {
+ nonStringTestHolder.testLong();
+ }
+
+ @Test
+ public void testLongDefaultValue() throws Exception {
+ nonStringTestHolder.testLongDefaultValue();
+ }
+
+ @Test
+ public void testShort() throws Exception {
+ nonStringTestHolder.testShort();
+ }
+
+ @Test
+ public void testShortDefaultValue() throws Exception {
+ nonStringTestHolder.testShortDefaultValue();
+ }
+
+ @Test
+ public void testListMissing() throws Exception {
+ nonStringTestHolder.testListMissing();
+ }
+
+ @Test
+ public void testSubset() throws Exception {
+ nonStringTestHolder.testSubset();
+ }
+
+ @Test
+ public void testProperties() throws Exception {
+ Object o = conf.getProperty("test.boolean");
+ assertNotNull(o);
+ assertEquals("true", o.toString());
+ }
+
+ @Test
+ public void testContainsKey()
+ {
+ String key = "test.boolean";
+ assertTrue("'" + key + "' not found", conf.containsKey(key));
+
+ conf.clearProperty(key);
+ assertFalse("'" + key + "' still found", conf.containsKey(key));
+ }
+
+ @Test
+ public void testChangePrefix()
+ {
+ assertEquals("'test.boolean' property", "true", conf.getString("test.boolean"));
+ assertEquals("'boolean' property", null, conf.getString("boolean"));
+
+ // change the prefix
+ conf.setPrefix("test");
+ assertEquals("'test.boolean' property", null, conf.getString("test.boolean"));
+ assertEquals("'boolean' property", "true", conf.getString("boolean"));
+ }
+
+ @Test
+ public void testResetRemovedProperties() throws Exception
+ {
+ assertEquals("'test.boolean' property", "true", conf.getString("test.boolean"));
+
+ // remove the property
+ conf.clearProperty("test.boolean");
+ assertEquals("'test.boolean' property", null, conf.getString("test.boolean"));
+
+ // change the context
+ conf.setContext(new InitialContext());
+
+ // get the property
+ assertEquals("'test.boolean' property", "true", conf.getString("test.boolean"));
+ }
+
+ @Test
+ public void testConstructor() throws Exception
+ {
+ // test the constructor accepting a context
+ JNDIConfiguration c = new JNDIConfiguration(new InitialContext());
+
+ assertEquals("'test.boolean' property", "true", c.getString("test.boolean"));
+
+ // test the constructor accepting a context and a prefix
+ c = new JNDIConfiguration(new InitialContext(), "test");
+
+ assertEquals("'boolean' property", "true", c.getString("boolean"));
+ }
+
+ /**
+ * Configures the test config to throw an exception.
+ */
+ private PotentialErrorJNDIConfiguration setUpErrorConfig()
+ {
+ conf.installException();
+ conf.removeErrorListener(conf.getErrorListeners().iterator().next());
+ return conf;
+ }
+
+ /**
+ * Tests whether the expected error events have been received.
+ *
+ * @param type the expected event type
+ * @param propName the name of the property
+ * @param propValue the property value
+ */
+ private void checkErrorListener(int type, String propName, Object propValue)
+ {
+ listener.verify(type, propName, propValue);
+ assertTrue("Wrong exception class",
+ listener.getLastEvent().getCause() instanceof NamingException);
+ listener = null;
+ }
+
+ /**
+ * Tests whether a JNDI configuration registers an error log listener.
+ */
+ @Test
+ public void testLogListener() throws NamingException
+ {
+ JNDIConfiguration c = new JNDIConfiguration();
+ assertEquals("No error log listener registered", 1, c
+ .getErrorListeners().size());
+ }
+
+ /**
+ * Tests handling of errors in getKeys().
+ */
+ @Test
+ public void testGetKeysError()
+ {
+ assertFalse("Iteration not empty", setUpErrorConfig().getKeys()
+ .hasNext());
+ checkErrorListener(AbstractConfiguration.EVENT_READ_PROPERTY, null,
+ null);
+ }
+
+ /**
+ * Tests handling of errors in isEmpty().
+ */
+ @Test
+ public void testIsEmptyError() throws Exception
+ {
+ assertTrue("Error config not empty", setUpErrorConfig().isEmpty());
+ checkErrorListener(AbstractConfiguration.EVENT_READ_PROPERTY, null,
+ null);
+ }
+
+ /**
+ * Tests handling of errors in the containsKey() method.
+ */
+ @Test
+ public void testContainsKeyError()
+ {
+ assertFalse("Key contained after error", setUpErrorConfig()
+ .containsKey("key"));
+ checkErrorListener(AbstractConfiguration.EVENT_READ_PROPERTY, "key",
+ null);
+ }
+
+ /**
+ * Tests handling of errors in getProperty().
+ */
+ @Test
+ public void testGetPropertyError()
+ {
+ assertNull("Wrong property value after error", setUpErrorConfig()
+ .getProperty("key"));
+ checkErrorListener(AbstractConfiguration.EVENT_READ_PROPERTY, "key",
+ null);
+ }
+
+ /**
+ * Tests the getKeys() method when there are cycles in the tree.
+ */
+ @Test
+ public void testGetKeysWithCycles() throws NamingException
+ {
+ Hashtable<Object, Object> env = new Hashtable<Object, Object>();
+ env.put(MockInitialContextFactory.PROP_CYCLES, Boolean.TRUE);
+ InitialContext initCtx = new InitialContext(env);
+ JNDIConfiguration c = new JNDIConfiguration(initCtx);
+ c.getKeys("cycle");
+ }
+
+ /**
+ * Tests getKeys() if no data is found. This should not cause a problem and
+ * not notify the error listeners.
+ */
+ @Test
+ public void testGetKeysNoData()
+ {
+ conf.installException(new NameNotFoundException("Test exception"));
+ assertFalse("Got keys", conf.getKeys().hasNext());
+ listener.verify();
+ }
+
+ /**
+ * A special JNDI configuration implementation that can be configured to
+ * throw an exception when accessing the base context. Used for testing the
+ * exception handling.
+ */
+ public static class PotentialErrorJNDIConfiguration extends
+ JNDIConfiguration
+ {
+ /** An exception to be thrown by getBaseContext(). */
+ private NamingException exception;
+
+ public PotentialErrorJNDIConfiguration(Context ctx)
+ throws NamingException
+ {
+ super(ctx);
+ }
+
+ /**
+ * Prepares this object to throw an exception when the JNDI context is
+ * queried.
+ *
+ * @param nex the exception to be thrown
+ */
+ public void installException(NamingException nex)
+ {
+ exception = nex;
+ }
+
+ /**
+ * Prepares this object to throw a standard exception when the JNDI
+ * context is queried.
+ */
+ public void installException()
+ {
+ installException(new NamingException("Simulated JNDI exception!"));
+ }
+
+ /**
+ * Returns the JNDI context. Optionally throws an exception.
+ */
+ @Override
+ public Context getBaseContext() throws NamingException
+ {
+ if (exception != null)
+ {
+ throw exception;
+ }
+ return super.getBaseContext();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/commons/configuration/TestJNDIEnvironmentValues.java b/src/test/java/org/apache/commons/configuration/TestJNDIEnvironmentValues.java
new file mode 100644
index 0000000..56d7339
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestJNDIEnvironmentValues.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestJNDIEnvironmentValues
+{
+ private JNDIConfiguration conf = null;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ System.setProperty("java.naming.factory.initial", TestJNDIConfiguration.CONTEXT_FACTORY);
+
+ conf = new JNDIConfiguration();
+ conf.setThrowExceptionOnMissing(true);
+ }
+
+ @Test
+ public void testThrowExceptionOnMissing()
+ {
+ assertTrue("Throw Exception Property is not set!", conf.isThrowExceptionOnMissing());
+ }
+
+ @Test
+ public void testSimpleGet() throws Exception
+ {
+ String s = conf.getString("test.key");
+ assertEquals("jndivalue", s);
+ }
+
+ @Test
+ public void testMoreGets() throws Exception
+ {
+ String s = conf.getString("test.key");
+ assertEquals("jndivalue", s);
+ assertEquals("jndivalue2", conf.getString("test.key2"));
+ assertEquals(1, conf.getShort("test.short"));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testGetMissingKey() throws Exception
+ {
+ conf.getString("test.imaginarykey");
+ }
+
+ @Test
+ public void testGetMissingKeyWithDefault() throws Exception
+ {
+ String result = conf.getString("test.imaginarykey", "bob");
+ assertEquals("bob", result);
+ }
+
+ @Test
+ public void testContainsKey() throws Exception
+ {
+ assertTrue(conf.containsKey("test.key"));
+ assertTrue(!conf.containsKey("test.imaginarykey"));
+ }
+
+ @Test
+ public void testClearProperty()
+ {
+ assertNotNull("null short for the 'test.short' key", conf.getShort("test.short", null));
+ conf.clearProperty("test.short");
+ assertNull("'test.short' property not cleared", conf.getShort("test.short", null));
+ }
+
+ @Test
+ public void testIsEmpty()
+ {
+ assertFalse("the configuration shouldn't be empty", conf.isEmpty());
+ }
+
+ @Test
+ public void testGetKeys() throws Exception
+ {
+ boolean found = false;
+ Iterator<String> it = conf.getKeys();
+
+ assertTrue("no key found", it.hasNext());
+
+ while (it.hasNext() && !found)
+ {
+ found = "test.boolean".equals(it.next());
+ }
+
+ assertTrue("'test.boolean' key not found", found);
+ }
+
+ @Test
+ public void testGetKeysWithUnknownPrefix()
+ {
+ // test for a unknown prefix
+ Iterator<String> it = conf.getKeys("foo.bar");
+ assertFalse("no key should be found", it.hasNext());
+ }
+
+ @Test
+ public void testGetKeysWithExistingPrefix()
+ {
+ // test for an existing prefix
+ Iterator<String> it = conf.getKeys("test");
+ boolean found = false;
+ while (it.hasNext() && !found)
+ {
+ found = "test.boolean".equals(it.next());
+ }
+
+ assertTrue("'test.boolean' key not found", found);
+ }
+
+ @Test
+ public void testGetKeysWithKeyAsPrefix()
+ {
+ // test for a prefix matching exactly the key of a property
+ Iterator<String> it = conf.getKeys("test.boolean");
+ boolean found = false;
+ while (it.hasNext() && !found)
+ {
+ found = "test.boolean".equals(it.next());
+ }
+
+ assertTrue("'test.boolean' key not found", found);
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestMapConfiguration.java b/src/test/java/org/apache/commons/configuration/TestMapConfiguration.java
new file mode 100644
index 0000000..1e3def7
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestMapConfiguration.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.junit.Test;
+
+/**
+ * Tests for MapConfiguration.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: TestMapConfiguration.java 1222465 2011-12-22 21:32:56Z oheger $
+ */
+public class TestMapConfiguration extends TestAbstractConfiguration
+{
+ /** Constant for a test key.*/
+ private static final String KEY = "key1";
+
+ /** Constant for a test property value with whitespace.*/
+ private static final String SPACE_VALUE = " Value with whitespace ";
+
+ /** The trimmed test value.*/
+ private static final String TRIM_VALUE = SPACE_VALUE.trim();
+
+ @Override
+ protected AbstractConfiguration getConfiguration()
+ {
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.put(KEY, "value1");
+ map.put("key2", "value2");
+ map.put("list", "value1, value2");
+ map.put("listesc", "value1\\,value2");
+
+ return new MapConfiguration(map);
+ }
+
+ @Override
+ protected AbstractConfiguration getEmptyConfiguration()
+ {
+ return new MapConfiguration(new HashMap<String, Object>());
+ }
+
+ @Test
+ public void testGetMap()
+ {
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ MapConfiguration conf = new MapConfiguration(map);
+ assertEquals(map, conf.getMap());
+ }
+
+ @Test
+ public void testClone()
+ {
+ MapConfiguration config = (MapConfiguration) getConfiguration();
+ MapConfiguration copy = (MapConfiguration) config.clone();
+ StrictConfigurationComparator comp = new StrictConfigurationComparator();
+ assertTrue("Configurations are not equal", comp.compare(config, copy));
+ }
+
+ /**
+ * Tests if the cloned configuration is decoupled from the original.
+ */
+ @Test
+ public void testCloneModify()
+ {
+ MapConfiguration config = (MapConfiguration) getConfiguration();
+ config.addConfigurationListener(new ConfigurationListener()
+ {
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ // Just a dummy
+ }
+ });
+ MapConfiguration copy = (MapConfiguration) config.clone();
+ assertTrue("Event listeners were copied", copy
+ .getConfigurationListeners().isEmpty());
+
+ config.addProperty("cloneTest", Boolean.TRUE);
+ assertFalse("Map not decoupled", copy.containsKey("cloneTest"));
+ copy.clearProperty("key1");
+ assertEquals("Map not decoupled (2)", "value1", config
+ .getString("key1"));
+ }
+
+ /**
+ * Tests adding another value to an existing property.
+ */
+ @Test
+ public void testAddProperty()
+ {
+ MapConfiguration config = (MapConfiguration) getConfiguration();
+ config.addProperty(KEY, TRIM_VALUE);
+ config.addProperty(KEY, "anotherValue");
+ List<Object> values = config.getList(KEY);
+ assertEquals("Wrong number of values", 3, values.size());
+ }
+
+ /**
+ * Tests querying a property when trimming is active.
+ */
+ @Test
+ public void testGetPropertyTrim()
+ {
+ MapConfiguration config = (MapConfiguration) getConfiguration();
+ config.getMap().put(KEY, SPACE_VALUE);
+ assertEquals("Wrong trimmed value", TRIM_VALUE, config.getProperty(KEY));
+ }
+
+ /**
+ * Tests querying a property when trimming is disabled.
+ */
+ @Test
+ public void testGetPropertyTrimDisabled()
+ {
+ MapConfiguration config = (MapConfiguration) getConfiguration();
+ config.getMap().put(KEY, SPACE_VALUE);
+ config.setTrimmingDisabled(true);
+ assertEquals("Wrong trimmed value", SPACE_VALUE, config.getProperty(KEY));
+ }
+
+ /**
+ * Tests querying a property when trimming is enabled, but list splitting is
+ * disabled. In this case no trimming is performed (trimming only works if
+ * list splitting is enabled).
+ */
+ @Test
+ public void testGetPropertyTrimNoSplit()
+ {
+ MapConfiguration config = (MapConfiguration) getConfiguration();
+ config.getMap().put(KEY, SPACE_VALUE);
+ config.setDelimiterParsingDisabled(true);
+ assertEquals("Wrong trimmed value", SPACE_VALUE, config.getProperty(KEY));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestMapConfigurationRegression.java b/src/test/java/org/apache/commons/configuration/TestMapConfigurationRegression.java
new file mode 100644
index 0000000..124df61
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestMapConfigurationRegression.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.UUID;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestMapConfigurationRegression
+{
+ @Test
+ public void testMapConfigurationRegression()
+ {
+ final String key = UUID.randomUUID().toString();
+ final String value = UUID.randomUUID().toString();
+ final Map<String, String> map = Collections.singletonMap(key, value);
+ final MapConfiguration mc = new MapConfiguration(map);
+ Assert.assertEquals(1, mc.getMap().size());
+ Assert.assertEquals(value, mc.getString(key));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestMultiFileHierarchicalConfiguration.java b/src/test/java/org/apache/commons/configuration/TestMultiFileHierarchicalConfiguration.java
new file mode 100644
index 0000000..3967d6e
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestMultiFileHierarchicalConfiguration.java
@@ -0,0 +1,316 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+
+import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;
+import org.junit.Test;
+import org.xml.sax.SAXParseException;
+
+/**
+ * Unit test for simple MultiConfigurationTest.
+ *
+ * @version $Id: TestMultiFileHierarchicalConfiguration.java 1224811 2011-12-26 21:04:25Z oheger $
+ */
+public class TestMultiFileHierarchicalConfiguration
+{
+ private static String PATTERN1 = "target/test-classes/testMultiConfiguration_${sys:Id}.xml";
+
+ private static final File MULTI_TENENT_FILE = new File(
+ "conf/testMultiTenentConfigurationBuilder2.xml");
+
+ private static final File MULTI_TENENT_FILE2 = new File(
+ "target/test-classes/testMultiTenentConfigurationBuilder2.xml");
+
+ private static final File MULTI_RELOAD_FILE = new File(
+ "conf/testMultiTenentConfigurationBuilder3.xml");
+
+ /**
+ * Rigourous Test :-)
+ */
+ @Test
+ public void testMultiConfiguration()
+ {
+ //set up a reloading strategy
+ FileChangedReloadingStrategy strategy = new FileChangedReloadingStrategy();
+ strategy.setRefreshDelay(10000);
+
+ MultiFileHierarchicalConfiguration config = new MultiFileHierarchicalConfiguration(PATTERN1);
+ config.setReloadingStrategy(strategy);
+
+ System.setProperty("Id", "1001");
+ assertTrue(config.getInt("rowsPerPage") == 15);
+
+ System.setProperty("Id", "1002");
+ assertTrue(config.getInt("rowsPerPage") == 25);
+
+ System.setProperty("Id", "1003");
+ assertTrue(config.getInt("rowsPerPage") == 35);
+ }
+
+ @Test
+ public void testSchemaValidationError() throws Exception
+ {
+ System.getProperties().remove("Id");
+ DefaultConfigurationBuilder factory = new DefaultConfigurationBuilder();
+ factory.setFile(MULTI_TENENT_FILE);
+ CombinedConfiguration config = factory.getConfiguration(true);
+ try
+ {
+ System.setProperty("Id", "2001");
+ config.getInt("rowsPerPage");
+ fail("No exception thrown");
+ }
+ catch (Exception ex)
+ {
+ Throwable cause = ex.getCause();
+ while (cause != null && !(cause instanceof SAXParseException))
+ {
+ cause = cause.getCause();
+ }
+ assertTrue("SAXParseException was not thrown", cause instanceof SAXParseException);
+ }
+ }
+
+ @Test
+ public void testSchemaValidation() throws Exception
+ {
+ System.getProperties().remove("Id");
+ DefaultConfigurationBuilder factory = new DefaultConfigurationBuilder();
+ factory.setFile(MULTI_TENENT_FILE);
+ CombinedConfiguration config = factory.getConfiguration(true);
+ System.setProperty("Id", "2002");
+ int rows = config.getInt("rowsPerPage");
+ assertTrue("expected: " + rows + " actual: " + "25", 25 == rows);
+ }
+
+ @Test
+ public void testMissingFile() throws Exception
+ {
+ System.getProperties().remove("Id");
+ DefaultConfigurationBuilder factory = new DefaultConfigurationBuilder();
+ factory.setFile(MULTI_TENENT_FILE);
+ CombinedConfiguration config = factory.getConfiguration(true);
+ System.setProperty("Id", "3099");
+ int rows = config.getInt("rowsPerPage");
+ assertTrue("expected: " + rows + " actual: " + "50", 50 == rows);
+
+ }
+
+ @Test
+ public void testFileReload1() throws Exception
+ {
+ System.getProperties().remove("Id");
+ DefaultConfigurationBuilder factory = new DefaultConfigurationBuilder();
+ factory.setFile(MULTI_RELOAD_FILE);
+ CombinedConfiguration config = factory.getConfiguration(true);
+
+ // create a new configuration
+ File input = new File("target/test-classes/testMultiConfiguration_3001.xml");
+ File output = new File("target/test-classes/testwrite/testMultiConfiguration_3001.xml");
+ output.delete();
+ output.getParentFile().mkdir();
+ copyFile(input, output);
+
+ assertNotNull(config);
+ verify("3001", config, 15);
+ Thread.sleep(1100);
+ XMLConfiguration x = new XMLConfiguration();
+ x.setFile(output);
+ x.setAttributeSplittingDisabled(true);
+ x.setDelimiterParsingDisabled(true);
+ x.load();
+ x.setProperty("rowsPerPage", "35");
+ //Insure orginal timestamp and new timestamp aren't the same second.
+ Thread.sleep(1100);
+ x.save();
+ verify("3001", config, 35);
+ output.delete();
+ }
+
+ @Test
+ public void testFileReload2() throws Exception
+ {
+ // create a new configuration
+ File input = new File("target/test-classes/testMultiConfiguration_3002.xml");
+ File output = new File("target/test-classes/testwrite/testMultiConfiguration_3002.xml");
+ output.delete();
+
+ System.getProperties().remove("Id");
+ DefaultConfigurationBuilder factory = new DefaultConfigurationBuilder();
+ factory.setFile(MULTI_RELOAD_FILE);
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertNotNull(config);
+ // The file should not exist yet.
+ verify("3002", config, 50);
+
+ output.getParentFile().mkdir();
+ copyFile(input, output);
+ Thread.sleep(600);
+ verify("3002", config, 25);
+ output.delete();
+ }
+
+ @Test
+ public void testFileReload3() throws Exception
+ {
+ // create a new configuration
+ File input = new File("target/test-classes/testMultiConfiguration_3001.xml");
+ File output = new File("target/test-classes/testwrite/testMultiConfiguration_3001.xml");
+ output.delete();
+ output.getParentFile().mkdir();
+
+ System.getProperties().remove("Id");
+ DefaultConfigurationBuilder factory = new DefaultConfigurationBuilder();
+ factory.setFile(MULTI_RELOAD_FILE);
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertNotNull(config);
+ //The file does not exist yet.
+ verify("3001", config, 50);
+ copyFile(input, output);
+ //Sleep so refreshDelay elapses
+ Thread.sleep(600);
+ verify("3001", config, 15);
+ Thread.sleep(500);
+ XMLConfiguration x = new XMLConfiguration();
+ x.setFile(output);
+ x.setAttributeSplittingDisabled(true);
+ x.setDelimiterParsingDisabled(true);
+ x.load();
+ x.setProperty("rowsPerPage", "35");
+ // Insure original timestamp and new timestamp are not the same second.
+ Thread.sleep(1100);
+ x.save();
+ verify("3001", config, 35);
+ output.delete();
+ }
+
+ @Test
+ public void testReloadDefault() throws Exception
+ {
+ // create a new configuration
+ String defaultName = "target/test-classes/testMultiConfiguration_default.xml";
+ File input = new File(defaultName);
+
+ System.getProperties().remove("Id");
+ DefaultConfigurationBuilder factory = new DefaultConfigurationBuilder();
+ factory.setFile(MULTI_TENENT_FILE2);
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertNotNull(config);
+ verify("3001", config, 15);
+ verify("3002", config, 25);
+ System.setProperty("Id", "3002");
+ config.addProperty("/ TestProp", "Test");
+ assertTrue("Property not added", "Test".equals(config.getString("TestProp")));
+ System.getProperties().remove("Id");
+ //Sleep so refreshDelay elapses
+ Thread.sleep(600);
+ long time = System.currentTimeMillis();
+ long original = input.lastModified();
+ input.setLastModified(time);
+ File defaultFile = new File(defaultName);
+ long newTime = defaultFile.lastModified();
+ assertTrue("time mismatch", original != newTime);
+ Thread.sleep(600);
+ verify("3001", config, 15);
+ verify("3002", config, 25);
+ System.setProperty("Id", "3002");
+ String test = config.getString("TestProp");
+ assertNull("Property was not cleared by reload", test);
+ }
+
+ @Test
+ public void testFileReloadSchemaValidationError() throws Exception
+ {
+ System.getProperties().remove("Id");
+ DefaultConfigurationBuilder factory = new DefaultConfigurationBuilder();
+ factory.setFile(MULTI_RELOAD_FILE);
+ CombinedConfiguration config = factory.getConfiguration(true);
+
+ // create a new configuration
+ File input = new File("target/test-classes/testMultiConfiguration_3001.xml");
+ File output = new File("target/test-classes/testwrite/testMultiConfiguration_3001.xml");
+ output.delete();
+ output.getParentFile().mkdir();
+ copyFile(input, output);
+
+ assertNotNull(config);
+ verify("3001", config, 15);
+ Thread.sleep(1100);
+ XMLConfiguration x = new XMLConfiguration();
+ x.setFile(output);
+ x.setAttributeSplittingDisabled(true);
+ x.setDelimiterParsingDisabled(true);
+ x.load();
+ x.setProperty("rowsPerPage", "test");
+ //Insure orginal timestamp and new timestamp aren't the same second.
+ Thread.sleep(1100);
+ x.save();
+ System.setProperty("Id", "3001");
+ try
+ {
+ config.getInt("rowsPerPage");
+ fail("No exception was thrown");
+ }
+ catch (Exception ex)
+ {
+
+ }
+
+ output.delete();
+ }
+
+ private void copyFile(File input, File output) throws IOException
+ {
+ Reader reader = new FileReader(input);
+ Writer writer = new FileWriter(output);
+ char[] buffer = new char[4096];
+ int n = 0;
+ while (-1 != (n = reader.read(buffer)))
+ {
+ writer.write(buffer, 0, n);
+ }
+ reader.close();
+ writer.close();
+ }
+
+ private void verify(String key, CombinedConfiguration config, int rows)
+ {
+ if (key == null)
+ {
+ System.getProperties().remove("Id");
+ }
+ else
+ {
+ System.setProperty("Id", key);
+ }
+ int actual = config.getInt("rowsPerPage");
+ assertTrue("expected: " + rows + " actual: " + actual, actual == rows);
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestNonStringProperties.java b/src/test/java/org/apache/commons/configuration/TestNonStringProperties.java
new file mode 100644
index 0000000..1e4e166
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestNonStringProperties.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import org.junit.Before;
+
+/**
+ * Test if non-string properties are handled correctly.
+ *
+ * @version $Id: TestNonStringProperties.java 1222445 2011-12-22 20:53:47Z oheger $
+ */
+public class TestNonStringProperties extends BaseNonStringProperties
+{
+ /** The File that we test with */
+ private String testProperties = ConfigurationAssert.getTestFile("test.properties").getAbsolutePath();
+
+ @Before
+ public void setUp() throws Exception
+ {
+ conf = new PropertiesConfiguration(testProperties);
+ nonStringTestHolder.setConfiguration(conf);
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestNullCompositeConfiguration.java b/src/test/java/org/apache/commons/configuration/TestNullCompositeConfiguration.java
new file mode 100644
index 0000000..934abc5
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestNullCompositeConfiguration.java
@@ -0,0 +1,451 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test loading multiple configurations.
+ *
+ * @version $Id: TestNullCompositeConfiguration.java 1224814 2011-12-26 21:15:53Z oheger $
+ */
+public class TestNullCompositeConfiguration
+{
+ protected PropertiesConfiguration conf1;
+ protected PropertiesConfiguration conf2;
+ protected XMLConfiguration xmlConf;
+ protected CompositeConfiguration cc;
+
+ /** The File that we test with */
+ private String testProperties = ConfigurationAssert.getTestFile("test.properties").getAbsolutePath();
+ private String testProperties2 = ConfigurationAssert.getTestFile("test2.properties").getAbsolutePath();
+ private String testPropertiesXML = ConfigurationAssert.getTestFile("test.xml").getAbsolutePath();
+
+ @Before
+ public void setUp() throws Exception
+ {
+ cc = new CompositeConfiguration();
+ conf1 = new PropertiesConfiguration(testProperties);
+ conf2 = new PropertiesConfiguration(testProperties2);
+ xmlConf = new XMLConfiguration(new File(testPropertiesXML));
+
+ cc.setThrowExceptionOnMissing(false);
+ }
+
+ @Test
+ public void testThrowExceptionOnMissing()
+ {
+ assertFalse("Throw Exception Property is set!", cc.isThrowExceptionOnMissing());
+ }
+
+ @Test
+ public void testAddRemoveConfigurations() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ assertEquals(2, cc.getNumberOfConfigurations());
+ cc.addConfiguration(conf1);
+ assertEquals(2, cc.getNumberOfConfigurations());
+ cc.addConfiguration(conf2);
+ assertEquals(3, cc.getNumberOfConfigurations());
+ cc.removeConfiguration(conf1);
+ assertEquals(2, cc.getNumberOfConfigurations());
+ cc.clear();
+ assertEquals(1, cc.getNumberOfConfigurations());
+ }
+
+ @Test
+ public void testGetPropertyWIncludes() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(conf2);
+ List<Object> l = cc.getList("packages");
+ assertTrue(l.contains("packagea"));
+ }
+
+ @Test
+ public void testGetProperty() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(conf2);
+ assertEquals("Make sure we get the property from conf1 first", "test.properties", cc.getString("propertyInOrder"));
+ cc.clear();
+
+ cc.addConfiguration(conf2);
+ cc.addConfiguration(conf1);
+ assertEquals("Make sure we get the property from conf2 first", "test2.properties", cc.getString("propertyInOrder"));
+ }
+
+ @Test
+ public void testCantRemoveMemoryConfig() throws Exception
+ {
+ cc.clear();
+ assertEquals(1, cc.getNumberOfConfigurations());
+
+ Configuration internal = cc.getConfiguration(0);
+ cc.removeConfiguration(internal);
+
+ assertEquals(1, cc.getNumberOfConfigurations());
+ }
+
+ @Test
+ public void testGetPropertyMissing() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(conf2);
+
+ assertNull("Bogus property is not null!", cc.getString("bogus.property"));
+
+ assertTrue("Should be false", !cc.getBoolean("test.missing.boolean", false));
+ assertTrue("Should be true", cc.getBoolean("test.missing.boolean.true", true));
+ }
+
+ @Test
+ public void testMultipleTypesOfConfigs() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+ assertEquals("Make sure we get the property from conf1 first", 1, cc.getInt("test.short"));
+ cc.clear();
+
+ cc.addConfiguration(xmlConf);
+ cc.addConfiguration(conf1);
+ assertEquals("Make sure we get the property from xml", 8, cc.getInt("test.short"));
+ }
+
+ @Test
+ public void testPropertyExistsInOnlyOneConfig() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+ assertEquals("value", cc.getString("element"));
+ }
+
+ /**
+ * Tests getting a default when the key doesn't exist
+ */
+ @Test
+ public void testDefaultValueWhenKeyMissing() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+ assertEquals("default", cc.getString("bogus", "default"));
+ assertTrue(1.4 == cc.getDouble("bogus", 1.4));
+ assertTrue(1.4 == cc.getDouble("bogus", 1.4));
+ }
+
+ @Test
+ public void testGettingConfiguration() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+ assertEquals(PropertiesConfiguration.class, cc.getConfiguration(0).getClass());
+ assertEquals(XMLConfiguration.class, cc.getConfiguration(1).getClass());
+ }
+
+ /**
+ * Tests setting values. These are set in memory mode only!
+ */
+ @Test
+ public void testClearingProperty() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+ cc.clearProperty("test.short");
+ assertTrue("Make sure test.short is gone!", !cc.containsKey("test.short"));
+ }
+
+ /**
+ * Tests adding values. Make sure they _DON'T_ override any other properties but add to the
+ * existing properties and keep sequence
+ */
+ @Test
+ public void testAddingProperty() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+
+ String[] values = cc.getStringArray("test.short");
+
+ assertEquals("Number of values before add is wrong!", 1, values.length);
+ assertEquals("First Value before add is wrong", "1", values[0]);
+
+ cc.addProperty("test.short", "88");
+
+ values = cc.getStringArray("test.short");
+
+ assertEquals("Number of values is wrong!", 2, values.length);
+ assertEquals("First Value is wrong", "1", values[0]);
+ assertEquals("Third Value is wrong", "88", values[1]);
+ }
+
+ /**
+ * Tests setting values. These are set in memory mode only!
+ */
+ @Test
+ public void testSettingMissingProperty() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+ cc.setProperty("my.new.property", "supernew");
+ assertEquals("supernew", cc.getString("my.new.property"));
+ }
+
+ /**
+ * Tests retrieving subsets of configurations
+ */
+ @Test
+ public void testGettingSubset() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+
+ Configuration subset = null;
+ subset = cc.subset("test");
+ assertNotNull(subset);
+ assertFalse("Shouldn't be empty", subset.isEmpty());
+ assertEquals("Make sure the initial loaded configs subset overrides any later add configs subset", "1", subset.getString("short"));
+
+ cc.setProperty("test.short", "43");
+ subset = cc.subset("test");
+ assertEquals("Make sure the initial loaded configs subset overrides any later add configs subset", "43", subset.getString("short"));
+ }
+
+ /**
+ * Tests subsets and still can resolve elements
+ */
+ @Test
+ public void testSubsetCanResolve() throws Exception
+ {
+ cc = new CompositeConfiguration();
+ final BaseConfiguration config = new BaseConfiguration();
+ config.addProperty("subset.tempfile", "${java.io.tmpdir}/file.tmp");
+ cc.addConfiguration(config);
+ cc.addConfiguration(ConfigurationConverter.getConfiguration(System.getProperties()));
+
+ Configuration subset = cc.subset("subset");
+ assertEquals(System.getProperty("java.io.tmpdir") + "/file.tmp", subset.getString("tempfile"));
+ }
+
+ /**
+ * Tests {@code List} parsing.
+ */
+ @Test
+ public void testList() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+
+ List<Object> packages = cc.getList("packages");
+ // we should get 3 packages here
+ assertEquals(3, packages.size());
+
+ List<Object> defaultList = new ArrayList<Object>();
+ defaultList.add("1");
+ defaultList.add("2");
+
+ packages = cc.getList("packages.which.dont.exist", defaultList);
+ // we should get 2 packages here
+ assertEquals(2, packages.size());
+ }
+
+ /**
+ * Tests {@code String} array parsing.
+ */
+ @Test
+ public void testStringArray() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(xmlConf);
+
+ String[] packages = cc.getStringArray("packages");
+ // we should get 3 packages here
+ assertEquals(3, packages.length);
+
+ packages = cc.getStringArray("packages.which.dont.exist");
+ // we should get 0 packages here
+ assertEquals(0, packages.length);
+ }
+
+ @Test
+ public void testGetList()
+ {
+ Configuration conf1 = new BaseConfiguration();
+ conf1.addProperty("array", "value1");
+ conf1.addProperty("array", "value2");
+
+ Configuration conf2 = new BaseConfiguration();
+ conf2.addProperty("array", "value3");
+ conf2.addProperty("array", "value4");
+
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(conf2);
+
+ // check the composite 'array' property
+ List<Object> list = cc.getList("array");
+ assertNotNull("null list", list);
+ assertEquals("list size", 2, list.size());
+ assertTrue("'value1' not found in the list", list.contains("value1"));
+ assertTrue("'value2' not found in the list", list.contains("value2"));
+
+ // add an element to the list in the composite configuration
+ cc.addProperty("array", "value5");
+
+ // test the new list
+ list = cc.getList("array");
+ assertNotNull("null list", list);
+ assertEquals("list size", 3, list.size());
+ assertTrue("'value1' not found in the list", list.contains("value1"));
+ assertTrue("'value2' not found in the list", list.contains("value2"));
+ assertTrue("'value5' not found in the list", list.contains("value5"));
+ }
+
+ @Test
+ public void testGetVector()
+ {
+ Configuration conf1 = new BaseConfiguration();
+ conf1.addProperty("array", "value1");
+ conf1.addProperty("array", "value2");
+
+ Configuration conf2 = new BaseConfiguration();
+ conf2.addProperty("array", "value3");
+ conf2.addProperty("array", "value4");
+
+ cc.addConfiguration(conf1);
+ cc.addConfiguration(conf2);
+
+ // add an element to the vector in the composite configuration
+ cc.addProperty("array", "value5");
+
+ List<Object> list = cc.getList("array");
+ assertEquals("Wrong number of elements", 3, list.size());
+ assertEquals("Wrong element 1", "value1", list.get(0));
+ assertEquals("Wrong element 2", "value2", list.get(1));
+ assertEquals("Wrong element 3", "value5", list.get(2));
+ }
+
+ /**
+ * Tests {@code getKeys()} preserves the order
+ */
+ @Test
+ public void testGetKeysPreservesOrder() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ List<String> orderedList = new ArrayList<String>();
+ for (Iterator<String> keys = conf1.getKeys(); keys.hasNext();)
+ {
+ orderedList.add(keys.next());
+ }
+ List<String> iteratedList = new ArrayList<String>();
+ for (Iterator<String> keys = cc.getKeys(); keys.hasNext();)
+ {
+ iteratedList.add(keys.next());
+ }
+ assertEquals(orderedList.size(), iteratedList.size());
+ for (int i = 0; i < orderedList.size(); i++)
+ {
+ assertEquals(orderedList.get(i), iteratedList.get(i));
+ }
+ }
+
+ /**
+ * Tests {@code getKeys(String key)} preserves the order
+ */
+ @Test
+ public void testGetKeys2PreservesOrder() throws Exception
+ {
+ cc.addConfiguration(conf1);
+ List<String> orderedList = new ArrayList<String>();
+ for (Iterator<String> keys = conf1.getKeys("test"); keys.hasNext();)
+ {
+ orderedList.add(keys.next());
+ }
+ List<String> iteratedList = new ArrayList<String>();
+ for (Iterator<String> keys = cc.getKeys("test"); keys.hasNext();)
+ {
+ iteratedList.add(keys.next());
+ }
+ assertEquals(orderedList.size(), iteratedList.size());
+ for (int i = 0; i < orderedList.size(); i++)
+ {
+ assertEquals(orderedList.get(i), iteratedList.get(i));
+ }
+ }
+
+ @Test
+ public void testGetStringWithDefaults()
+ {
+ BaseConfiguration defaults = new BaseConfiguration();
+ defaults.addProperty("default", "default string");
+
+ Configuration c = new CompositeConfiguration(defaults);
+
+ c.addProperty("string", "test string");
+
+ assertEquals("test string", c.getString("string"));
+
+ assertNull("XXX should have been null!", c.getString("XXX"));
+
+ //test defaults
+ assertEquals(
+ "test string",
+ c.getString("string", "some default value"));
+ assertEquals("default string", c.getString("default"));
+ assertEquals(
+ "default string",
+ c.getString("default", "some default value"));
+ assertEquals(
+ "some default value",
+ c.getString("XXX", "some default value"));
+ }
+
+ @Test
+ public void testCheckingInMemoryConfiguration() throws Exception
+ {
+ String TEST_KEY = "testKey";
+ Configuration defaults = new PropertiesConfiguration();
+ defaults.setProperty(TEST_KEY, "testValue");
+ Configuration testConfiguration = new CompositeConfiguration(defaults);
+ assertTrue(testConfiguration.containsKey(TEST_KEY));
+ assertFalse(testConfiguration.isEmpty());
+ boolean foundTestKey = false;
+ Iterator<String> i = testConfiguration.getKeys();
+ for (; i.hasNext();)
+ {
+ String key = i.next();
+ if (key.equals(TEST_KEY))
+ {
+ foundTestKey = true;
+ }
+ }
+ assertTrue(foundTestKey);
+ testConfiguration.clearProperty(TEST_KEY);
+ assertFalse(testConfiguration.containsKey(TEST_KEY));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestNullJNDIEnvironmentValues.java b/src/test/java/org/apache/commons/configuration/TestNullJNDIEnvironmentValues.java
new file mode 100644
index 0000000..5456897
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestNullJNDIEnvironmentValues.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestNullJNDIEnvironmentValues
+{
+ private JNDIConfiguration conf = null;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ System.setProperty("java.naming.factory.initial", TestJNDIConfiguration.CONTEXT_FACTORY);
+
+ conf = new JNDIConfiguration();
+ conf.setThrowExceptionOnMissing(false);
+ }
+
+ @Test
+ public void testThrowExceptionOnMissing()
+ {
+ assertFalse("Throw Exception Property is set!", conf.isThrowExceptionOnMissing());
+ }
+
+ @Test
+ public void testSimpleGet() throws Exception
+ {
+ String s = conf.getString("test.key");
+ assertEquals("jndivalue", s);
+ }
+
+ @Test
+ public void testMoreGets() throws Exception
+ {
+ String s = conf.getString("test.key");
+ assertEquals("jndivalue", s);
+ assertEquals("jndivalue2", conf.getString("test.key2"));
+ assertEquals(1, conf.getShort("test.short"));
+ }
+
+ @Test
+ public void testGetMissingKey() throws Exception
+ {
+ assertNull("Missing Key is not null!", conf.getString("test.imaginarykey"));
+ }
+
+ @Test
+ public void testGetMissingKeyWithDefault() throws Exception
+ {
+ String result = conf.getString("test.imaginarykey", "bob");
+ assertEquals("bob", result);
+ }
+
+ @Test
+ public void testContainsKey() throws Exception
+ {
+ assertTrue(conf.containsKey("test.key"));
+ assertTrue(!conf.containsKey("test.imaginarykey"));
+ }
+
+ @Test
+ public void testClearProperty()
+ {
+ assertNotNull("null short for the 'test.short' key", conf.getShort("test.short", null));
+ conf.clearProperty("test.short");
+ assertNull("'test.short' property not cleared", conf.getShort("test.short", null));
+ }
+
+ @Test
+ public void testIsEmpty()
+ {
+ assertFalse("the configuration shouldn't be empty", conf.isEmpty());
+ }
+
+ @Test
+ public void testGetKeys() throws Exception
+ {
+ boolean found = false;
+ Iterator<String> it = conf.getKeys();
+
+ assertTrue("no key found", it.hasNext());
+
+ while (it.hasNext() && !found)
+ {
+ found = "test.boolean".equals(it.next());
+ }
+
+ assertTrue("'test.boolean' key not found", found);
+ }
+
+ @Test
+ public void testGetKeysWithUnknownPrefix()
+ {
+ // test for a unknown prefix
+ Iterator<String> it = conf.getKeys("foo.bar");
+ assertFalse("no key should be found", it.hasNext());
+ }
+
+ @Test
+ public void testGetKeysWithExistingPrefix()
+ {
+ // test for an existing prefix
+ Iterator<String> it = conf.getKeys("test");
+ boolean found = false;
+ while (it.hasNext() && !found)
+ {
+ found = "test.boolean".equals(it.next());
+ }
+
+ assertTrue("'test.boolean' key not found", found);
+ }
+
+ @Test
+ public void testGetKeysWithKeyAsPrefix()
+ {
+ // test for a prefix matching exactly the key of a property
+ Iterator<String> it = conf.getKeys("test.boolean");
+ boolean found = false;
+ while (it.hasNext() && !found)
+ {
+ found = "test.boolean".equals(it.next());
+ }
+
+ assertTrue("'test.boolean' key not found", found);
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestPatternSubtreeConfiguration.java b/src/test/java/org/apache/commons/configuration/TestPatternSubtreeConfiguration.java
new file mode 100644
index 0000000..5c6c477
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestPatternSubtreeConfiguration.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+
+import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;
+import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit test for simple MultiConfigurationTest.
+ *
+ * @version $Id: TestPatternSubtreeConfiguration.java 1224818 2011-12-26 21:20:17Z oheger $
+ */
+public class TestPatternSubtreeConfiguration
+{
+ private static String CONFIG_FILE = "target/test-classes/testPatternSubtreeConfig.xml";
+ private static String PATTERN = "BusinessClient[@name='${sys:Id}']";
+ private XMLConfiguration conf;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ conf = new XMLConfiguration();
+ conf.setFile(new File(CONFIG_FILE));
+ conf.load();
+ }
+
+ /**
+ * Rigourous Test :-)
+ */
+ @Test
+ public void testMultiConfiguration()
+ {
+ //set up a reloading strategy
+ FileChangedReloadingStrategy strategy = new FileChangedReloadingStrategy();
+ strategy.setRefreshDelay(10000);
+
+ PatternSubtreeConfigurationWrapper config = new PatternSubtreeConfigurationWrapper(this.conf, PATTERN);
+ config.setReloadingStrategy(strategy);
+ config.setExpressionEngine(new XPathExpressionEngine());
+
+ System.setProperty("Id", "1001");
+ assertTrue(config.getInt("rowsPerPage") == 15);
+
+ System.setProperty("Id", "1002");
+ assertTrue(config.getInt("rowsPerPage") == 25);
+
+ System.setProperty("Id", "1003");
+ assertTrue(config.getInt("rowsPerPage") == 35);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/commons/configuration/TestPropertiesConfiguration.java b/src/test/java/org/apache/commons/configuration/TestPropertiesConfiguration.java
new file mode 100644
index 0000000..c60e8b9
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestPropertiesConfiguration.java
@@ -0,0 +1,1359 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;
+import org.apache.commons.lang.SystemUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test for loading and saving properties files.
+ *
+ * @version $Id: TestPropertiesConfiguration.java 1534402 2013-10-21 22:35:52Z henning $
+ */
+public class TestPropertiesConfiguration
+{
+ /** Constant for a test property name.*/
+ private static final String PROP_NAME = "testProperty";
+
+ /** Constant for a test property value.*/
+ private static final String PROP_VALUE = "value";
+
+ /** Constant for the line break character. */
+ private static final String CR = System.getProperty("line.separator");
+
+ /** The configuration to be tested.*/
+ private PropertiesConfiguration conf;
+
+ /** The File that we test with */
+ private static String testProperties = ConfigurationAssert.getTestFile("test.properties").getAbsolutePath();
+
+ private static String testBasePath = ConfigurationAssert.TEST_DIR.getAbsolutePath();
+ private static String testBasePath2 = ConfigurationAssert.TEST_DIR.getParentFile().getAbsolutePath();
+ private static File testSavePropertiesFile = ConfigurationAssert.getOutFile("testsave.properties");
+
+ @Before
+ public void setUp() throws Exception
+ {
+ conf = new PropertiesConfiguration(testProperties);
+
+ // remove the test save file if it exists
+ if (testSavePropertiesFile.exists())
+ {
+ assertTrue("Test output file could not be deleted",
+ testSavePropertiesFile.delete());
+ }
+ }
+
+ @Test
+ public void testLoad() throws Exception
+ {
+ String loaded = conf.getString("configuration.loaded");
+ assertEquals("true", loaded);
+ }
+
+ /**
+ * Tests if properties can be appended by simply calling load() another
+ * time.
+ */
+ @Test
+ public void testAppend() throws Exception
+ {
+ File file2 = ConfigurationAssert.getTestFile("threesome.properties");
+ conf.load(file2);
+ assertEquals("aaa", conf.getString("test.threesome.one"));
+ assertEquals("true", conf.getString("configuration.loaded"));
+ }
+
+ /**
+ * Tests that empty properties are treated as the empty string
+ * (rather than as null).
+ */
+ @Test
+ public void testEmpty() throws Exception
+ {
+ String empty = conf.getString("test.empty");
+ assertNotNull(empty);
+ assertEquals("", empty);
+ }
+
+ /**
+ * Tests that references to other properties work
+ */
+ @Test
+ public void testReference() throws Exception
+ {
+ assertEquals("baseextra", conf.getString("base.reference"));
+ }
+
+ /**
+ * test if includes properties get loaded too
+ */
+ @Test
+ public void testLoadInclude() throws Exception
+ {
+ String loaded = conf.getString("include.loaded");
+ assertEquals("true", loaded);
+ }
+
+ /**
+ * test if includes properties from interpolated file
+ * name get loaded
+ */
+ @Test
+ public void testLoadIncludeInterpol() throws Exception
+ {
+ String loaded = conf.getString("include.interpol.loaded");
+ assertEquals("true", loaded);
+ }
+
+ /**
+ * Tests whether include files can be resolved if a configuration file is
+ * read from a reader.
+ */
+ @Test
+ public void testLoadIncludeFromReader() throws ConfigurationException,
+ IOException
+ {
+ StringReader in =
+ new StringReader(PropertiesConfiguration.getInclude() + " = "
+ + ConfigurationAssert.getTestURL("include.properties"));
+ conf = new PropertiesConfiguration();
+ conf.load(in);
+ assertEquals("Include file not loaded", "true",
+ conf.getString("include.loaded"));
+ }
+
+ /**
+ * Tests whether include files can be disabled.
+ */
+ @Test
+ public void testDisableIncludes() throws ConfigurationException,
+ IOException
+ {
+ String content =
+ PropertiesConfiguration.getInclude()
+ + " = nonExistingIncludeFile" + CR + PROP_NAME + " = "
+ + PROP_VALUE + CR;
+ StringReader in = new StringReader(content);
+ conf = new PropertiesConfiguration();
+ conf.setIncludesAllowed(false);
+ conf.load(in);
+ assertEquals("Data not loaded", PROP_VALUE, conf.getString(PROP_NAME));
+ }
+
+ @Test
+ public void testSetInclude() throws Exception
+ {
+ // change the include key
+ PropertiesConfiguration.setInclude("import");
+
+ // load the configuration
+ PropertiesConfiguration conf = new PropertiesConfiguration();
+ conf.load(ConfigurationAssert.getTestFile("test.properties"));
+
+ // restore the previous value for the other tests
+ PropertiesConfiguration.setInclude("include");
+
+ assertNull(conf.getString("include.loaded"));
+ }
+
+ /**
+ * Tests {@code List} parsing.
+ */
+ @Test
+ public void testList() throws Exception
+ {
+ List<Object> packages = conf.getList("packages");
+ // we should get 3 packages here
+ assertEquals(3, packages.size());
+ }
+
+ @Test
+ public void testSave() throws Exception
+ {
+ // add an array of strings to the configuration
+ conf.addProperty("string", "value1");
+ List<Object> list = new ArrayList<Object>();
+ for (int i = 1; i < 5; i++)
+ {
+ list.add("value" + i);
+ }
+ conf.addProperty("array", list);
+
+ // save the configuration
+ String filename = testSavePropertiesFile.getAbsolutePath();
+ conf.save(filename);
+
+ assertTrue("The saved file doesn't exist", testSavePropertiesFile.exists());
+
+ // read the configuration and compare the properties
+ PropertiesConfiguration checkConfig = new PropertiesConfiguration(filename);
+ ConfigurationAssert.assertEquals(conf, checkConfig);
+
+ // Save it again, verifying a save with a filename works.
+ checkConfig.save();
+ }
+
+ @Test
+ public void testSaveToCustomURL() throws Exception
+ {
+ // save the configuration to a custom URL
+ URL url = new URL("foo", "", 0, "./target/testsave-custom-url.properties", new FileURLStreamHandler());
+ conf.save(url);
+
+ // reload the configuration
+ Configuration config2 = new PropertiesConfiguration(url);
+ assertEquals("true", config2.getString("configuration.loaded"));
+ }
+
+ @Test
+ public void testInMemoryCreatedSave() throws Exception
+ {
+ PropertiesConfiguration pc = new PropertiesConfiguration();
+ // add an array of strings to the configuration
+ pc.addProperty("string", "value1");
+ List<Object> list = new ArrayList<Object>();
+ for (int i = 1; i < 5; i++)
+ {
+ list.add("value" + i);
+ }
+ pc.addProperty("array", list);
+
+ // save the configuration
+ String filename = testSavePropertiesFile.getAbsolutePath();
+ pc.save(filename);
+
+ assertTrue("The saved file doesn't exist", testSavePropertiesFile.exists());
+
+ // read the configuration and compare the properties
+ PropertiesConfiguration checkConfig = new PropertiesConfiguration(filename);
+ ConfigurationAssert.assertEquals(pc, checkConfig);
+
+ // Save it again, verifying a save with a filename works.
+ checkConfig.save();
+ }
+
+ /**
+ * Tests saving a configuration when delimiter parsing is disabled.
+ */
+ @Test
+ public void testSaveWithDelimiterParsingDisabled() throws ConfigurationException
+ {
+ conf.clear();
+ conf.setDelimiterParsingDisabled(true);
+ conf.addProperty("test.list", "a,b,c");
+ conf.addProperty("test.dirs", "C:\\Temp\\,D:\\Data\\");
+ conf.save(testSavePropertiesFile);
+
+ PropertiesConfiguration checkConfig = new PropertiesConfiguration();
+ checkConfig.setDelimiterParsingDisabled(true);
+ checkConfig.setFile(testSavePropertiesFile);
+ checkConfig.load();
+ ConfigurationAssert.assertEquals(conf, checkConfig);
+ }
+
+ @Test(expected = ConfigurationException.class)
+ public void testSaveMissingFilename() throws ConfigurationException
+ {
+ PropertiesConfiguration pc = new PropertiesConfiguration();
+ pc.save();
+ }
+
+ /**
+ * Tests if the base path is taken into account by the save() method.
+ * @throws Exception if an error occurs
+ */
+ @Test
+ public void testSaveWithBasePath() throws Exception
+ {
+ conf.setProperty("test", "true");
+ conf.setBasePath(testSavePropertiesFile.getParentFile().toURI().toURL()
+ .toString());
+ conf.setFileName(testSavePropertiesFile.getName());
+ conf.save();
+ assertTrue(testSavePropertiesFile.exists());
+ }
+
+ /**
+ * Tests whether the escape character for list delimiters can be itself
+ * escaped and survives a save operation.
+ */
+ @Test
+ public void testSaveEscapedEscapingCharacter()
+ throws ConfigurationException
+ {
+ conf.addProperty("test.dirs", "C:\\Temp\\\\,D:\\Data\\\\,E:\\Test\\");
+ List<Object> dirs = conf.getList("test.dirs");
+ assertEquals("Wrong number of list elements", 3, dirs.size());
+ conf.save(testSavePropertiesFile);
+
+ PropertiesConfiguration checkConfig = new PropertiesConfiguration(
+ testSavePropertiesFile);
+ ConfigurationAssert.assertEquals(conf, checkConfig);
+ }
+
+ @Test
+ public void testLoadViaProperty() throws Exception
+ {
+ PropertiesConfiguration pc = new PropertiesConfiguration();
+ pc.setFileName(testProperties);
+ pc.load();
+
+ assertTrue("Make sure we have multiple keys", pc.getBoolean("test.boolean"));
+ }
+
+ @Test
+ public void testLoadViaPropertyWithBasePath() throws Exception
+ {
+ PropertiesConfiguration pc = new PropertiesConfiguration();
+ pc.setBasePath(testBasePath);
+ pc.setFileName("test.properties");
+ pc.load();
+
+ assertTrue("Make sure we have multiple keys", pc.getBoolean("test.boolean"));
+ }
+
+ @Test
+ public void testLoadViaPropertyWithBasePath2() throws Exception
+ {
+ PropertiesConfiguration pc = new PropertiesConfiguration();
+ pc.setBasePath(testBasePath2);
+ pc.setFileName("test.properties");
+ pc.load();
+
+ assertTrue("Make sure we have multiple keys", pc.getBoolean("test.boolean"));
+
+ pc = new PropertiesConfiguration();
+ pc.setBasePath(testBasePath2);
+ pc.setFileName("test.properties");
+ pc.load();
+
+ assertTrue("Make sure we have multiple keys", pc.getBoolean("test.boolean"));
+ }
+
+ @Test
+ public void testLoadFromFile() throws Exception
+ {
+ File file = ConfigurationAssert.getTestFile("test.properties");
+ conf = new PropertiesConfiguration(file);
+
+ assertEquals("true", conf.getString("configuration.loaded"));
+ }
+
+ @Test(expected = ConfigurationException.class)
+ public void testLoadUnexistingFile() throws ConfigurationException
+ {
+ conf = new PropertiesConfiguration("Unexisting file");
+ }
+
+ /**
+ * Tests to load a file with enabled auto save mode.
+ */
+ @Test
+ public void testLoadWithAutoSave() throws Exception
+ {
+ setUpSavedProperties();
+ }
+
+ /**
+ * Tests the auto save functionality when an existing property is modified.
+ */
+ @Test
+ public void testLoadWithAutoSaveAndSetExisting() throws Exception
+ {
+ setUpSavedProperties();
+ conf.setProperty("a", "moreThanOne");
+ checkSavedConfig();
+ }
+
+ /**
+ * Tests the auto save functionality when a new property is added using the
+ * setProperty() method.
+ */
+ @Test
+ public void testLoadWithAutoSaveAndSetNew() throws Exception
+ {
+ setUpSavedProperties();
+ conf.setProperty("d", "four");
+ checkSavedConfig();
+ }
+
+ /**
+ * Tests the auto save functionality when a new property is added using the
+ * addProperty() method.
+ */
+ @Test
+ public void testLoadWithAutoSaveAndAdd() throws Exception
+ {
+ setUpSavedProperties();
+ conf.addProperty("d", "four");
+ checkSavedConfig();
+ }
+
+ /**
+ * Tests the auto save functionality when a property is removed.
+ */
+ @Test
+ public void testLoadWithAutoSaveAndClear() throws Exception
+ {
+ setUpSavedProperties();
+ conf.clearProperty("c");
+ PropertiesConfiguration checkConfig = checkSavedConfig();
+ assertFalse("The saved configuration contain the key '" + "c" + "'", checkConfig.containsKey("c"));
+ }
+
+ /**
+ * Creates a properties file on disk. Used for testing load and save
+ * operations.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ private void setUpSavedProperties() throws IOException, ConfigurationException
+ {
+ PrintWriter out = null;
+
+ try
+ {
+ out = new PrintWriter(new FileWriter(testSavePropertiesFile));
+ out.println("a = one");
+ out.println("b = two");
+ out.println("c = three");
+ out.close();
+ out = null;
+
+ conf = new PropertiesConfiguration();
+ conf.setAutoSave(true);
+ conf.setFile(testSavePropertiesFile);
+ conf.load();
+ assertEquals("one", conf.getString("a"));
+ assertEquals("two", conf.getString("b"));
+ assertEquals("three", conf.getString("c"));
+ }
+ finally
+ {
+ if (out != null)
+ {
+ out.close();
+ }
+ }
+ }
+
+ /**
+ * Helper method for testing a saved configuration. Reads in the file using
+ * a new instance and compares this instance with the original one.
+ *
+ * @return the newly created configuration instance
+ * @throws ConfigurationException if an error occurs
+ */
+ private PropertiesConfiguration checkSavedConfig()
+ throws ConfigurationException
+ {
+ PropertiesConfiguration checkConfig = new PropertiesConfiguration(testSavePropertiesFile);
+ ConfigurationAssert.assertEquals(conf, checkConfig);
+ return checkConfig;
+ }
+
+ @Test
+ public void testGetStringWithEscapedChars()
+ {
+ String property = conf.getString("test.unescape");
+ assertEquals("String with escaped characters", "This \n string \t contains \" escaped \\ characters", property);
+ }
+
+ @Test
+ public void testGetStringWithEscapedComma()
+ {
+ String property = conf.getString("test.unescape.list-separator");
+ assertEquals("String with an escaped list separator", "This string contains , an escaped list separator", property);
+ }
+
+ @Test
+ public void testUnescapeJava()
+ {
+ assertEquals("test\\,test", PropertiesConfiguration.unescapeJava("test\\,test", ','));
+ }
+
+ @Test
+ public void testEscapedKey() throws Exception
+ {
+ PropertiesConfiguration conf = new PropertiesConfiguration();
+ conf.load(new StringReader("\\u0066\\u006f\\u006f=bar"));
+
+ assertEquals("value of the 'foo' property", "bar", conf.getString("foo"));
+ }
+
+ @Test
+ public void testMixedArray()
+ {
+ String[] array = conf.getStringArray("test.mixed.array");
+
+ assertEquals("array length", 4, array.length);
+ assertEquals("1st element", "a", array[0]);
+ assertEquals("2nd element", "b", array[1]);
+ assertEquals("3rd element", "c", array[2]);
+ assertEquals("4th element", "d", array[3]);
+ }
+
+ @Test
+ public void testMultilines()
+ {
+ String property = "This is a value spread out across several adjacent "
+ + "natural lines by escaping the line terminator with "
+ + "a backslash character.";
+
+ assertEquals("'test.multilines' property", property, conf.getString("test.multilines"));
+ }
+
+ @Test
+ public void testChangingDefaultListDelimiter() throws Exception
+ {
+ PropertiesConfiguration pc = new PropertiesConfiguration(testProperties);
+ assertEquals(4, pc.getList("test.mixed.array").size());
+
+ char delimiter = PropertiesConfiguration.getDefaultListDelimiter();
+ PropertiesConfiguration.setDefaultListDelimiter('^');
+ pc = new PropertiesConfiguration(testProperties);
+ assertEquals(2, pc.getList("test.mixed.array").size());
+ PropertiesConfiguration.setDefaultListDelimiter(delimiter);
+ }
+
+ @Test
+ public void testChangingListDelimiter() throws Exception
+ {
+ PropertiesConfiguration pc1 = new PropertiesConfiguration(testProperties);
+ assertEquals(4, pc1.getList("test.mixed.array").size());
+
+ PropertiesConfiguration pc2 = new PropertiesConfiguration();
+ pc2.setListDelimiter('^');
+ pc2.setFileName(testProperties);
+ pc2.load();
+ assertEquals("Should obtain the first value", "a", pc2.getString("test.mixed.array"));
+ assertEquals(2, pc2.getList("test.mixed.array").size());
+ }
+
+ @Test
+ public void testDisableListDelimiter() throws Exception
+ {
+ PropertiesConfiguration pc1 = new PropertiesConfiguration(testProperties);
+ assertEquals(4, pc1.getList("test.mixed.array").size());
+
+ PropertiesConfiguration pc2 = new PropertiesConfiguration();
+ pc2.setDelimiterParsingDisabled(true);
+ pc2.setFileName(testProperties);
+ pc2.load();
+ assertEquals(2, pc2.getList("test.mixed.array").size());
+ }
+
+ /**
+ * Tests escaping of an end of line with a backslash.
+ */
+ @Test
+ public void testNewLineEscaping()
+ {
+ List<Object> list = conf.getList("test.path");
+ assertEquals(3, list.size());
+ assertEquals("C:\\path1\\", list.get(0));
+ assertEquals("C:\\path2\\", list.get(1));
+ assertEquals("C:\\path3\\complex\\test\\", list.get(2));
+ }
+
+ /**
+ * Tests if included files are loaded when the source lies in the class path.
+ */
+ @Test
+ public void testLoadIncludeFromClassPath() throws ConfigurationException
+ {
+ conf = new PropertiesConfiguration("test.properties");
+ assertEquals("true", conf.getString("include.loaded"));
+ }
+
+ /**
+ * Test if the lines starting with # or ! are properly ignored.
+ */
+ @Test
+ public void testComment() {
+ assertFalse("comment line starting with '#' parsed as a property", conf.containsKey("#comment"));
+ assertFalse("comment line starting with '!' parsed as a property", conf.containsKey("!comment"));
+ }
+
+ /**
+ * Check that key/value separators can be part of a key.
+ */
+ @Test
+ public void testEscapedKeyValueSeparator()
+ {
+ assertEquals("Escaped separator '=' not supported in keys", "foo", conf.getProperty("test.separator=in.key"));
+ assertEquals("Escaped separator ':' not supported in keys", "bar", conf.getProperty("test.separator:in.key"));
+ assertEquals("Escaped separator '\\t' not supported in keys", "foo", conf.getProperty("test.separator\tin.key"));
+ assertEquals("Escaped separator '\\f' not supported in keys", "bar", conf.getProperty("test.separator\fin.key"));
+ assertEquals("Escaped separator ' ' not supported in keys" , "foo", conf.getProperty("test.separator in.key"));
+ }
+
+ /**
+ * Test all acceptable key/value separators ('=', ':' or white spaces).
+ */
+ @Test
+ public void testKeyValueSeparators() {
+ assertEquals("equal separator not properly parsed", "foo", conf.getProperty("test.separator.equal"));
+ assertEquals("colon separator not properly parsed", "foo", conf.getProperty("test.separator.colon"));
+ assertEquals("tab separator not properly parsed", "foo", conf.getProperty("test.separator.tab"));
+ assertEquals("formfeed separator not properly parsed", "foo", conf.getProperty("test.separator.formfeed"));
+ assertEquals("whitespace separator not properly parsed", "foo", conf.getProperty("test.separator.whitespace"));
+ }
+
+ /**
+ * Tests including properties when they are loaded from a nested directory
+ * structure.
+ */
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testIncludeInSubDir() throws ConfigurationException
+ {
+ ConfigurationFactory factory = new ConfigurationFactory("conf/testFactoryPropertiesInclude.xml");
+ Configuration config = factory.getConfiguration();
+ assertTrue(config.getBoolean("deeptest"));
+ assertTrue(config.getBoolean("deepinclude"));
+ assertFalse(config.containsKey("deeptestinvalid"));
+ }
+
+ /**
+ * Tests whether the correct line separator is used.
+ */
+ @Test
+ public void testLineSeparator() throws ConfigurationException
+ {
+ final String EOL = System.getProperty("line.separator");
+ conf = new PropertiesConfiguration();
+ conf.setHeader("My header");
+ conf.setProperty("prop", "value");
+
+ StringWriter out = new StringWriter();
+ conf.save(out);
+ String content = out.toString();
+ assertTrue("Header could not be found", content.indexOf("# My header"
+ + EOL + EOL) == 0);
+ assertTrue("Property could not be found", content.indexOf("prop = value" + EOL) > 0);
+ }
+
+ /**
+ * Tests what happens if a reloading strategy's <code>reloadingRequired()</code>
+ * implementation accesses methods of the configuration that in turn cause a reload.
+ */
+ @Test
+ public void testReentrantReload()
+ {
+ conf.setProperty("shouldReload", Boolean.FALSE);
+ conf.setReloadingStrategy(new FileChangedReloadingStrategy()
+ {
+ @Override
+ public boolean reloadingRequired()
+ {
+ return configuration.getBoolean("shouldReload");
+ }
+ });
+ assertFalse("Property has wrong value", conf.getBoolean("shouldReload"));
+ }
+
+ /**
+ * Tests accessing the layout object.
+ */
+ @Test
+ public void testGetLayout()
+ {
+ PropertiesConfigurationLayout layout = conf.getLayout();
+ assertNotNull("Layout is null", layout);
+ assertSame("Different object returned", layout, conf.getLayout());
+ conf.setLayout(null);
+ PropertiesConfigurationLayout layout2 = conf.getLayout();
+ assertNotNull("Layout 2 is null", layout2);
+ assertNotSame("Same object returned", layout, layout2);
+ }
+
+ /**
+ * Tests the propertyLoaded() method for a simple property.
+ */
+ @Test
+ public void testPropertyLoaded() throws ConfigurationException
+ {
+ DummyLayout layout = new DummyLayout(conf);
+ conf.setLayout(layout);
+ conf.propertyLoaded("layoutLoadedProperty", "yes");
+ assertEquals("Layout's load() was called", 0, layout.loadCalls);
+ assertEquals("Property not added", "yes", conf.getString("layoutLoadedProperty"));
+ }
+
+ /**
+ * Tests the propertyLoaded() method for an include property.
+ */
+ @Test
+ public void testPropertyLoadedInclude() throws ConfigurationException
+ {
+ DummyLayout layout = new DummyLayout(conf);
+ conf.setLayout(layout);
+ conf.propertyLoaded(PropertiesConfiguration.getInclude(), "testClasspath.properties,testEqual.properties");
+ assertEquals("Layout's load() was not correctly called", 2, layout.loadCalls);
+ assertFalse("Property was added", conf.containsKey(PropertiesConfiguration.getInclude()));
+ }
+
+ /**
+ * Tests propertyLoaded() for an include property, when includes are
+ * disabled.
+ */
+ @Test
+ public void testPropertyLoadedIncludeNotAllowed() throws ConfigurationException
+ {
+ DummyLayout layout = new DummyLayout(conf);
+ conf.setLayout(layout);
+ conf.setIncludesAllowed(false);
+ conf.propertyLoaded(PropertiesConfiguration.getInclude(), "testClassPath.properties,testEqual.properties");
+ assertEquals("Layout's load() was called", 0, layout.loadCalls);
+ assertFalse("Property was added", conf.containsKey(PropertiesConfiguration.getInclude()));
+ }
+
+ /**
+ * Tests whether comment lines are correctly detected.
+ */
+ @Test
+ public void testIsCommentLine()
+ {
+ assertTrue("Comment not detected", PropertiesConfiguration.isCommentLine("# a comment"));
+ assertTrue("Alternative comment not detected", PropertiesConfiguration.isCommentLine("! a comment"));
+ assertTrue("Comment with no space not detected", PropertiesConfiguration.isCommentLine("#a comment"));
+ assertTrue("Comment with leading space not detected", PropertiesConfiguration.isCommentLine(" ! a comment"));
+ assertFalse("Wrong comment", PropertiesConfiguration.isCommentLine(" a#comment"));
+ }
+
+ /**
+ * Tests whether a properties configuration can be successfully cloned. It
+ * is especially checked whether the layout object is taken into account.
+ */
+ @Test
+ public void testClone() throws ConfigurationException
+ {
+ PropertiesConfiguration copy = (PropertiesConfiguration) conf.clone();
+ assertNotSame("Copy has same layout object", conf.getLayout(), copy.getLayout());
+ assertEquals("Wrong number of event listeners for original", 1, conf.getConfigurationListeners().size());
+ assertEquals("Wrong number of event listeners for clone", 1, copy.getConfigurationListeners().size());
+ assertSame("Wrong event listener for original", conf.getLayout(), conf.getConfigurationListeners().iterator().next());
+ assertSame("Wrong event listener for clone", copy.getLayout(), copy.getConfigurationListeners().iterator().next());
+ StringWriter outConf = new StringWriter();
+ conf.save(outConf);
+ StringWriter outCopy = new StringWriter();
+ copy.save(outCopy);
+ assertEquals("Output from copy is different", outConf.toString(), outCopy.toString());
+ }
+
+ /**
+ * Tests the clone() method when no layout object exists yet.
+ */
+ @Test
+ public void testCloneNullLayout()
+ {
+ conf = new PropertiesConfiguration();
+ PropertiesConfiguration copy = (PropertiesConfiguration) conf.clone();
+ assertNotSame("Layout objects are the same", conf.getLayout(), copy.getLayout());
+ }
+
+ /**
+ * Tests saving a file-based configuration to a HTTP server.
+ */
+ @Test
+ public void testSaveToHTTPServerSuccess() throws Exception
+ {
+ MockHttpURLStreamHandler handler = new MockHttpURLStreamHandler(
+ HttpURLConnection.HTTP_OK, testSavePropertiesFile);
+ URL url = new URL(null, "http://jakarta.apache.org", handler);
+ conf.save(url);
+ MockHttpURLConnection con = handler.getMockConnection();
+ assertTrue("Wrong output flag", con.getDoOutput());
+ assertEquals("Wrong method", "PUT", con.getRequestMethod());
+
+ PropertiesConfiguration checkConfig = new PropertiesConfiguration(
+ testSavePropertiesFile);
+ ConfigurationAssert.assertEquals(conf, checkConfig);
+ }
+
+ /**
+ * Tests saving a file-based configuration to a HTTP server when the server
+ * reports a failure. This should cause an exception.
+ */
+ @Test
+ public void testSaveToHTTPServerFail() throws Exception
+ {
+ MockHttpURLStreamHandler handler = new MockHttpURLStreamHandler(
+ HttpURLConnection.HTTP_BAD_REQUEST, testSavePropertiesFile);
+ URL url = new URL(null, "http://jakarta.apache.org", handler);
+ try
+ {
+ conf.save(url);
+ fail("Response code was not checked!");
+ }
+ catch (ConfigurationException cex)
+ {
+ assertTrue("Wrong root cause: " + cex,
+ cex.getCause() instanceof IOException);
+ }
+ }
+
+ /**
+ * Test the creation of a file containing a '#' in its name. This test is
+ * skipped on Java 1.3 as it always fails.
+ */
+ @Test
+ public void testFileWithSharpSymbol() throws Exception
+ {
+ if (SystemUtils.isJavaVersionAtLeast(1.4f))
+ {
+ File file = new File("target/sharp#1.properties");
+ file.createNewFile();
+
+ PropertiesConfiguration conf = new PropertiesConfiguration(file);
+ conf.save();
+
+ assertTrue("Missing file " + file, file.exists());
+ }
+ }
+
+ /**
+ * Tests initializing a properties configuration from a non existing file.
+ * There was a bug, which caused properties getting lost when later save()
+ * is called.
+ */
+ @Test
+ public void testInitFromNonExistingFile() throws ConfigurationException
+ {
+ final String testProperty = "test.successfull";
+ conf = new PropertiesConfiguration(testSavePropertiesFile);
+ conf.addProperty(testProperty, Boolean.TRUE);
+ conf.save();
+ PropertiesConfiguration checkConfig = new PropertiesConfiguration(
+ testSavePropertiesFile);
+ assertTrue("Test property not found", checkConfig
+ .getBoolean(testProperty));
+ }
+
+ /**
+ * Tests copying another configuration into the test configuration. This
+ * test ensures that the layout object is informed about the newly added
+ * properties.
+ */
+ @Test
+ public void testCopyAndSave() throws ConfigurationException
+ {
+ Configuration copyConf = setUpCopyConfig();
+ conf.copy(copyConf);
+ checkCopiedConfig(copyConf);
+ }
+
+ /**
+ * Tests appending a configuration to the test configuration. Again it has
+ * to be ensured that the layout object is correctly updated.
+ */
+ @Test
+ public void testAppendAndSave() throws ConfigurationException
+ {
+ Configuration copyConf = setUpCopyConfig();
+ conf.append(copyConf);
+ checkCopiedConfig(copyConf);
+ }
+
+ /**
+ * Tests adding properties through a DataConfiguration. This is related to
+ * CONFIGURATION-332.
+ */
+ @Test
+ public void testSaveWithDataConfig() throws ConfigurationException
+ {
+ conf = new PropertiesConfiguration(testSavePropertiesFile);
+ DataConfiguration dataConfig = new DataConfiguration(conf);
+ dataConfig.setProperty("foo", "bar");
+ assertEquals("Property not set", "bar", conf.getString("foo"));
+
+ conf.save();
+ PropertiesConfiguration config2 = new PropertiesConfiguration(
+ testSavePropertiesFile);
+ assertEquals("Property not saved", "bar", config2.getString("foo"));
+ }
+
+ /**
+ * Tests whether the correct default encoding is used when loading a
+ * properties file. This test is related to CONFIGURATION-345.
+ */
+ @Test
+ public void testLoadWithDefaultEncoding() throws ConfigurationException
+ {
+ class PropertiesConfigurationTestImpl extends PropertiesConfiguration
+ {
+ String loadEncoding;
+
+ public PropertiesConfigurationTestImpl(String fileName)
+ throws ConfigurationException
+ {
+ super(fileName);
+ }
+
+ @Override
+ public void load(InputStream in, String encoding)
+ throws ConfigurationException
+ {
+ loadEncoding = encoding;
+ super.load(in, encoding);
+ }
+ }
+
+ PropertiesConfigurationTestImpl testConf = new PropertiesConfigurationTestImpl(
+ testProperties);
+ assertEquals("Default encoding not used", "ISO-8859-1",
+ testConf.loadEncoding);
+ }
+
+ /**
+ * Tests whether a default IOFactory is set.
+ */
+ @Test
+ public void testGetIOFactoryDefault()
+ {
+ assertNotNull("No default IO factory", conf.getIOFactory());
+ }
+
+ /**
+ * Tests setting the IOFactory to null. This should cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetIOFactoryNull()
+ {
+ conf.setIOFactory(null);
+ }
+
+ /**
+ * Tests setting an IOFactory that uses a specialized reader.
+ */
+ @Test
+ public void testSetIOFactoryReader() throws ConfigurationException
+ {
+ final int propertyCount = 10;
+ conf.clear();
+ conf.setIOFactory(new PropertiesConfiguration.IOFactory()
+ {
+ public PropertiesConfiguration.PropertiesReader createPropertiesReader(
+ Reader in, char delimiter)
+ {
+ return new PropertiesReaderTestImpl(in, delimiter,
+ propertyCount);
+ }
+
+ public PropertiesConfiguration.PropertiesWriter createPropertiesWriter(
+ Writer out, char delimiter)
+ {
+ throw new UnsupportedOperationException("Unexpected call!");
+ }
+ });
+ conf.load();
+ for (int i = 1; i <= propertyCount; i++)
+ {
+ assertEquals("Wrong property value at " + i, PROP_VALUE + i, conf
+ .getString(PROP_NAME + i));
+ }
+ }
+
+ /**
+ * Tests setting an IOFactory that uses a specialized writer.
+ */
+ @Test
+ public void testSetIOFactoryWriter() throws ConfigurationException, IOException
+ {
+ final PropertiesWriterTestImpl testWriter = new PropertiesWriterTestImpl(',');
+ conf.setIOFactory(new PropertiesConfiguration.IOFactory()
+ {
+ public PropertiesConfiguration.PropertiesReader createPropertiesReader(
+ Reader in, char delimiter)
+ {
+ throw new UnsupportedOperationException("Unexpected call!");
+ }
+
+ public PropertiesConfiguration.PropertiesWriter createPropertiesWriter(
+ Writer out, char delimiter)
+ {
+ return testWriter;
+ }
+ });
+ conf.save(new StringWriter());
+ testWriter.close();
+ checkSavedConfig();
+ }
+
+ /**
+ * Tests that the property separators are retained when saving the
+ * configuration.
+ */
+ @Test
+ public void testKeepSeparators() throws ConfigurationException, IOException
+ {
+ conf.save(testSavePropertiesFile);
+ final String[] separatorTests = {
+ "test.separator.equal = foo", "test.separator.colon : foo",
+ "test.separator.tab\tfoo", "test.separator.whitespace foo",
+ "test.separator.no.space=foo"
+ };
+ Set<String> foundLines = new HashSet<String>();
+ BufferedReader in = new BufferedReader(new FileReader(
+ testSavePropertiesFile));
+ try
+ {
+ String s;
+ while ((s = in.readLine()) != null)
+ {
+ for (int i = 0; i < separatorTests.length; i++)
+ {
+ if (separatorTests[i].equals(s))
+ {
+ foundLines.add(s);
+ }
+ }
+ }
+ }
+ finally
+ {
+ in.close();
+ }
+ assertEquals("No all separators were found: " + foundLines,
+ separatorTests.length, foundLines.size());
+ }
+
+ /**
+ * Tests whether properties with slashes in their values can be saved. This
+ * test is related to CONFIGURATION-408.
+ */
+ @Test
+ public void testSlashEscaping() throws ConfigurationException
+ {
+ conf.setProperty(PROP_NAME, "http://www.apache.org");
+ StringWriter writer = new StringWriter();
+ conf.save(writer);
+ String s = writer.toString();
+ assertTrue("Value not found: " + s, s.indexOf(PROP_NAME
+ + " = http://www.apache.org") >= 0);
+ }
+
+ /**
+ * Tests whether backslashes are correctly handled if lists are parsed. This
+ * test is related to CONFIGURATION-418.
+ */
+ @Test
+ public void testBackslashEscapingInLists() throws Exception
+ {
+ checkBackslashList("share2");
+ checkBackslashList("share1");
+ }
+
+ /**
+ * Tests whether a list property is handled correctly if delimiter parsing
+ * is disabled. This test is related to CONFIGURATION-495.
+ */
+ @Test
+ public void testSetPropertyListWithDelimiterParsingDisabled()
+ throws ConfigurationException
+ {
+ String prop = "delimiterListProp";
+ conf.setDelimiterParsingDisabled(true);
+ List<String> list = Arrays.asList("val", "val2", "val3");
+ conf.setProperty(prop, list);
+ conf.setFile(testSavePropertiesFile);
+ conf.save();
+ conf.clear();
+ conf.load();
+ assertEquals("Wrong list property", list, conf.getProperty(prop));
+ }
+
+ /**
+ * Tests whether a footer comment is correctly read.
+ */
+ @Test
+ public void testReadFooterComment()
+ {
+ assertEquals("Wrong footer comment", "\n# This is a foot comment\n",
+ conf.getFooter());
+ assertEquals("Wrong footer comment from layout",
+ "\nThis is a foot comment\n", conf.getLayout()
+ .getCanonicalFooterCooment(false));
+ }
+
+ /**
+ * Tests whether a footer comment is correctly written out.
+ */
+ @Test
+ public void testWriteFooterComment() throws ConfigurationException,
+ IOException
+ {
+ final String footer = "my footer";
+ conf.clear();
+ conf.setProperty(PROP_NAME, PROP_VALUE);
+ conf.setFooter(footer);
+ StringWriter out = new StringWriter();
+ conf.save(out);
+ assertEquals("Wrong result", PROP_NAME + " = " + PROP_VALUE + CR + "# "
+ + footer + CR, out.toString());
+ }
+
+ /**
+ * Helper method for testing the content of a list with elements that
+ * contain backslashes.
+ *
+ * @param key the key
+ */
+ private void checkBackslashList(String key)
+ {
+ Object prop = conf.getProperty("test." + key);
+ assertTrue("Not a list", prop instanceof List);
+ List<?> list = (List<?>) prop;
+ assertEquals("Wrong number of list elements", 2, list.size());
+ final String prefix = "\\\\" + key;
+ assertEquals("Wrong element 1", prefix + "a", list.get(0));
+ assertEquals("Wrong element 2", prefix + "b", list.get(1));
+ }
+
+ /**
+ * Creates a configuration that can be used for testing copy operations.
+ *
+ * @return the configuration to be copied
+ */
+ private Configuration setUpCopyConfig()
+ {
+ final int count = 25;
+ Configuration result = new BaseConfiguration();
+ for (int i = 1; i <= count; i++)
+ {
+ result.addProperty("copyKey" + i, "copyValue" + i);
+ }
+ return result;
+ }
+
+ /**
+ * Tests whether the data of a configuration that was copied into the test
+ * configuration is correctly saved.
+ *
+ * @param copyConf the copied configuration
+ * @throws ConfigurationException if an error occurs
+ */
+ private void checkCopiedConfig(Configuration copyConf)
+ throws ConfigurationException
+ {
+ conf.save(testSavePropertiesFile);
+ PropertiesConfiguration checkConf = new PropertiesConfiguration(
+ testSavePropertiesFile);
+ for (Iterator<String> it = copyConf.getKeys(); it.hasNext();)
+ {
+ String key = it.next();
+ assertEquals("Wrong value for property " + key, checkConf
+ .getProperty(key), copyConf.getProperty(key));
+ }
+ }
+
+ /**
+ * A dummy layout implementation for checking whether certain methods are
+ * correctly called by the configuration.
+ */
+ static class DummyLayout extends PropertiesConfigurationLayout
+ {
+ /** Stores the number how often load() was called. */
+ public int loadCalls;
+
+ public DummyLayout(PropertiesConfiguration config)
+ {
+ super(config);
+ }
+
+ @Override
+ public void load(Reader in) throws ConfigurationException
+ {
+ loadCalls++;
+ }
+ }
+
+ /**
+ * A mock implementation of a HttpURLConnection used for testing saving to
+ * a HTTP server.
+ */
+ static class MockHttpURLConnection extends HttpURLConnection
+ {
+ /** The response code to return.*/
+ private final int returnCode;
+
+ /** The output file. The output stream will point to this file.*/
+ private final File outputFile;
+
+ protected MockHttpURLConnection(URL u, int respCode, File outFile)
+ {
+ super(u);
+ returnCode = respCode;
+ outputFile = outFile;
+ }
+
+ @Override
+ public void disconnect()
+ {
+ }
+
+ @Override
+ public boolean usingProxy()
+ {
+ return false;
+ }
+
+ @Override
+ public void connect() throws IOException
+ {
+ }
+
+ @Override
+ public int getResponseCode() throws IOException
+ {
+ return returnCode;
+ }
+
+ @Override
+ public OutputStream getOutputStream() throws IOException
+ {
+ return new FileOutputStream(outputFile);
+ }
+ }
+
+ /**
+ * A mock stream handler for working with the mock HttpURLConnection.
+ */
+ static class MockHttpURLStreamHandler extends URLStreamHandler
+ {
+ /** Stores the response code.*/
+ private int responseCode;
+
+ /** Stores the output file.*/
+ private File outputFile;
+
+ /** Stores the connection.*/
+ private MockHttpURLConnection connection;
+
+ public MockHttpURLStreamHandler(int respCode, File outFile)
+ {
+ responseCode = respCode;
+ outputFile = outFile;
+ }
+
+ public MockHttpURLConnection getMockConnection()
+ {
+ return connection;
+ }
+
+ @Override
+ protected URLConnection openConnection(URL u) throws IOException
+ {
+ connection = new MockHttpURLConnection(u, responseCode, outputFile);
+ return connection;
+ }
+ }
+
+ /**
+ * A test PropertiesReader for testing whether a custom reader can be
+ * injected. This implementation creates a configurable number of synthetic
+ * test properties.
+ */
+ private static class PropertiesReaderTestImpl extends
+ PropertiesConfiguration.PropertiesReader
+ {
+ /** The number of test properties to be created. */
+ private final int maxProperties;
+
+ /** The current number of properties. */
+ private int propertyCount;
+
+ public PropertiesReaderTestImpl(Reader reader, char listDelimiter,
+ int maxProps)
+ {
+ super(reader, listDelimiter);
+ assertEquals("Wrong list delimiter", ',', listDelimiter);
+ maxProperties = maxProps;
+ }
+
+ @Override
+ public String getPropertyName()
+ {
+ return PROP_NAME + propertyCount;
+ }
+
+ @Override
+ public String getPropertyValue()
+ {
+ return PROP_VALUE + propertyCount;
+ }
+
+ @Override
+ public boolean nextProperty() throws IOException
+ {
+ propertyCount++;
+ return propertyCount <= maxProperties;
+ }
+ }
+
+ /**
+ * A test PropertiesWriter for testing whether a custom writer can be
+ * injected. This implementation simply redirects all output into a test
+ * file.
+ */
+ private static class PropertiesWriterTestImpl extends
+ PropertiesConfiguration.PropertiesWriter
+ {
+ public PropertiesWriterTestImpl(char delimiter) throws IOException
+ {
+ super(new FileWriter(testSavePropertiesFile), delimiter);
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestPropertiesConfigurationLayout.java b/src/test/java/org/apache/commons/configuration/TestPropertiesConfigurationLayout.java
new file mode 100644
index 0000000..b85e9b0
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestPropertiesConfigurationLayout.java
@@ -0,0 +1,845 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Iterator;
+
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for PropertiesConfigurationLayout.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestPropertiesConfigurationLayout.java 1534402 2013-10-21 22:35:52Z henning $
+ */
+public class TestPropertiesConfigurationLayout
+{
+ /** Constant for the line break character. */
+ private static final String CR = System.getProperty("line.separator");
+
+ /** Constant for the normalized line break character. */
+ private static final String CRNORM = "\n";
+
+ /** Constant for a test property key. */
+ static final String TEST_KEY = "myProperty";
+
+ /** Constant for a test comment. */
+ static final String TEST_COMMENT = "A comment for my test property";
+
+ /** Constant for a test property value. */
+ static final String TEST_VALUE = "myPropertyValue";
+
+ /** The layout object under test. */
+ PropertiesConfigurationLayout layout;
+
+ /** The associated configuration object. */
+ LayoutTestConfiguration config;
+
+ /** A properties builder that can be used for testing. */
+ PropertiesBuilder builder;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ config = new LayoutTestConfiguration();
+ layout = new PropertiesConfigurationLayout(config);
+ config.setLayout(layout);
+ builder = new PropertiesBuilder();
+ }
+
+ /**
+ * Tests a newly created instance.
+ */
+ @Test
+ public void testInit()
+ {
+ assertTrue("Object contains keys", layout.getKeys().isEmpty());
+ assertNull("Header comment not null", layout.getHeaderComment());
+ Iterator<ConfigurationListener> it = config.getConfigurationListeners().iterator();
+ assertTrue("No event listener registered", it.hasNext());
+ assertSame("Layout not registered as event listener", layout, it.next());
+ assertFalse("Multiple event listeners registered", it.hasNext());
+ assertSame("Configuration not stored", config, layout
+ .getConfiguration());
+ assertFalse("Force single line flag set", layout.isForceSingleLine());
+ assertNull("Got a global separator", layout.getGlobalSeparator());
+ }
+
+ /**
+ * Tests creating a layout object with a null configuration. This should
+ * cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testInitNull()
+ {
+ new PropertiesConfigurationLayout(null);
+ }
+
+ /**
+ * Tests reading a simple properties file.
+ */
+ @Test
+ public void testReadSimple() throws ConfigurationException
+ {
+ builder.addComment(TEST_COMMENT);
+ builder.addProperty(TEST_KEY, TEST_VALUE);
+ layout.load(builder.getReader());
+ assertNull("A header comment was found", layout.getHeaderComment());
+ assertEquals("Wrong number of properties", 1, layout.getKeys().size());
+ assertTrue("Property not found", layout.getKeys().contains(TEST_KEY));
+ assertEquals("Comment not found", TEST_COMMENT, layout
+ .getCanonicalComment(TEST_KEY, false));
+ assertEquals("Wrong number of blanc lines", 0, layout
+ .getBlancLinesBefore(TEST_KEY));
+ assertTrue("Wrong single line flag", layout.isSingleLine(TEST_KEY));
+ assertEquals("Property not stored in config", TEST_VALUE, config
+ .getString(TEST_KEY));
+ }
+
+ /**
+ * Tests whether blanc lines before a property are correctly detected.
+ */
+ @Test
+ public void testBlancLines() throws ConfigurationException
+ {
+ builder.addProperty("prop", "value");
+ builder.addComment(null);
+ builder.addComment(null);
+ builder.addComment(TEST_COMMENT);
+ builder.addComment(null);
+ builder.addProperty(TEST_KEY, TEST_VALUE);
+ layout.load(builder.getReader());
+ assertEquals("Wrong number of blanc lines", 2, layout
+ .getBlancLinesBefore(TEST_KEY));
+ assertEquals("Wrong comment", TEST_COMMENT + CRNORM, layout
+ .getCanonicalComment(TEST_KEY, false));
+ assertEquals("Wrong property value", TEST_VALUE, config
+ .getString(TEST_KEY));
+ }
+
+ /**
+ * Tests the single line flag for a simple property definition.
+ */
+ @Test
+ public void testIsSingleLine() throws ConfigurationException
+ {
+ builder.addProperty(TEST_KEY, TEST_VALUE + "," + TEST_VALUE + "2");
+ layout.load(builder.getReader());
+ assertTrue("Wrong single line flag", layout.isSingleLine(TEST_KEY));
+ assertEquals("Wrong number of values", 2, config.getList(TEST_KEY)
+ .size());
+ }
+
+ /**
+ * Tests the single line flag if there are multiple property definitions.
+ */
+ @Test
+ public void testIsSingleLineMulti() throws ConfigurationException
+ {
+ builder.addProperty(TEST_KEY, TEST_VALUE);
+ builder.addProperty("anotherProp", "a value");
+ builder.addProperty(TEST_KEY, TEST_VALUE + "2");
+ layout.load(builder.getReader());
+ assertFalse("Wrong single line flag", layout.isSingleLine(TEST_KEY));
+ assertEquals("Wrong number of values", 2, config.getList(TEST_KEY)
+ .size());
+ }
+
+ /**
+ * Tests whether comments are combined for multiple occurrences.
+ */
+ @Test
+ public void testCombineComments() throws ConfigurationException
+ {
+ builder.addComment(TEST_COMMENT);
+ builder.addProperty(TEST_KEY, TEST_VALUE);
+ builder.addComment(null);
+ builder.addComment(TEST_COMMENT);
+ builder.addProperty(TEST_KEY, TEST_VALUE + "2");
+ layout.load(builder.getReader());
+ assertEquals("Wrong combined comment",
+ TEST_COMMENT + CRNORM + TEST_COMMENT, layout.getCanonicalComment(
+ TEST_KEY, false));
+ assertEquals("Wrong combined blanc numbers", 0, layout
+ .getBlancLinesBefore(TEST_KEY));
+ }
+
+ /**
+ * Tests if a header comment is detected.
+ */
+ @Test
+ public void testHeaderComment() throws ConfigurationException
+ {
+ builder.addComment(TEST_COMMENT);
+ builder.addComment(null);
+ builder.addProperty(TEST_KEY, TEST_VALUE);
+ layout.load(builder.getReader());
+ assertEquals("Wrong header comment", TEST_COMMENT, layout
+ .getCanonicalHeaderComment(false));
+ assertNull("Wrong comment for property", layout.getCanonicalComment(
+ TEST_KEY, false));
+ }
+
+ /**
+ * Tests if a header comment containing blanc lines is correctly detected.
+ */
+ @Test
+ public void testHeaderCommentWithBlancs() throws ConfigurationException
+ {
+ builder.addComment(TEST_COMMENT);
+ builder.addComment(null);
+ builder.addComment(TEST_COMMENT);
+ builder.addComment(null);
+ builder.addProperty(TEST_KEY, TEST_VALUE);
+ layout.load(builder.getReader());
+ assertEquals("Wrong header comment", TEST_COMMENT + CRNORM + CRNORM
+ + TEST_COMMENT, layout.getCanonicalHeaderComment(false));
+ assertNull("Wrong comment for property", layout.getComment(TEST_KEY));
+ }
+
+ /**
+ * Tests if a header comment is correctly detected when it contains blanc
+ * lines and the first property has a comment, too.
+ */
+ @Test
+ public void testHeaderCommentWithBlancsAndPropComment()
+ throws ConfigurationException
+ {
+ builder.addComment(TEST_COMMENT);
+ builder.addComment(null);
+ builder.addComment(TEST_COMMENT);
+ builder.addComment(null);
+ builder.addComment(TEST_COMMENT);
+ builder.addProperty(TEST_KEY, TEST_VALUE);
+ layout.load(builder.getReader());
+ assertEquals("Wrong header comment", TEST_COMMENT + CRNORM + CRNORM
+ + TEST_COMMENT, layout.getCanonicalHeaderComment(false));
+ assertEquals("Wrong comment for property", TEST_COMMENT, layout
+ .getCanonicalComment(TEST_KEY, false));
+ }
+
+ /**
+ * Tests fetching a canonical header comment when no comment is set.
+ */
+ @Test
+ public void testHeaderCommentNull()
+ {
+ assertNull("No null comment with comment chars", layout
+ .getCanonicalHeaderComment(true));
+ assertNull("No null comment without comment chars", layout
+ .getCanonicalHeaderComment(false));
+ }
+
+ /**
+ * Tests if a property add event is correctly processed.
+ */
+ @Test
+ public void testEventAdd()
+ {
+ ConfigurationEvent event = new ConfigurationEvent(this,
+ AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_KEY, TEST_VALUE,
+ false);
+ layout.configurationChanged(event);
+ assertTrue("Property not stored", layout.getKeys().contains(TEST_KEY));
+ assertEquals("Blanc lines before new property", 0, layout
+ .getBlancLinesBefore(TEST_KEY));
+ assertTrue("No single line property", layout.isSingleLine(TEST_KEY));
+ assertEquals("Wrong separator", " = ", layout.getSeparator(TEST_KEY));
+ }
+
+ /**
+ * Tests adding a property multiple time through an event. The property
+ * should then be a multi-line property.
+ */
+ @Test
+ public void testEventAddMultiple()
+ {
+ ConfigurationEvent event = new ConfigurationEvent(this,
+ AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_KEY, TEST_VALUE,
+ false);
+ layout.configurationChanged(event);
+ layout.configurationChanged(event);
+ assertFalse("No multi-line property", layout.isSingleLine(TEST_KEY));
+ }
+
+ /**
+ * Tests if an add event is correctly processed if the affected property is
+ * already stored in the layout object.
+ */
+ @Test
+ public void testEventAddExisting() throws ConfigurationException
+ {
+ builder.addComment(TEST_COMMENT);
+ builder.addProperty(TEST_KEY, TEST_VALUE);
+ layout.load(builder.getReader());
+ ConfigurationEvent event = new ConfigurationEvent(this,
+ AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_KEY, TEST_VALUE,
+ false);
+ layout.configurationChanged(event);
+ assertFalse("No multi-line property", layout.isSingleLine(TEST_KEY));
+ assertEquals("Comment was modified", TEST_COMMENT, layout
+ .getCanonicalComment(TEST_KEY, false));
+ }
+
+ /**
+ * Tests if a set property event for a non existing property is correctly
+ * handled.
+ */
+ @Test
+ public void testEventSetNonExisting()
+ {
+ ConfigurationEvent event = new ConfigurationEvent(this,
+ AbstractConfiguration.EVENT_SET_PROPERTY, TEST_KEY, TEST_VALUE,
+ false);
+ layout.configurationChanged(event);
+ assertTrue("New property was not found", layout.getKeys().contains(
+ TEST_KEY));
+ }
+
+ /**
+ * Tests if a property delete event is correctly processed.
+ */
+ @Test
+ public void testEventDelete()
+ {
+ ConfigurationEvent event = new ConfigurationEvent(this,
+ AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_KEY, TEST_VALUE,
+ false);
+ layout.configurationChanged(event);
+ event = new ConfigurationEvent(this,
+ AbstractConfiguration.EVENT_CLEAR_PROPERTY, TEST_KEY, null,
+ false);
+ layout.configurationChanged(event);
+ assertFalse("Property still existing", layout.getKeys().contains(
+ TEST_KEY));
+ }
+
+ /**
+ * Tests if a clear event is correctly processed.
+ */
+ @Test
+ public void testEventClearConfig() throws Exception
+ {
+ fillLayout();
+ ConfigurationEvent event = new ConfigurationEvent(this,
+ AbstractConfiguration.EVENT_CLEAR, null, null, false);
+ layout.configurationChanged(event);
+ assertTrue("Keys not empty", layout.getKeys().isEmpty());
+ assertNull("Header comment was not reset", layout.getHeaderComment());
+ }
+
+ /**
+ * Tests if a before update event is correctly ignored.
+ */
+ @Test
+ public void testEventAddBefore()
+ {
+ ConfigurationEvent event = new ConfigurationEvent(this,
+ AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_KEY, TEST_VALUE,
+ true);
+ layout.configurationChanged(event);
+ assertFalse("Property already stored", layout.getKeys().contains(
+ TEST_KEY));
+ }
+
+ /**
+ * Tests if a reload update is correctly processed.
+ */
+ @Test
+ public void testEventReload()
+ {
+ fillLayout();
+ ConfigurationEvent event = new ConfigurationEvent(this,
+ AbstractFileConfiguration.EVENT_RELOAD, null, null, true);
+ layout.configurationChanged(event);
+ assertTrue("Keys not empty", layout.getKeys().isEmpty());
+ assertNull("Header comment was not reset", layout.getHeaderComment());
+ }
+
+ /**
+ * Tests the event after a reload has been performed. This should be
+ * ignored.
+ */
+ @Test
+ public void testEventReloadAfter()
+ {
+ fillLayout();
+ ConfigurationEvent event = new ConfigurationEvent(this,
+ AbstractFileConfiguration.EVENT_RELOAD, null, null, false);
+ layout.configurationChanged(event);
+ assertFalse("Keys are empty", layout.getKeys().isEmpty());
+ assertNotNull("Header comment was reset", layout.getHeaderComment());
+ }
+
+ /**
+ * Tests a recursive load call.
+ */
+ @Test
+ public void testRecursiveLoadCall() throws ConfigurationException
+ {
+ PropertiesBuilder b = new PropertiesBuilder();
+ b.addComment("A nested header comment.");
+ b.addComment("With multiple lines");
+ b.addComment(null);
+ b.addComment("Second comment");
+ b.addProperty(TEST_KEY, TEST_VALUE);
+ b.addProperty(TEST_KEY + "2", TEST_VALUE + "2");
+ config.builder = b;
+
+ builder.addComment("Header comment");
+ builder.addComment(null);
+ builder.addComment(TEST_COMMENT);
+ builder.addProperty(TEST_KEY, TEST_VALUE);
+ builder.addComment("Include file");
+ builder.addProperty(PropertiesConfiguration.getInclude(), "test");
+
+ layout.load(builder.getReader());
+
+ assertEquals("Wrong header comment", "Header comment", layout
+ .getCanonicalHeaderComment(false));
+ assertFalse("Include property was stored", layout.getKeys().contains(
+ PropertiesConfiguration.getInclude()));
+ assertEquals("Wrong comment for property", TEST_COMMENT + CRNORM
+ + "A nested header comment." + CRNORM + "With multiple lines" + CRNORM
+ + CRNORM + "Second comment", layout.getCanonicalComment(TEST_KEY,
+ false));
+ }
+
+ /**
+ * Tests whether the output of the layout object is identical to the source
+ * file (at least for simple properties files).
+ */
+ @Test
+ public void testReadAndWrite() throws ConfigurationException
+ {
+ builder.addComment("This is my test properties file,");
+ builder.addComment("which contains a header comment.");
+ builder.addComment(null);
+ builder.addComment(TEST_COMMENT);
+ builder.addProperty(TEST_KEY, TEST_COMMENT);
+ builder.addComment(null);
+ builder.addComment(null);
+ builder.addComment("Another comment");
+ builder.addProperty("property", "and a value");
+ layout.load(builder.getReader());
+ checkLayoutString(builder.toString());
+ }
+
+ /**
+ * Tests if the content of the layout object is correctly written.
+ */
+ @Test
+ public void testSave() throws ConfigurationException
+ {
+ config.addProperty(TEST_KEY, TEST_VALUE);
+ layout.setComment(TEST_KEY, TEST_COMMENT);
+ config.addProperty(TEST_KEY, TEST_VALUE + "2");
+ config.addProperty("AnotherProperty", "AnotherValue");
+ config.addProperty("AnotherProperty", "3rdValue");
+ layout.setComment("AnotherProperty", "AnotherComment");
+ layout.setBlancLinesBefore("AnotherProperty", 2);
+ layout.setSingleLine("AnotherProperty", true);
+ layout.setHeaderComment("A header comment" + CRNORM + "for my properties");
+ checkLayoutString("# A header comment" + CR + "# for my properties"
+ + CR + CR + "# " + TEST_COMMENT + CR + TEST_KEY + " = "
+ + TEST_VALUE + CR + TEST_KEY + " = " + TEST_VALUE + "2" + CR
+ + CR + CR + "# AnotherComment" + CR
+ + "AnotherProperty = AnotherValue,3rdValue" + CR);
+ }
+
+ /**
+ * Tests the force single line flag.
+ */
+ @Test
+ public void testSaveForceSingleLine() throws ConfigurationException
+ {
+ config.setListDelimiter(';');
+ config.addProperty(TEST_KEY, TEST_VALUE);
+ config.addProperty(TEST_KEY, TEST_VALUE + "2");
+ config.addProperty("AnotherProperty", "value1;value2;value3");
+ layout.setComment(TEST_KEY, TEST_COMMENT);
+ layout.setForceSingleLine(true);
+ checkLayoutString("# " + TEST_COMMENT + CR + TEST_KEY + " = "
+ + TEST_VALUE + ';' + TEST_VALUE + "2" + CR
+ + "AnotherProperty = value1;value2;value3" + CR);
+ }
+
+ /**
+ * Tests the trimComment method.
+ */
+ @Test
+ public void testTrimComment()
+ {
+ assertEquals("Wrong trimmed comment", "This is a comment" + CR
+ + "that spans multiple" + CR + "lines in a" + CR
+ + " complex way.", PropertiesConfigurationLayout.trimComment(
+ " # This is a comment" + CR + "that spans multiple" + CR
+ + "!lines in a" + CR + " complex way.", false));
+ }
+
+ /**
+ * Tests trimming a comment with trailing CRs.
+ */
+ @Test
+ public void testTrimCommentTrainlingCR()
+ {
+ assertEquals("Wrong trimmed comment", "Comment with" + CR
+ + "trailing CR" + CR, PropertiesConfigurationLayout
+ .trimComment("Comment with" + CR + "! trailing CR" + CR, false));
+ }
+
+ /**
+ * Tests enforcing comment characters in a comment.
+ */
+ @Test
+ public void testTrimCommentFalse()
+ {
+ assertEquals("Wrong trimmed comment", "# Comment with" + CR
+ + " ! some mixed " + CR + "#comment" + CR + "# lines",
+ PropertiesConfigurationLayout.trimComment("Comment with" + CR
+ + " ! some mixed " + CR + "#comment" + CR + "lines",
+ true));
+ }
+
+ /**
+ * Tests accessing data for a property, which is not stored.
+ */
+ @Test
+ public void testGetNonExistingLayouData()
+ {
+ assertNull("A comment was found", layout.getComment("unknown"));
+ assertTrue("A multi-line property", layout.isSingleLine("unknown"));
+ assertEquals("Leading blanc lines", 0, layout
+ .getBlancLinesBefore("unknown"));
+ }
+
+ /**
+ * Tests accessing a property with a null key. This should throw an
+ * exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetNullLayouttData()
+ {
+ layout.setComment(null, TEST_COMMENT);
+ }
+
+ /**
+ * Tests resetting a comment.
+ */
+ @Test
+ public void testSetNullComment()
+ {
+ fillLayout();
+ layout.setComment(TEST_KEY, null);
+ assertNull("Comment was not reset", layout.getComment(TEST_KEY));
+ }
+
+ /**
+ * Tests saving when a comment for a non existing property is contained in
+ * the layout object. This comment should be ignored.
+ */
+ @Test
+ public void testSaveCommentForUnexistingProperty()
+ throws ConfigurationException
+ {
+ fillLayout();
+ layout.setComment("NonExistingKey", "NonExistingComment");
+ String output = getLayoutString();
+ assertTrue("Non existing key was found", output
+ .indexOf("NonExistingKey") < 0);
+ assertTrue("Non existing comment was found", output
+ .indexOf("NonExistingComment") < 0);
+ }
+
+ /**
+ * Tests saving an empty layout object.
+ */
+ @Test
+ public void testSaveEmptyLayout() throws ConfigurationException
+ {
+ checkLayoutString("");
+ }
+
+ /**
+ * Tests the copy constructor.
+ */
+ @Test
+ public void testInitCopy()
+ {
+ fillLayout();
+ PropertiesConfigurationLayout l2 = new PropertiesConfigurationLayout(
+ config, layout);
+ assertEquals("Wrong number of keys", layout.getKeys().size(), l2
+ .getKeys().size());
+ for (String key : layout.getKeys())
+ {
+ assertTrue("Key was not found: " + key, l2.getKeys().contains(key));
+ }
+ assertEquals("Wrong header comment", layout.getHeaderComment(),
+ l2.getHeaderComment());
+ assertEquals("Wrong footer comment", layout.getFooterComment(),
+ l2.getFooterComment());
+ }
+
+ /**
+ * Tests if the copy and the original are independent from each other.
+ */
+ @Test
+ public void testInitCopyModify()
+ {
+ fillLayout();
+ PropertiesConfigurationLayout l2 = new PropertiesConfigurationLayout(
+ config, layout);
+ assertEquals("Comments are not equal", layout.getComment(TEST_KEY), l2
+ .getComment(TEST_KEY));
+ layout.setComment(TEST_KEY, "A new comment");
+ assertEquals("Comment was changed", TEST_COMMENT, l2
+ .getCanonicalComment(TEST_KEY, false));
+ l2.setBlancLinesBefore(TEST_KEY, l2.getBlancLinesBefore(TEST_KEY) + 1);
+ assertFalse("Blanc lines do not differ", layout
+ .getBlancLinesBefore(TEST_KEY) == l2
+ .getBlancLinesBefore(TEST_KEY));
+ }
+
+ /**
+ * Tests changing the separator for a property.
+ */
+ @Test
+ public void testSetSeparator() throws ConfigurationException
+ {
+ config.addProperty(TEST_KEY, TEST_VALUE);
+ layout.setSeparator(TEST_KEY, ":");
+ checkLayoutString(TEST_KEY + ":" + TEST_VALUE + CR);
+ }
+
+ /**
+ * Tests setting the global separator. This separator should override the
+ * separators for all properties.
+ */
+ @Test
+ public void testSetGlobalSeparator() throws ConfigurationException
+ {
+ final String sep = "=";
+ config.addProperty(TEST_KEY, TEST_VALUE);
+ config.addProperty("key2", "value2");
+ layout.setSeparator(TEST_KEY, " : ");
+ layout.setGlobalSeparator(sep);
+ checkLayoutString(TEST_KEY + sep + TEST_VALUE + CR + "key2" + sep
+ + "value2" + CR);
+ }
+
+ /**
+ * Tests setting the line separator.
+ */
+ @Test
+ public void testSetLineSeparator() throws ConfigurationException
+ {
+ final String lf = CR + CR;
+ config.addProperty(TEST_KEY, TEST_VALUE);
+ layout.setBlancLinesBefore(TEST_KEY, 2);
+ layout.setComment(TEST_KEY, TEST_COMMENT);
+ layout.setHeaderComment("Header comment");
+ layout.setLineSeparator(lf);
+ checkLayoutString("# Header comment" + lf + lf + lf + lf + "# "
+ + TEST_COMMENT + lf + TEST_KEY + " = " + TEST_VALUE + lf);
+ }
+
+ /**
+ * Tests whether the line separator is also taken into account within
+ * comments.
+ */
+ @Test
+ public void testSetLineSeparatorInComments() throws ConfigurationException
+ {
+ final String lf = "<-\n";
+ config.addProperty(TEST_KEY, TEST_VALUE);
+ layout.setComment(TEST_KEY, TEST_COMMENT + "\nMore comment");
+ layout.setHeaderComment("Header\ncomment");
+ layout.setLineSeparator(lf);
+ checkLayoutString("# Header" + lf + "# comment" + lf + lf + "# "
+ + TEST_COMMENT + lf + "# More comment" + lf + TEST_KEY + " = "
+ + TEST_VALUE + lf);
+ }
+
+ /**
+ * Helper method for filling the layout object with some properties.
+ */
+ private void fillLayout()
+ {
+ builder.addComment("A header comment");
+ builder.addComment(null);
+ builder.addProperty("prop", "value");
+ builder.addComment(TEST_COMMENT);
+ builder.addProperty(TEST_KEY, TEST_VALUE);
+ builder.addProperty("anotherProp", "anotherValue");
+ builder.addComment("A footer comment");
+ try
+ {
+ layout.load(builder.getReader());
+ }
+ catch (ConfigurationException cex)
+ {
+ // should not happen
+ fail("Exception was thrown: " + cex);
+ }
+ }
+
+ /**
+ * Writes the layout's data into a string.
+ *
+ * @return the layout file's content as string
+ * @throws ConfigurationException if an error occurs
+ */
+ private String getLayoutString() throws ConfigurationException
+ {
+ StringWriter out = new StringWriter();
+ layout.save(out);
+ return out.toString();
+ }
+
+ /**
+ * Checks if the layout's output is correct.
+ *
+ * @param expected the expected result
+ * @throws ConfigurationException if an error occurs
+ */
+ private void checkLayoutString(String expected)
+ throws ConfigurationException
+ {
+ assertEquals("Wrong layout file content", expected, getLayoutString());
+ }
+
+ /**
+ * A helper class used for constructing test properties files.
+ */
+ static class PropertiesBuilder
+ {
+ /** A buffer for storing the data. */
+ private StringBuilder buf = new StringBuilder();
+
+ /** A counter for varying the comment character. */
+ private int commentCounter;
+
+ /**
+ * Adds a property to the simulated file.
+ *
+ * @param key the property key
+ * @param value the value
+ */
+ public void addProperty(String key, String value)
+ {
+ buf.append(key).append(" = ").append(value).append(CR);
+ }
+
+ /**
+ * Adds a comment line.
+ *
+ * @param s the comment (can be <b>null</b>, then a blanc line is
+ * added)
+ */
+ public void addComment(String s)
+ {
+ if (s != null)
+ {
+ if (commentCounter % 2 == 0)
+ {
+ buf.append("# ");
+ }
+ else
+ {
+ buf.append("! ");
+ }
+ buf.append(s);
+ commentCounter++;
+ }
+ buf.append(CR);
+ }
+
+ /**
+ * Returns a reader for the simulated properties.
+ *
+ * @return a reader
+ */
+ public Reader getReader()
+ {
+ return new StringReader(buf.toString());
+ }
+
+ /**
+ * Returns a string representation of the buffer's content.
+ *
+ * @return the buffer as string
+ */
+ @Override
+ public String toString()
+ {
+ return buf.toString();
+ }
+ }
+
+ /**
+ * A mock properties configuration implementation that is used to check
+ * whether some expected methods are called.
+ */
+ static class LayoutTestConfiguration extends PropertiesConfiguration
+ {
+ /** Stores a builder object. */
+ public PropertiesBuilder builder;
+
+ /**
+ * Simulates the propertyLoaded() callback. If a builder was set, a
+ * load() call on the layout is invoked.
+ */
+ @Override
+ boolean propertyLoaded(String key, String value)
+ throws ConfigurationException
+ {
+ if (builder == null)
+ {
+ return super.propertyLoaded(key, value);
+ }
+ else
+ {
+ if (PropertiesConfiguration.getInclude().equals(key))
+ {
+ getLayout().load(builder.getReader());
+ return false;
+ }
+ else
+ {
+ return true;
+ }
+ }
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestPropertiesSequence.java b/src/test/java/org/apache/commons/configuration/TestPropertiesSequence.java
new file mode 100644
index 0000000..daa9fe4
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestPropertiesSequence.java
@@ -0,0 +1,146 @@
+package org.apache.commons.configuration;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.commons.collections.IteratorUtils;
+import org.apache.commons.lang.StringUtils;
+import org.junit.Test;
+
+/**
+ * Test that the configuration factory returns keys in the same
+ * sequence as the properties configurator
+ *
+ * @version $Id: TestPropertiesSequence.java 1225011 2011-12-27 20:46:13Z oheger $
+ */
+ at SuppressWarnings("deprecation")
+public class TestPropertiesSequence
+{
+ @Test
+ public void testConfigurationValuesInSameOrderFromFile() throws Exception
+ {
+ String simpleConfigurationFile = ConfigurationAssert.getTestFile("testSequence.properties").getAbsolutePath();
+ String compositeConfigurationFile = ConfigurationAssert.getTestFile("testSequenceDigester.xml").getAbsolutePath();
+
+ Configuration simpleConfiguration = new PropertiesConfiguration(simpleConfigurationFile);
+
+ ConfigurationFactory configurationFactory = new ConfigurationFactory();
+ configurationFactory.setConfigurationFileName(compositeConfigurationFile);
+ Configuration compositeConfiguration = configurationFactory.getConfiguration();
+
+ Configuration a = simpleConfiguration.subset("prefix");
+ Configuration b = compositeConfiguration.subset("prefix");
+
+ List<?> keysSimpleConfiguration = IteratorUtils.toList(a.getKeys());
+ List<?> keysCompositeConfiguration = IteratorUtils.toList(b.getKeys());
+
+ assertTrue("Size:" + keysSimpleConfiguration.size(), keysSimpleConfiguration.size() > 0);
+ assertEquals(keysSimpleConfiguration.size(), keysCompositeConfiguration.size());
+
+ for (int i = 0; i < keysSimpleConfiguration.size(); i++)
+ {
+ assertEquals(keysSimpleConfiguration.get(i), keysCompositeConfiguration.get(i));
+ }
+ }
+
+ @Test
+ public void testConfigurationValuesInSameOrderWithManualAdd() throws Exception
+ {
+ String simpleConfigurationFile = ConfigurationAssert.getTestFile("testSequence.properties").getAbsolutePath();
+ String compositeConfigurationFile = ConfigurationAssert.getTestFile("testSequenceDigester.xml").getAbsolutePath();
+
+ Configuration simpleConfiguration = new PropertiesConfiguration(simpleConfigurationFile);
+
+ ConfigurationFactory configurationFactory = new ConfigurationFactory();
+ configurationFactory.setConfigurationFileName(compositeConfigurationFile);
+ Configuration compositeConfiguration = configurationFactory.getConfiguration();
+
+ simpleConfiguration.setProperty("prefix.Co.test", Boolean.TRUE);
+ simpleConfiguration.setProperty("prefix.Av.test", Boolean.TRUE);
+
+ compositeConfiguration.setProperty("prefix.Co.test", Boolean.TRUE);
+ compositeConfiguration.setProperty("prefix.Av.test", Boolean.TRUE);
+
+ Configuration a = simpleConfiguration.subset("prefix");
+ Configuration b = compositeConfiguration.subset("prefix");
+
+ List<?> keysSimpleConfiguration = IteratorUtils.toList(a.getKeys());
+ List<?> keysCompositeConfiguration = IteratorUtils.toList(b.getKeys());
+
+ assertTrue("Size:" + keysSimpleConfiguration.size(), keysSimpleConfiguration.size() > 0);
+ assertEquals(keysSimpleConfiguration.size(), keysCompositeConfiguration.size());
+
+ for (int i = 0; i < keysSimpleConfiguration.size(); i++)
+ {
+ assertEquals(keysSimpleConfiguration.get(i), keysCompositeConfiguration.get(i));
+ }
+ }
+
+ @Test
+ public void testMappingInSameOrder() throws Exception
+ {
+ String simpleConfigurationFile = ConfigurationAssert.getTestFile("testSequence.properties").getAbsolutePath();
+ String compositeConfigurationFile = ConfigurationAssert.getTestFile("testSequenceDigester.xml").getAbsolutePath();
+
+ Configuration simpleConfiguration = new PropertiesConfiguration(simpleConfigurationFile);
+
+ ConfigurationFactory configurationFactory = new ConfigurationFactory();
+ configurationFactory.setConfigurationFileName(compositeConfigurationFile);
+ Configuration compositeConfiguration = configurationFactory.getConfiguration();
+
+ Configuration mapping = new BaseConfiguration();
+ Configuration mapping2 = new BaseConfiguration();
+
+ for (Iterator<String> keys = simpleConfiguration.getKeys(); keys.hasNext();)
+ {
+ String key = keys.next();
+ String[] keyParts = StringUtils.split(key, ".");
+
+ if ((keyParts.length == 3) && keyParts[0].equals("prefix") && keyParts[2].equals("postfix"))
+ {
+ String serviceKey = keyParts[1];
+
+ if (!mapping.containsKey(serviceKey))
+ {
+ mapping.setProperty(serviceKey, simpleConfiguration.getString(key));
+ }
+ }
+ }
+
+ for (Iterator<String> keys = compositeConfiguration.getKeys(); keys.hasNext();)
+ {
+ String key = keys.next();
+ String[] keyParts = StringUtils.split(key, ".");
+
+ if ((keyParts.length == 3) && keyParts[0].equals("prefix") && keyParts[2].equals("postfix"))
+ {
+ String serviceKey = keyParts[1];
+
+ if (!mapping2.containsKey(serviceKey))
+ {
+ mapping2.setProperty(serviceKey, compositeConfiguration.getString(key));
+ }
+ }
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestPropertyConverter.java b/src/test/java/org/apache/commons/configuration/TestPropertyConverter.java
new file mode 100644
index 0000000..59c73f2
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestPropertyConverter.java
@@ -0,0 +1,390 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.lang.annotation.ElementType;
+import java.math.BigDecimal;
+import java.util.Iterator;
+import java.util.List;
+
+import org.junit.Test;
+
+/**
+ * Test class for PropertyConverter.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: TestPropertyConverter.java 1534376 2013-10-21 21:14:18Z henning $
+ */
+public class TestPropertyConverter
+{
+ /** Constant for an enumeration class used by some tests. */
+ private static final Class<ElementType> ENUM_CLASS = ElementType.class;
+
+ @Test
+ public void testSplit()
+ {
+ String s = "abc, xyz , 123";
+ List<String> list = PropertyConverter.split(s, ',');
+
+ assertEquals("size", 3, list.size());
+ assertEquals("1st token for '" + s + "'", "abc", list.get(0));
+ assertEquals("2nd token for '" + s + "'", "xyz", list.get(1));
+ assertEquals("3rd token for '" + s + "'", "123", list.get(2));
+ }
+
+ @Test
+ public void testSplitNoTrim()
+ {
+ String s = "abc, xyz , 123";
+ List<String> list = PropertyConverter.split(s, ',', false);
+
+ assertEquals("size", 3, list.size());
+ assertEquals("1st token for '" + s + "'", "abc", list.get(0));
+ assertEquals("2nd token for '" + s + "'", " xyz ", list.get(1));
+ assertEquals("3rd token for '" + s + "'", " 123", list.get(2));
+ }
+
+ @Test
+ public void testSplitWithEscapedSeparator()
+ {
+ String s = "abc\\,xyz, 123";
+ List<String> list = PropertyConverter.split(s, ',');
+
+ assertEquals("size", 2, list.size());
+ assertEquals("1st token for '" + s + "'", "abc,xyz", list.get(0));
+ assertEquals("2nd token for '" + s + "'", "123", list.get(1));
+ }
+
+ @Test
+ public void testSplitEmptyValues()
+ {
+ String s = ",,";
+ List<String> list = PropertyConverter.split(s, ',');
+
+ assertEquals("size", 3, list.size());
+ assertEquals("1st token for '" + s + "'", "", list.get(0));
+ assertEquals("2nd token for '" + s + "'", "", list.get(1));
+ assertEquals("3rd token for '" + s + "'", "", list.get(2));
+ }
+
+ @Test
+ public void testSplitWithEndingSlash()
+ {
+ String s = "abc, xyz\\";
+ List<String> list = PropertyConverter.split(s, ',');
+
+ assertEquals("size", 2, list.size());
+ assertEquals("1st token for '" + s + "'", "abc", list.get(0));
+ assertEquals("2nd token for '" + s + "'", "xyz\\", list.get(1));
+ }
+
+ @Test
+ public void testSplitNull()
+ {
+ List<String> list = PropertyConverter.split(null, ',');
+ assertNotNull(list);
+ assertTrue(list.isEmpty());
+ }
+
+ /**
+ * Tests whether an escape character can be itself escaped.
+ */
+ @Test
+ public void testSplitEscapeEscapeChar()
+ {
+ List<String> list = PropertyConverter.split("C:\\Temp\\\\,xyz", ',');
+ assertEquals("Wrong list size", 2, list.size());
+ assertEquals("Wrong element 1", "C:\\Temp\\", list.get(0));
+ assertEquals("Wrong element 2", "xyz", list.get(1));
+ }
+
+ /**
+ * Tests whether delimiters are correctly escaped.
+ */
+ @Test
+ public void testEscapeDelimiters()
+ {
+ assertEquals("Wrong escaped delimiters",
+ "C:\\\\Temp\\\\\\,D:\\\\Data\\\\", PropertyConverter
+ .escapeDelimiters("C:\\Temp\\,D:\\Data\\", ','));
+ }
+
+ /**
+ * Tests whether only the list delimiter can be escaped.
+ */
+ @Test
+ public void testEscapeListDelimiter()
+ {
+ assertEquals("Wrong escaped list delimiter", "C:\\Temp\\\\,D:\\Data\\",
+ PropertyConverter.escapeListDelimiter("C:\\Temp\\,D:\\Data\\",
+ ','));
+ }
+
+ @Test
+ public void testToIterator()
+ {
+ int[] array = new int[]{1, 2, 3};
+
+ Iterator<?> it = PropertyConverter.toIterator(array, ',');
+
+ assertEquals("1st element", new Integer(1), it.next());
+ assertEquals("2nd element", new Integer(2), it.next());
+ assertEquals("3rd element", new Integer(3), it.next());
+ }
+
+ /**
+ * Tests the interpolation features.
+ */
+ @Test
+ public void testInterpolateString()
+ {
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ config.addProperty("animal", "quick brown fox");
+ config.addProperty("target", "lazy dog");
+ assertEquals("Wrong interpolation",
+ "The quick brown fox jumps over the lazy dog.",
+ PropertyConverter.interpolate("The ${animal} jumps over the ${target}.", config));
+ }
+
+ /**
+ * Tests interpolation of an object. Here nothing should be substituted.
+ */
+ @Test
+ public void testInterpolateObject()
+ {
+ assertEquals("Object was not correctly interpolated", new Integer(42),
+ PropertyConverter.interpolate(new Integer(42), new PropertiesConfiguration()));
+ }
+
+ /**
+ * Tests complex interpolation where the variables' values contain in turn
+ * other variables.
+ */
+ @Test
+ public void testInterpolateRecursive()
+ {
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ config.addProperty("animal", "${animal_attr} fox");
+ config.addProperty("target", "${target_attr} dog");
+ config.addProperty("animal_attr", "quick brown");
+ config.addProperty("target_attr", "lazy");
+ assertEquals("Wrong complex interpolation",
+ "The quick brown fox jumps over the lazy dog.",
+ PropertyConverter.interpolate("The ${animal} jumps over the ${target}.", config));
+ }
+
+ /**
+ * Tests an interpolation that leads to a cycle. This should throw an
+ * exception.
+ */
+ @Test(expected = IllegalStateException.class)
+ public void testCyclicInterpolation()
+ {
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ config.addProperty("animal", "${animal_attr} ${species}");
+ config.addProperty("animal_attr", "quick brown");
+ config.addProperty("species", "${animal}");
+ PropertyConverter.interpolate("This is a ${animal}", config);
+ }
+
+ /**
+ * Tests interpolation if a variable is unknown. Then the variable won't be
+ * substituted.
+ */
+ @Test
+ public void testInterpolationUnknownVariable()
+ {
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ config.addProperty("animal", "quick brown fox");
+ assertEquals("Wrong interpolation",
+ "The quick brown fox jumps over ${target}.",
+ PropertyConverter.interpolate("The ${animal} jumps over ${target}.", config));
+ }
+
+ /**
+ * Tests conversion to numbers when the passed in objects are already
+ * numbers.
+ */
+ @Test
+ public void testToNumberDirect()
+ {
+ Integer i = new Integer(42);
+ assertSame("Wrong integer", i, PropertyConverter.toNumber(i, Integer.class));
+ BigDecimal d = new BigDecimal("3.1415");
+ assertSame("Wrong BigDecimal", d, PropertyConverter.toNumber(d, Integer.class));
+ }
+
+ /**
+ * Tests conversion to numbers when the passed in objects have a compatible
+ * string representation.
+ */
+ @Test
+ public void testToNumberFromString()
+ {
+ assertEquals("Incorrect Integer value", new Integer(42), PropertyConverter.toNumber("42", Integer.class));
+ assertEquals("Incorrect Short value", new Short((short) 10), PropertyConverter.toNumber(new StringBuffer("10"), Short.class));
+ }
+
+ /**
+ * Tests conversion to numbers when the passed in objects are strings with
+ * prefixes for special radices.
+ */
+ @Test
+ public void testToNumberFromHexString()
+ {
+ Number n = PropertyConverter.toNumber("0x10", Integer.class);
+ assertEquals("Incorrect Integer value", 16, n.intValue());
+ }
+
+ /**
+ * Tests conversion to numbers when an invalid Hex value is passed in.
+ * This should cause an exception.
+ */
+ @Test(expected = ConversionException.class)
+ public void testToNumberFromInvalidHexString()
+ {
+ PropertyConverter.toNumber("0xNotAHexValue", Integer.class);
+ }
+
+ /**
+ * Tests conversion to numbers when the passed in objects are strings with
+ * prefixes for special radices.
+ */
+ @Test
+ public void testToNumberFromBinaryString()
+ {
+ Number n = PropertyConverter.toNumber("0b1111", Integer.class);
+ assertEquals("Incorrect Integer value", 15, n.intValue());
+ }
+
+ /**
+ * Tests conversion to numbers when an invalid binary value is passed in.
+ * This should cause an exception.
+ */
+ @Test(expected = ConversionException.class)
+ public void testToNumberFromInvalidBinaryString()
+ {
+ PropertyConverter.toNumber("0bNotABinValue", Integer.class);
+ }
+
+ /**
+ * Tests conversion to numbers when the passed in objects have no numeric
+ * String representation. This should cause an exception.
+ */
+ @Test(expected = ConversionException.class)
+ public void testToNumberFromInvalidString()
+ {
+ PropertyConverter.toNumber("Not a number", Byte.class);
+ }
+
+ /**
+ * Tests conversion to numbers when the passed in target class is invalid.
+ * This should cause an exception.
+ */
+ @Test(expected = ConversionException.class)
+ public void testToNumberWithInvalidClass()
+ {
+ PropertyConverter.toNumber("42", Object.class);
+ }
+
+ @Test
+ public void testToEnumFromEnum()
+ {
+ assertEquals(ElementType.METHOD, PropertyConverter.toEnum(ElementType.METHOD, ENUM_CLASS));
+ }
+
+ @Test
+ public void testToEnumFromString()
+ {
+ assertEquals(ElementType.METHOD, PropertyConverter.toEnum("METHOD", ENUM_CLASS));
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testToEnumFromInvalidString()
+ {
+ PropertyConverter.toEnum("FOO", ENUM_CLASS);
+ }
+
+ @Test
+ public void testToEnumFromNumber()
+ {
+ assertEquals(ElementType.METHOD, PropertyConverter.toEnum(
+ Integer.valueOf(ElementType.METHOD.ordinal()),
+ ENUM_CLASS));
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testToEnumFromInvalidNumber()
+ {
+ PropertyConverter.toEnum(Integer.valueOf(-1), ENUM_CLASS);
+ }
+
+ /**
+ * Tests a trivial conversion: the value has already the desired type.
+ */
+ @Test
+ public void testToNoConversionNeeded()
+ {
+ String value = "testValue";
+ assertEquals("Wrong conversion result", value,
+ PropertyConverter.to(String.class, value, null));
+ }
+
+ /**
+ * Tests whether a conversion to character is possible.
+ */
+ @Test
+ public void testToCharSuccess()
+ {
+ assertEquals("Wrong conversion result", Character.valueOf('t'),
+ PropertyConverter.to(Character.class, "t", null));
+ }
+
+ /**
+ * Tests whether other objects implementing a toString() method can be
+ * converted to character.
+ */
+ @Test
+ public void testToCharViaToString()
+ {
+ Object value = new Object()
+ {
+ @Override
+ public String toString()
+ {
+ return "X";
+ }
+ };
+ assertEquals("Wrong conversion result", Character.valueOf('X'),
+ PropertyConverter.to(Character.TYPE, value, null));
+ }
+
+ /**
+ * Tests a failed conversion to character.
+ */
+ @Test(expected = ConversionException.class)
+ public void testToCharFailed()
+ {
+ PropertyConverter.to(Character.TYPE, "FF", null);
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestStrictConfigurationComparator.java b/src/test/java/org/apache/commons/configuration/TestStrictConfigurationComparator.java
new file mode 100644
index 0000000..c341b95
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestStrictConfigurationComparator.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/**
+ * Tests the StrintConfigurationComparator class
+ *
+ * @version $Id: TestStrictConfigurationComparator.java 1225015 2011-12-27 21:01:59Z oheger $
+ */
+public class TestStrictConfigurationComparator
+{
+ /**
+ * The comparator.
+ */
+ protected ConfigurationComparator comparator = new StrictConfigurationComparator();
+
+ /**
+ * The first configuration.
+ */
+ protected Configuration configuration = new BaseConfiguration();
+
+ /**
+ * Tests the comparator.
+ */
+ @Test
+ public void testCompare()
+ {
+ // Identity comparison for empty configuration
+ assertTrue(
+ "Compare an empty configuration with itself",
+ comparator.compare(configuration, configuration));
+
+ configuration.setProperty("one", "1");
+ configuration.setProperty("two", "2");
+ configuration.setProperty("three", "3");
+
+ // Identify comparison for non-empty configuration
+ assertTrue(
+ "Compare a configuration with itself",
+ comparator.compare(configuration, configuration));
+
+ // Create the second configuration
+ Configuration other = new BaseConfiguration();
+ assertFalse(
+ "Compare a configuration with an empty one",
+ comparator.compare(configuration, other));
+
+ other.setProperty("one", "1");
+ other.setProperty("two", "2");
+ other.setProperty("three", "3");
+
+ // Two identical, non-empty configurations
+ assertTrue(
+ "Compare a configuration with an identical one",
+ comparator.compare(configuration, other));
+
+ other.setProperty("four", "4");
+ assertFalse(
+ "Compare our configuration with another that has an additional key mapping",
+ comparator.compare(configuration, other));
+
+ configuration.setProperty("four", "4");
+ assertTrue(
+ "Compare our configuration with another that is identical",
+ comparator.compare(configuration, other));
+ }
+
+ @Test
+ public void testCompareNull()
+ {
+ assertTrue(comparator.compare(null, null));
+ assertFalse(comparator.compare(configuration, null));
+ assertFalse(comparator.compare(null, configuration));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestSubnodeConfiguration.java b/src/test/java/org/apache/commons/configuration/TestSubnodeConfiguration.java
new file mode 100644
index 0000000..1a3c17a
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestSubnodeConfiguration.java
@@ -0,0 +1,625 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.configuration.interpol.ConfigurationInterpolator;
+import org.apache.commons.configuration.reloading.FileAlwaysReloadingStrategy;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
+import org.apache.commons.lang.text.StrLookup;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Test case for SubnodeConfiguration.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestSubnodeConfiguration.java 1225018 2011-12-27 21:14:59Z oheger $
+ */
+public class TestSubnodeConfiguration
+{
+ /** An array with names of tables (test data). */
+ private static final String[] TABLE_NAMES =
+ { "documents", "users" };
+
+ /** An array with the fields of the test tables (test data). */
+ private static final String[][] TABLE_FIELDS =
+ {
+ { "docid", "docname", "author", "dateOfCreation", "version", "size" },
+ { "userid", "uname", "firstName", "lastName" } };
+
+ /** Constant for an updated table name.*/
+ private static final String NEW_TABLE_NAME = "newTable";
+
+ /** A helper object for creating temporary files. */
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ /** The parent configuration. */
+ HierarchicalConfiguration parent;
+
+ /** The subnode configuration to be tested. */
+ SubnodeConfiguration config;
+
+ /** Stores a counter for the created nodes. */
+ int nodeCounter;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ parent = setUpParentConfig();
+ nodeCounter = 0;
+ }
+
+ /**
+ * Tests creation of a subnode config.
+ */
+ @Test
+ public void testInitSubNodeConfig()
+ {
+ setUpSubnodeConfig();
+ assertSame("Wrong root node in subnode", getSubnodeRoot(parent), config
+ .getRoot());
+ assertSame("Wrong parent config", parent, config.getParent());
+ }
+
+ /**
+ * Tests constructing a subnode configuration with a null parent. This
+ * should cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testInitSubNodeConfigWithNullParent()
+ {
+ config = new SubnodeConfiguration(null, getSubnodeRoot(parent));
+ }
+
+ /**
+ * Tests constructing a subnode configuration with a null root node. This
+ * should cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testInitSubNodeConfigWithNullNode()
+ {
+ config = new SubnodeConfiguration(parent, null);
+ }
+
+ /**
+ * Tests if properties of the sub node can be accessed.
+ */
+ @Test
+ public void testGetProperties()
+ {
+ setUpSubnodeConfig();
+ assertEquals("Wrong table name", TABLE_NAMES[0], config
+ .getString("name"));
+ List<Object> fields = config.getList("fields.field.name");
+ assertEquals("Wrong number of fields", TABLE_FIELDS[0].length, fields
+ .size());
+ for (int i = 0; i < TABLE_FIELDS[0].length; i++)
+ {
+ assertEquals("Wrong field at position " + i, TABLE_FIELDS[0][i],
+ fields.get(i));
+ }
+ }
+
+ /**
+ * Tests setting of properties in both the parent and the subnode
+ * configuration and whether the changes are visible to each other.
+ */
+ @Test
+ public void testSetProperty()
+ {
+ setUpSubnodeConfig();
+ config.setProperty(null, "testTable");
+ config.setProperty("name", TABLE_NAMES[0] + "_tested");
+ assertEquals("Root value was not set", "testTable", parent
+ .getString("tables.table(0)"));
+ assertEquals("Table name was not changed", TABLE_NAMES[0] + "_tested",
+ parent.getString("tables.table(0).name"));
+
+ parent.setProperty("tables.table(0).fields.field(1).name", "testField");
+ assertEquals("Field name was not changed", "testField", config
+ .getString("fields.field(1).name"));
+ }
+
+ /**
+ * Tests adding of properties.
+ */
+ @Test
+ public void testAddProperty()
+ {
+ setUpSubnodeConfig();
+ config.addProperty("[@table-type]", "test");
+ assertEquals("parent.createNode() was not called", 1, nodeCounter);
+ assertEquals("Attribute not set", "test", parent
+ .getString("tables.table(0)[@table-type]"));
+
+ parent.addProperty("tables.table(0).fields.field(-1).name", "newField");
+ List<Object> fields = config.getList("fields.field.name");
+ assertEquals("New field was not added", TABLE_FIELDS[0].length + 1,
+ fields.size());
+ assertEquals("Wrong last field", "newField", fields
+ .get(fields.size() - 1));
+ }
+
+ /**
+ * Tests listing the defined keys.
+ */
+ @Test
+ public void testGetKeys()
+ {
+ setUpSubnodeConfig();
+ Set<String> keys = new HashSet<String>();
+ CollectionUtils.addAll(keys, config.getKeys());
+ assertEquals("Incorrect number of keys", 2, keys.size());
+ assertTrue("Key 1 not contained", keys.contains("name"));
+ assertTrue("Key 2 not contained", keys.contains("fields.field.name"));
+ }
+
+ /**
+ * Tests setting the exception on missing flag. The subnode config obtains
+ * this flag from its parent.
+ */
+ @Test(expected = NoSuchElementException.class)
+ public void testSetThrowExceptionOnMissing()
+ {
+ parent.setThrowExceptionOnMissing(true);
+ setUpSubnodeConfig();
+ assertTrue("Exception flag not fetchted from parent", config
+ .isThrowExceptionOnMissing());
+ config.getString("non existing key");
+ }
+
+ /**
+ * Tests whether the exception flag can be set independently from the parent.
+ */
+ @Test
+ public void testSetThrowExceptionOnMissingAffectsParent()
+ {
+ parent.setThrowExceptionOnMissing(true);
+ setUpSubnodeConfig();
+ config.setThrowExceptionOnMissing(false);
+ assertTrue("Exception flag reset on parent", parent
+ .isThrowExceptionOnMissing());
+ }
+
+ /**
+ * Tests handling of the delimiter parsing disabled flag. This is shared
+ * with the parent, too.
+ */
+ @Test
+ public void testSetDelimiterParsingDisabled()
+ {
+ parent.setDelimiterParsingDisabled(true);
+ setUpSubnodeConfig();
+ parent.setDelimiterParsingDisabled(false);
+ assertTrue("Delimiter parsing flag was not received from parent",
+ config.isDelimiterParsingDisabled());
+ config.addProperty("newProp", "test1,test2,test3");
+ assertEquals("New property was splitted", "test1,test2,test3", parent
+ .getString("tables.table(0).newProp"));
+ parent.setDelimiterParsingDisabled(true);
+ config.setDelimiterParsingDisabled(false);
+ assertTrue("Delimiter parsing flag was reset on parent", parent
+ .isDelimiterParsingDisabled());
+ }
+
+ /**
+ * Tests manipulating the list delimiter. This piece of data is derived from
+ * the parent.
+ */
+ @Test
+ public void testSetListDelimiter()
+ {
+ parent.setListDelimiter('/');
+ setUpSubnodeConfig();
+ parent.setListDelimiter(';');
+ assertEquals("List delimiter not obtained from parent", '/', config
+ .getListDelimiter());
+ config.addProperty("newProp", "test1,test2/test3");
+ assertEquals("List was incorrectly splitted", "test1,test2", parent
+ .getString("tables.table(0).newProp"));
+ config.setListDelimiter(',');
+ assertEquals("List delimiter changed on parent", ';', parent
+ .getListDelimiter());
+ }
+
+ /**
+ * Tests changing the expression engine.
+ */
+ @Test
+ public void testSetExpressionEngine()
+ {
+ parent.setExpressionEngine(new XPathExpressionEngine());
+ setUpSubnodeConfig();
+ assertEquals("Wrong field name", TABLE_FIELDS[0][1], config
+ .getString("fields/field[2]/name"));
+ Set<String> keys = new HashSet<String>();
+ CollectionUtils.addAll(keys, config.getKeys());
+ assertEquals("Wrong number of keys", 2, keys.size());
+ assertTrue("Key 1 not contained", keys.contains("name"));
+ assertTrue("Key 2 not contained", keys.contains("fields/field/name"));
+ config.setExpressionEngine(null);
+ assertTrue("Expression engine reset on parent", parent
+ .getExpressionEngine() instanceof XPathExpressionEngine);
+ }
+
+ /**
+ * Tests the configurationAt() method.
+ */
+ @Test
+ public void testConfiguarationAt()
+ {
+ setUpSubnodeConfig();
+ SubnodeConfiguration sub2 = config
+ .configurationAt("fields.field(1)");
+ assertEquals("Wrong value of property", TABLE_FIELDS[0][1], sub2
+ .getString("name"));
+ assertEquals("Wrong parent", config.getParent(), sub2.getParent());
+ }
+
+ /**
+ * Tests interpolation features. The subnode config should use its parent
+ * for interpolation.
+ */
+ @Test
+ public void testInterpolation()
+ {
+ parent.addProperty("tablespaces.tablespace.name", "default");
+ parent.addProperty("tablespaces.tablespace(-1).name", "test");
+ parent.addProperty("tables.table(0).tablespace",
+ "${tablespaces.tablespace(0).name}");
+ assertEquals("Wrong interpolated tablespace", "default", parent
+ .getString("tables.table(0).tablespace"));
+
+ setUpSubnodeConfig();
+ assertEquals("Wrong interpolated tablespace in subnode", "default",
+ config.getString("tablespace"));
+ }
+
+ /**
+ * An additional test for interpolation when the configurationAt() method is
+ * involved.
+ */
+ @Test
+ public void testInterpolationFromConfigurationAt()
+ {
+ parent.addProperty("base.dir", "/home/foo");
+ parent.addProperty("test.absolute.dir.dir1", "${base.dir}/path1");
+ parent.addProperty("test.absolute.dir.dir2", "${base.dir}/path2");
+ parent.addProperty("test.absolute.dir.dir3", "${base.dir}/path3");
+
+ Configuration sub = parent.configurationAt("test.absolute.dir");
+ for (int i = 1; i < 4; i++)
+ {
+ assertEquals("Wrong interpolation in parent", "/home/foo/path" + i,
+ parent.getString("test.absolute.dir.dir" + i));
+ assertEquals("Wrong interpolation in subnode",
+ "/home/foo/path" + i, sub.getString("dir" + i));
+ }
+ }
+
+ /**
+ * An additional test for interpolation when the configurationAt() method is
+ * involved for a local interpolation.
+ */
+ @Test
+ public void testLocalInterpolationFromConfigurationAt()
+ {
+ parent.addProperty("base.dir", "/home/foo");
+ parent.addProperty("test.absolute.dir.dir1", "${base.dir}/path1");
+ parent.addProperty("test.absolute.dir.dir2", "${dir1}");
+
+ Configuration sub = parent.configurationAt("test.absolute.dir");
+ assertEquals("Wrong interpolation in subnode",
+ "/home/foo/path1", sub.getString("dir1"));
+ assertEquals("Wrong local interpolation in subnode",
+ "/home/foo/path1", sub.getString("dir2"));
+ }
+
+ /**
+ * Tests manipulating the interpolator.
+ */
+ @Test
+ public void testInterpolator()
+ {
+ parent.addProperty("tablespaces.tablespace.name", "default");
+ parent.addProperty("tablespaces.tablespace(-1).name", "test");
+
+ setUpSubnodeConfig();
+ InterpolationTestHelper.testGetInterpolator(config);
+ }
+
+ @Test
+ public void testLocalLookupsInInterpolatorAreInherited() {
+ parent.addProperty("tablespaces.tablespace.name", "default");
+ parent.addProperty("tablespaces.tablespace(-1).name", "test");
+ parent.addProperty("tables.table(0).var", "${brackets:x}");
+
+ ConfigurationInterpolator interpolator = parent.getInterpolator();
+ interpolator.registerLookup("brackets", new StrLookup(){
+
+ @Override
+ public String lookup(String key) {
+ return "(" + key +")";
+ }
+
+ });
+ setUpSubnodeConfig();
+ assertEquals("Local lookup was not inherited", "(x)", config.getString("var", ""));
+ }
+
+ /**
+ * Tests a reload operation for the parent configuration when the subnode
+ * configuration does not support reloads. Then the new value should not be
+ * detected.
+ */
+ @Test
+ public void testParentReloadNotSupported() throws ConfigurationException
+ {
+ Configuration c = setUpReloadTest(false);
+ assertEquals("Name changed in sub config", TABLE_NAMES[1], config
+ .getString("name"));
+ assertEquals("Name not changed in parent", NEW_TABLE_NAME, c
+ .getString("tables.table(1).name"));
+ }
+
+ /**
+ * Tests a reload operation for the parent configuration when the subnode
+ * configuration does support reloads. The new value should be returned.
+ */
+ @Test
+ public void testParentReloadSupported() throws ConfigurationException
+ {
+ Configuration c = setUpReloadTest(true);
+ assertEquals("Name not changed in sub config", NEW_TABLE_NAME, config
+ .getString("name"));
+ assertEquals("Name not changed in parent", NEW_TABLE_NAME, c
+ .getString("tables.table(1).name"));
+ }
+
+ /**
+ * Tests whether events are fired if a change of the parent is detected.
+ */
+ @Test
+ public void testParentReloadEvents() throws ConfigurationException
+ {
+ setUpReloadTest(true);
+ ConfigurationListenerTestImpl l = new ConfigurationListenerTestImpl();
+ config.addConfigurationListener(l);
+ config.getString("name");
+ assertEquals("Wrong number of events", 2, l.events.size());
+ boolean before = true;
+ for (ConfigurationEvent e : l.events)
+ {
+ assertEquals("Wrong configuration", config, e.getSource());
+ assertEquals("Wrong event type",
+ HierarchicalConfiguration.EVENT_SUBNODE_CHANGED, e
+ .getType());
+ assertNull("Got a property name", e.getPropertyName());
+ assertNull("Got a property value", e.getPropertyValue());
+ assertEquals("Wrong before flag", before, e.isBeforeUpdate());
+ before = !before;
+ }
+ }
+
+ /**
+ * Tests a reload operation for the parent configuration when the subnode
+ * configuration is aware of reloads, and the parent configuration is
+ * accessed first. The new value should be returned.
+ */
+ @Test
+ public void testParentReloadSupportAccessParent()
+ throws ConfigurationException
+ {
+ Configuration c = setUpReloadTest(true);
+ assertEquals("Name not changed in parent", NEW_TABLE_NAME, c
+ .getString("tables.table(1).name"));
+ assertEquals("Name not changed in sub config", NEW_TABLE_NAME, config
+ .getString("name"));
+ }
+
+ /**
+ * Tests whether reloads work with sub subnode configurations.
+ */
+ @Test
+ public void testParentReloadSubSubnode() throws ConfigurationException
+ {
+ setUpReloadTest(true);
+ SubnodeConfiguration sub = config.configurationAt("fields", true);
+ assertEquals("Wrong subnode key", "tables.table(1).fields", sub
+ .getSubnodeKey());
+ assertEquals("Changed field not detected", "newField", sub
+ .getString("field(0).name"));
+ }
+
+ /**
+ * Tests creating a sub sub config when the sub config is not aware of
+ * changes. Then the sub sub config shouldn't be either.
+ */
+ @Test
+ public void testParentReloadSubSubnodeNoChangeSupport()
+ throws ConfigurationException
+ {
+ setUpReloadTest(false);
+ SubnodeConfiguration sub = config.configurationAt("fields", true);
+ assertNull("Sub sub config is attached to parent", sub.getSubnodeKey());
+ assertEquals("Changed field name returned", TABLE_FIELDS[1][0], sub
+ .getString("field(0).name"));
+ }
+
+ /**
+ * Prepares a test for a reload operation.
+ *
+ * @param supportReload a flag whether the subnode configuration should
+ * support reload operations
+ * @return the parent configuration that can be used for testing
+ * @throws ConfigurationException if an error occurs
+ */
+ private XMLConfiguration setUpReloadTest(boolean supportReload)
+ throws ConfigurationException
+ {
+ try
+ {
+ File testFile = folder.newFile();
+ XMLConfiguration xmlConf = new XMLConfiguration(parent);
+ xmlConf.setFile(testFile);
+ xmlConf.save();
+ config = xmlConf.configurationAt("tables.table(1)", supportReload);
+ assertEquals("Wrong table name", TABLE_NAMES[1],
+ config.getString("name"));
+ xmlConf.setReloadingStrategy(new FileAlwaysReloadingStrategy());
+ // Now change the configuration file
+ XMLConfiguration confUpdate = new XMLConfiguration(testFile);
+ confUpdate.setProperty("tables.table(1).name", NEW_TABLE_NAME);
+ confUpdate.setProperty("tables.table(1).fields.field(0).name",
+ "newField");
+ confUpdate.save();
+ return xmlConf;
+ }
+ catch (IOException ioex)
+ {
+ throw new ConfigurationException(ioex);
+ }
+ }
+
+ /**
+ * Tests a manipulation of the parent configuration that causes the subnode
+ * configuration to become invalid. In this case the sub config should be
+ * detached and keep its old values.
+ */
+ @Test
+ public void testParentChangeDetach()
+ {
+ final String key = "tables.table(1)";
+ config = parent.configurationAt(key, true);
+ assertEquals("Wrong subnode key", key, config.getSubnodeKey());
+ assertEquals("Wrong table name", TABLE_NAMES[1], config
+ .getString("name"));
+ parent.clearTree(key);
+ assertEquals("Wrong table name after change", TABLE_NAMES[1], config
+ .getString("name"));
+ assertNull("Sub config was not detached", config.getSubnodeKey());
+ }
+
+ /**
+ * Tests detaching a subnode configuration when an exception is thrown
+ * during reconstruction. This can happen e.g. if the expression engine is
+ * changed for the parent.
+ */
+ @Test
+ public void testParentChangeDetatchException()
+ {
+ config = parent.configurationAt("tables.table(1)", true);
+ parent.setExpressionEngine(new XPathExpressionEngine());
+ assertEquals("Wrong name of table", TABLE_NAMES[1], config
+ .getString("name"));
+ assertNull("Sub config was not detached", config.getSubnodeKey());
+ }
+
+ /**
+ * Initializes the parent configuration. This method creates the typical
+ * structure of tables and fields nodes.
+ *
+ * @return the parent configuration
+ */
+ protected HierarchicalConfiguration setUpParentConfig()
+ {
+ HierarchicalConfiguration conf = new HierarchicalConfiguration()
+ {
+ /**
+ * Serial version UID.
+ */
+ private static final long serialVersionUID = 1L;
+
+ // Provide a special implementation of createNode() to check
+ // if it is called by the subnode config
+ @Override
+ protected Node createNode(String name)
+ {
+ nodeCounter++;
+ return super.createNode(name);
+ }
+ };
+ for (int i = 0; i < TABLE_NAMES.length; i++)
+ {
+ conf.addProperty("tables.table(-1).name", TABLE_NAMES[i]);
+ for (int j = 0; j < TABLE_FIELDS[i].length; j++)
+ {
+ conf.addProperty("tables.table.fields.field(-1).name",
+ TABLE_FIELDS[i][j]);
+ }
+ }
+ return conf;
+ }
+
+ /**
+ * Returns the root node for the subnode config. This method returns the
+ * first table node.
+ *
+ * @param conf the parent config
+ * @return the root node for the subnode config
+ */
+ protected ConfigurationNode getSubnodeRoot(HierarchicalConfiguration conf)
+ {
+ ConfigurationNode root = conf.getRoot();
+ return root.getChild(0).getChild(0);
+ }
+
+ /**
+ * Performs a standard initialization of the subnode config to test.
+ */
+ protected void setUpSubnodeConfig()
+ {
+ config = new SubnodeConfiguration(parent, getSubnodeRoot(parent));
+ }
+
+ /**
+ * A specialized configuration listener for testing whether the expected
+ * events are fired.
+ */
+ private static class ConfigurationListenerTestImpl implements ConfigurationListener
+ {
+ /** Stores the events received.*/
+ final List<ConfigurationEvent> events = new ArrayList<ConfigurationEvent>();
+
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ events.add(event);
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestSubsetConfiguration.java b/src/test/java/org/apache/commons/configuration/TestSubsetConfiguration.java
new file mode 100644
index 0000000..9365366
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestSubsetConfiguration.java
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import org.apache.commons.configuration.interpol.ConfigurationInterpolator;
+import org.apache.commons.lang.text.StrLookup;
+import org.junit.Test;
+
+/**
+ * Test case for the {@link SubsetConfiguration} class.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: TestSubsetConfiguration.java 1225019 2011-12-27 21:18:54Z oheger $
+ */
+public class TestSubsetConfiguration
+{
+ static final String TEST_DIR = ConfigurationAssert.TEST_DIR_NAME;
+ static final String TEST_FILE = "testDigesterConfiguration2.xml";
+
+ @Test
+ public void testGetProperty()
+ {
+ Configuration conf = new BaseConfiguration();
+ conf.setProperty("test.key1", "value1");
+ conf.setProperty("testing.key2", "value1");
+
+ Configuration subset = new SubsetConfiguration(conf, "test", ".");
+ assertFalse("the subset is empty", subset.isEmpty());
+ assertTrue("'key1' not found in the subset", subset.containsKey("key1"));
+ assertFalse("'ng.key2' found in the subset", subset.containsKey("ng.key2"));
+ }
+
+ @Test
+ public void testSetProperty()
+ {
+ Configuration conf = new BaseConfiguration();
+ Configuration subset = new SubsetConfiguration(conf, "test", ".");
+
+ // set a property in the subset and check the parent
+ subset.setProperty("key1", "value1");
+ assertEquals("key1 in the subset configuration", "value1", subset.getProperty("key1"));
+ assertEquals("test.key1 in the parent configuration", "value1", conf.getProperty("test.key1"));
+
+ // set a property in the parent and check in the subset
+ conf.setProperty("test.key2", "value2");
+ assertEquals("test.key2 in the parent configuration", "value2", conf.getProperty("test.key2"));
+ assertEquals("key2 in the subset configuration", "value2", subset.getProperty("key2"));
+ }
+
+ @Test
+ public void testGetParentKey()
+ {
+ // subset with delimiter
+ SubsetConfiguration subset = new SubsetConfiguration(null, "prefix", ".");
+ assertEquals("parent key for \"key\"", "prefix.key", subset.getParentKey("key"));
+ assertEquals("parent key for \"\"", "prefix", subset.getParentKey(""));
+
+ // subset without delimiter
+ subset = new SubsetConfiguration(null, "prefix", null);
+ assertEquals("parent key for \"key\"", "prefixkey", subset.getParentKey("key"));
+ assertEquals("parent key for \"\"", "prefix", subset.getParentKey(""));
+ }
+
+ @Test
+ public void testGetChildKey()
+ {
+ // subset with delimiter
+ SubsetConfiguration subset = new SubsetConfiguration(null, "prefix", ".");
+ assertEquals("parent key for \"prefixkey\"", "key", subset.getChildKey("prefix.key"));
+ assertEquals("parent key for \"prefix\"", "", subset.getChildKey("prefix"));
+
+ // subset without delimiter
+ subset = new SubsetConfiguration(null, "prefix", null);
+ assertEquals("parent key for \"prefixkey\"", "key", subset.getChildKey("prefixkey"));
+ assertEquals("parent key for \"prefix\"", "", subset.getChildKey("prefix"));
+ }
+
+ @Test
+ public void testGetKeys()
+ {
+ Configuration conf = new BaseConfiguration();
+ conf.setProperty("test", "value0");
+ conf.setProperty("test.key1", "value1");
+ conf.setProperty("testing.key2", "value1");
+
+ Configuration subset = new SubsetConfiguration(conf, "test", ".");
+
+ Iterator<String> it = subset.getKeys();
+ assertEquals("1st key", "", it.next());
+ assertEquals("2nd key", "key1", it.next());
+ assertFalse("too many elements", it.hasNext());
+ }
+
+ @Test
+ public void testGetKeysWithPrefix()
+ {
+ Configuration conf = new BaseConfiguration();
+ conf.setProperty("test.abc", "value0");
+ conf.setProperty("test.abc.key1", "value1");
+ conf.setProperty("test.abcdef.key2", "value1");
+
+ Configuration subset = new SubsetConfiguration(conf, "test", ".");
+
+ Iterator<String> it = subset.getKeys("abc");
+ assertEquals("1st key", "abc", it.next());
+ assertEquals("2nd key", "abc.key1", it.next());
+ assertFalse("too many elements", it.hasNext());
+ }
+
+ @Test
+ public void testGetList()
+ {
+ Configuration conf = new BaseConfiguration();
+ conf.setProperty("test.abc", "value0,value1");
+ conf.addProperty("test.abc", "value3");
+
+ Configuration subset = new SubsetConfiguration(conf, "test", ".");
+ List<Object> list = subset.getList("abc", new ArrayList<Object>());
+ assertEquals(3, list.size());
+ }
+
+ @Test
+ public void testGetParent()
+ {
+ Configuration conf = new BaseConfiguration();
+ SubsetConfiguration subset = new SubsetConfiguration(conf, "prefix", ".");
+
+ assertEquals("parent", conf, subset.getParent());
+ }
+
+ @Test
+ public void testGetPrefix()
+ {
+ Configuration conf = new BaseConfiguration();
+ SubsetConfiguration subset = new SubsetConfiguration(conf, "prefix", ".");
+
+ assertEquals("prefix", "prefix", subset.getPrefix());
+ }
+
+ @Test
+ public void testSetPrefix()
+ {
+ Configuration conf = new BaseConfiguration();
+ SubsetConfiguration subset = new SubsetConfiguration(conf, null, ".");
+ subset.setPrefix("prefix");
+
+ assertEquals("prefix", "prefix", subset.getPrefix());
+ }
+
+ @Test
+ public void testThrowExceptionOnMissing()
+ {
+ BaseConfiguration config = new BaseConfiguration();
+ config.setThrowExceptionOnMissing(true);
+
+ SubsetConfiguration subset = new SubsetConfiguration(config, "prefix");
+
+ try
+ {
+ subset.getString("foo");
+ fail("NoSuchElementException expected");
+ }
+ catch (NoSuchElementException e)
+ {
+ // expected
+ }
+
+ config.setThrowExceptionOnMissing(false);
+ assertNull(subset.getString("foo"));
+
+
+ subset.setThrowExceptionOnMissing(true);
+ try
+ {
+ config.getString("foo");
+ fail("NoSuchElementException expected");
+ }
+ catch (NoSuchElementException e)
+ {
+ // expected
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testNested() throws Exception
+ {
+ ConfigurationFactory factory = new ConfigurationFactory();
+ File src = new File(new File(TEST_DIR), TEST_FILE);
+ factory.setConfigurationURL(src.toURL());
+ Configuration config = factory.getConfiguration();
+ Configuration subConf = config.subset("tables.table(0)");
+ assertTrue(subConf.getKeys().hasNext());
+ Configuration subSubConf = subConf.subset("fields.field(1)");
+ Iterator<String> itKeys = subSubConf.getKeys();
+ Set<String> keys = new HashSet<String>();
+ keys.add("name");
+ keys.add("type");
+ while(itKeys.hasNext())
+ {
+ String k = itKeys.next();
+ assertTrue(keys.contains(k));
+ keys.remove(k);
+ }
+ assertTrue(keys.isEmpty());
+ }
+
+ @Test
+ public void testClear()
+ {
+ Configuration config = new BaseConfiguration();
+ config.setProperty("test.key1", "value1");
+ config.setProperty("testing.key2", "value1");
+
+ Configuration subset = config.subset("test");
+ subset.clear();
+
+ assertTrue("the subset is not empty", subset.isEmpty());
+ assertFalse("the parent configuration is empty", config.isEmpty());
+ }
+
+ @Test
+ public void testSetListDelimiter()
+ {
+ BaseConfiguration config = new BaseConfiguration();
+ Configuration subset = config.subset("prefix");
+ config.setListDelimiter('/');
+ subset.addProperty("list", "a/b/c");
+ assertEquals("Wrong size of list", 3, config.getList("prefix.list")
+ .size());
+
+ ((AbstractConfiguration) subset).setListDelimiter(';');
+ subset.addProperty("list2", "a;b;c");
+ assertEquals("Wrong size of list2", 3, config.getList("prefix.list2")
+ .size());
+ }
+
+ @Test
+ public void testGetListDelimiter()
+ {
+ BaseConfiguration config = new BaseConfiguration();
+ AbstractConfiguration subset = (AbstractConfiguration) config
+ .subset("prefix");
+ config.setListDelimiter('/');
+ assertEquals("Wrong list delimiter in subset", '/', subset
+ .getListDelimiter());
+ subset.setListDelimiter(';');
+ assertEquals("Wrong list delimiter in parent", ';', config
+ .getListDelimiter());
+ }
+
+ @Test
+ public void testSetDelimiterParsingDisabled()
+ {
+ BaseConfiguration config = new BaseConfiguration();
+ Configuration subset = config.subset("prefix");
+ config.setDelimiterParsingDisabled(true);
+ subset.addProperty("list", "a,b,c");
+ assertEquals("Wrong value of property", "a,b,c", config
+ .getString("prefix.list"));
+
+ ((AbstractConfiguration) subset).setDelimiterParsingDisabled(false);
+ subset.addProperty("list2", "a,b,c");
+ assertEquals("Wrong size of list2", 3, config.getList("prefix.list2")
+ .size());
+ }
+
+ @Test
+ public void testIsDelimiterParsingDisabled()
+ {
+ BaseConfiguration config = new BaseConfiguration();
+ AbstractConfiguration subset = (AbstractConfiguration) config
+ .subset("prefix");
+ config.setDelimiterParsingDisabled(true);
+ assertTrue("Wrong value of list parsing flag in subset", subset
+ .isDelimiterParsingDisabled());
+ subset.setDelimiterParsingDisabled(false);
+ assertFalse("Wrong value of list parsing flag in parent", config
+ .isDelimiterParsingDisabled());
+ }
+
+ /**
+ * Tests manipulating the interpolator.
+ */
+ @Test
+ public void testInterpolator()
+ {
+ BaseConfiguration config = new BaseConfiguration();
+ AbstractConfiguration subset = (AbstractConfiguration) config
+ .subset("prefix");
+ InterpolationTestHelper.testGetInterpolator(subset);
+ }
+
+ @Test
+ public void testLocalLookupsInInterpolatorAreInherited() {
+ BaseConfiguration config = new BaseConfiguration();
+ ConfigurationInterpolator interpolator = config.getInterpolator();
+ interpolator.registerLookup("brackets", new StrLookup(){
+
+ @Override
+ public String lookup(String key) {
+ return "(" + key +")";
+ }
+
+ });
+ config.setProperty("prefix.var", "${brackets:x}");
+ AbstractConfiguration subset = (AbstractConfiguration) config
+ .subset("prefix");
+ assertEquals("Local lookup was not inherited", "(x)", subset
+ .getString("var", ""));
+ }
+
+ @Test
+ public void testInterpolationForKeysOfTheParent() {
+ BaseConfiguration config = new BaseConfiguration();
+ config.setProperty("test", "junit");
+ config.setProperty("prefix.key", "${test}");
+ AbstractConfiguration subset = (AbstractConfiguration) config
+ .subset("prefix");
+ assertEquals("Interpolation does not resolve parent keys", "junit",
+ subset.getString("key", ""));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestSystemConfiguration.java b/src/test/java/org/apache/commons/configuration/TestSystemConfiguration.java
new file mode 100644
index 0000000..245dc50
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestSystemConfiguration.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Properties;
+
+import org.junit.Test;
+
+/**
+ * Tests for {@code SystemConfiguration}.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: TestSystemConfiguration.java 1225020 2011-12-27 21:20:45Z oheger $
+ */
+public class TestSystemConfiguration
+{
+ @Test
+ public void testSystemConfiguration()
+ {
+ Properties props = System.getProperties();
+ props.put("test.number", "123");
+
+ Configuration conf = new SystemConfiguration();
+ assertEquals("number", 123, conf.getInt("test.number"));
+ }
+
+ @Test
+ public void testSetSystemProperties()
+ {
+ PropertiesConfiguration props = new PropertiesConfiguration();
+ props.addProperty("test.name", "Apache");
+ SystemConfiguration.setSystemProperties(props);
+ assertEquals("System Properties", "Apache", System.getProperty("test.name"));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestSystemConfigurationRegression.java b/src/test/java/org/apache/commons/configuration/TestSystemConfigurationRegression.java
new file mode 100644
index 0000000..5d59b86
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestSystemConfigurationRegression.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import org.junit.Test;
+import org.junit.Assert;
+
+import java.util.UUID;
+
+public class TestSystemConfigurationRegression
+{
+ @Test
+ public void testSystemPropertiesRegression()
+ {
+ SystemConfiguration sc = new SystemConfiguration();
+ int oldSize = sc.getMap().size();
+ System.setProperty(UUID.randomUUID().toString(), "x");
+ Assert.assertEquals(oldSize + 1, sc.getMap().size());
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestThreesomeConfiguration.java b/src/test/java/org/apache/commons/configuration/TestThreesomeConfiguration.java
new file mode 100644
index 0000000..5f5311d
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestThreesomeConfiguration.java
@@ -0,0 +1,67 @@
+package org.apache.commons.configuration;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * A base class for testing {@link
+ * org.apache.commons.configuration.BasePropertiesConfiguration}
+ * extensions.
+ *
+ * @version $Id: TestThreesomeConfiguration.java 1225021 2011-12-27 21:23:22Z oheger $
+ */
+public class TestThreesomeConfiguration
+{
+ protected Configuration conf;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ conf = new PropertiesConfiguration("threesome.properties");
+ }
+
+ @Test
+ public void testList1() throws Exception
+ {
+ List<Object> packages = conf.getList("test.threesome.one");
+ // we should get 3 packages here
+ assertEquals(3, packages.size());
+ }
+
+ @Test
+ public void testList2() throws Exception
+ {
+ List<Object> packages = conf.getList("test.threesome.two");
+ // we should get 3 packages here
+ assertEquals(3, packages.size());
+ }
+
+ @Test
+ public void testList3() throws Exception
+ {
+ List<Object> packages = conf.getList("test.threesome.three");
+ // we should get 3 packages here
+ assertEquals(3, packages.size());
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestVFSConfigurationBuilder.java b/src/test/java/org/apache/commons/configuration/TestVFSConfigurationBuilder.java
new file mode 100644
index 0000000..768e2e8
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestVFSConfigurationBuilder.java
@@ -0,0 +1,1200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.configuration.beanutils.BeanHelper;
+import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;
+import org.apache.commons.configuration.tree.DefaultConfigurationNode;
+import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for VFSConfigurationBuilder.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestVFSConfigurationBuilder.java 1308148 2012-04-01 16:27:41Z rgoers $
+ */
+public class TestVFSConfigurationBuilder
+{
+ /** Test configuration definition file. */
+ private static final File TEST_FILE = ConfigurationAssert
+ .getTestFile("testDigesterConfiguration.xml");
+
+ private static final File ADDITIONAL_FILE = ConfigurationAssert
+ .getTestFile("testDigesterConfiguration2.xml");
+
+ private static final File OPTIONAL_FILE = ConfigurationAssert
+ .getTestFile("testDigesterOptionalConfiguration.xml");
+
+ private static final File OPTIONALEX_FILE = ConfigurationAssert
+ .getTestFile("testDigesterOptionalConfigurationEx.xml");
+
+ private static final File MULTI_FILE = ConfigurationAssert
+ .getTestFile("testDigesterConfiguration3.xml");
+
+ private static final File INIT_FILE = ConfigurationAssert
+ .getTestFile("testComplexInitialization.xml");
+
+ private static final File CLASS_FILE = ConfigurationAssert
+ .getTestFile("testExtendedClass.xml");
+
+ private static final File PROVIDER_FILE = ConfigurationAssert
+ .getTestFile("testConfigurationProvider.xml");
+
+ private static final File EXTENDED_PROVIDER_FILE = ConfigurationAssert
+ .getTestFile("testExtendedXMLConfigurationProvider.xml");
+
+ private static final File GLOBAL_LOOKUP_FILE = ConfigurationAssert
+ .getTestFile("testGlobalLookup.xml");
+
+ private static final File SYSTEM_PROPS_FILE = ConfigurationAssert
+ .getTestFile("testSystemProperties.xml");
+
+ private static final File VALIDATION_FILE = ConfigurationAssert
+ .getTestFile("testValidation.xml");
+
+ private static final File VALIDATION2_FILE = ConfigurationAssert
+ .getTestFile("testValidation2.xml");
+
+ private static final File MULTI_TENENT_FILE = ConfigurationAssert
+ .getTestFile("testMultiTenentConfigurationBuilder.xml");
+
+ private static final File FILESYSTEM_FILE = ConfigurationAssert
+ .getTestFile("testFileSystem.xml");
+
+ private static final File FILERELOAD_FILE = ConfigurationAssert
+ .getTestFile("testFileReloadConfigurationBuilder.xml");
+
+ private static final File MULTI_RELOAD_FILE1 = ConfigurationAssert
+ .getTestFile("testVFSMultiTenentConfigurationBuilder1.xml");
+
+ private static final File MULTI_RELOAD_FILE2 = ConfigurationAssert
+ .getTestFile("testVFSMultiTenentConfigurationBuilder2.xml");
+
+ /** Constant for the name of an optional configuration.*/
+ private static final String OPTIONAL_NAME = "optionalConfig";
+
+ /** Stores the object to be tested. */
+ DefaultConfigurationBuilder factory;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ System
+ .setProperty("java.naming.factory.initial",
+ "org.apache.commons.configuration.MockInitialContextFactory");
+ System.setProperty("test_file_xml", "test.xml");
+ System.setProperty("test_file_combine", "testcombine1.xml");
+ System.setProperty("basePath", "file://" + System.getProperty("user.dir") + "/target/test-classes");
+ FileSystem.setDefaultFileSystem(new VFSFileSystem());
+ factory = new DefaultConfigurationBuilder();
+ factory.clearErrorListeners(); // avoid exception messages
+ }
+
+ @After
+ public void tearDown() throws Exception
+ {
+ FileSystem.resetDefaultFileSystem();
+ }
+
+ /**
+ * Tests the isReservedNode() method of ConfigurationDeclaration.
+ */
+ @Test
+ public void testConfigurationDeclarationIsReserved()
+ {
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory);
+ DefaultConfigurationNode parent = new DefaultConfigurationNode();
+ DefaultConfigurationNode nd = new DefaultConfigurationNode("at");
+ parent.addAttribute(nd);
+ assertTrue("Attribute at not recognized", decl.isReservedNode(nd));
+ nd = new DefaultConfigurationNode("optional");
+ parent.addAttribute(nd);
+ assertTrue("Attribute optional not recognized", decl.isReservedNode(nd));
+ nd = new DefaultConfigurationNode("config-class");
+ parent.addAttribute(nd);
+ assertTrue("Inherited attribute not recognized", decl
+ .isReservedNode(nd));
+ nd = new DefaultConfigurationNode("different");
+ parent.addAttribute(nd);
+ assertFalse("Wrong reserved attribute", decl.isReservedNode(nd));
+ nd = new DefaultConfigurationNode("at");
+ parent.addChild(nd);
+ assertFalse("Node type not evaluated", decl.isReservedNode(nd));
+ }
+
+ /**
+ * Tests if the at attribute is correctly detected as reserved attribute.
+ */
+ @Test
+ public void testConfigurationDeclarationIsReservedAt()
+ {
+ checkOldReservedAttribute("at");
+ }
+
+ /**
+ * Tests if the optional attribute is correctly detected as reserved
+ * attribute.
+ */
+ @Test
+ public void testConfigurationDeclarationIsReservedOptional()
+ {
+ checkOldReservedAttribute("optional");
+ }
+
+ /**
+ * Tests if special reserved attributes are recognized by the
+ * isReservedNode() method. For compatibility reasons the attributes "at"
+ * and "optional" are also treated as reserved attributes, but only if there
+ * are no corresponding attributes with the "config-" prefix.
+ *
+ * @param name the attribute name
+ */
+ private void checkOldReservedAttribute(String name)
+ {
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory);
+ DefaultConfigurationNode parent = new DefaultConfigurationNode();
+ DefaultConfigurationNode nd = new DefaultConfigurationNode("config-"
+ + name);
+ parent.addAttribute(nd);
+ assertTrue("config-" + name + " attribute not recognized", decl
+ .isReservedNode(nd));
+ DefaultConfigurationNode nd2 = new DefaultConfigurationNode(name);
+ parent.addAttribute(nd2);
+ assertFalse(name + " is reserved though config- exists", decl
+ .isReservedNode(nd2));
+ assertTrue("config- attribute not recognized when " + name + " exists",
+ decl.isReservedNode(nd));
+ }
+
+ /**
+ * Tests access to certain reserved attributes of a
+ * ConfigurationDeclaration.
+ */
+ @Test
+ public void testConfigurationDeclarationGetAttributes()
+ {
+ factory.addProperty("xml.fileName", "test.xml");
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory.configurationAt("xml"));
+ assertNull("Found an at attribute", decl.getAt());
+ assertFalse("Found an optional attribute", decl.isOptional());
+ factory.addProperty("xml[@config-at]", "test1");
+ assertEquals("Wrong value of at attribute", "test1", decl.getAt());
+ factory.addProperty("xml[@at]", "test2");
+ assertEquals("Wrong value of config-at attribute", "test1", decl.getAt());
+ factory.clearProperty("xml[@config-at]");
+ assertEquals("Old at attribute not detected", "test2", decl.getAt());
+ factory.addProperty("xml[@config-optional]", "true");
+ assertTrue("Wrong value of optional attribute", decl.isOptional());
+ factory.addProperty("xml[@optional]", "false");
+ assertTrue("Wrong value of config-optional attribute", decl.isOptional());
+ factory.clearProperty("xml[@config-optional]");
+ factory.setProperty("xml[@optional]", Boolean.TRUE);
+ assertTrue("Old optional attribute not detected", decl.isOptional());
+ }
+
+ /**
+ * Tests whether an invalid value of an optional attribute is detected.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testConfigurationDeclarationOptionalAttributeInvalid()
+ {
+ factory.addProperty("xml.fileName", "test.xml");
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory.configurationAt("xml"));
+ factory.setProperty("xml[@optional]", "invalid value");
+ decl.isOptional();
+ }
+
+ /**
+ * Tests adding a new configuration provider.
+ */
+ @Test
+ public void testAddConfigurationProvider()
+ {
+ DefaultConfigurationBuilder.ConfigurationProvider provider = new DefaultConfigurationBuilder.ConfigurationProvider();
+ assertNull("Provider already registered", factory
+ .providerForTag("test"));
+ factory.addConfigurationProvider("test", provider);
+ assertSame("Provider not registered", provider, factory
+ .providerForTag("test"));
+ }
+
+ /**
+ * Tries to register a null configuration provider. This should cause an
+ * exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddConfigurationProviderNull()
+ {
+ factory.addConfigurationProvider("test", null);
+ }
+
+ /**
+ * Tries to register a configuration provider for a null tag. This should
+ * cause an exception to be thrown.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddConfigurationProviderNullTag()
+ {
+ factory.addConfigurationProvider(null,
+ new DefaultConfigurationBuilder.ConfigurationProvider());
+ }
+
+ /**
+ * Tests removing configuration providers.
+ */
+ @Test
+ public void testRemoveConfigurationProvider()
+ {
+ assertNull("Removing unknown provider", factory
+ .removeConfigurationProvider("test"));
+ assertNull("Removing provider for null tag", factory
+ .removeConfigurationProvider(null));
+ DefaultConfigurationBuilder.ConfigurationProvider provider = new DefaultConfigurationBuilder.ConfigurationProvider();
+ factory.addConfigurationProvider("test", provider);
+ assertSame("Failed to remove provider", provider, factory
+ .removeConfigurationProvider("test"));
+ assertNull("Provider still registered", factory.providerForTag("test"));
+ }
+
+ /**
+ * Tests creating a configuration object from a configuration declaration.
+ */
+ @Test
+ public void testConfigurationBeanFactoryCreateBean()
+ {
+ factory.addConfigurationProvider("test",
+ new DefaultConfigurationBuilder.ConfigurationProvider(
+ PropertiesConfiguration.class));
+ factory.addProperty("test[@throwExceptionOnMissing]", "true");
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory.configurationAt("test"));
+ PropertiesConfiguration conf = (PropertiesConfiguration) BeanHelper
+ .createBean(decl);
+ assertTrue("Property was not initialized", conf
+ .isThrowExceptionOnMissing());
+ }
+
+ /**
+ * Tests creating a configuration object from an unknown tag. This should
+ * cause an exception.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testConfigurationBeanFactoryCreateUnknownTag()
+ {
+ factory.addProperty("test[@throwExceptionOnMissing]", "true");
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory.configurationAt("test"));
+ BeanHelper.createBean(decl);
+ }
+
+ /**
+ * Tests loading a simple configuration definition file.
+ */
+ @Test
+ public void testLoadConfiguration() throws ConfigurationException
+ {
+ factory.setFile(TEST_FILE);
+ checkConfiguration();
+ }
+
+ /**
+ * Tests the file constructor.
+ */
+ @Test
+ public void testLoadConfigurationFromFile() throws ConfigurationException
+ {
+ factory = new DefaultConfigurationBuilder(TEST_FILE);
+ checkConfiguration();
+ }
+
+ /**
+ * Tests the file name constructor.
+ */
+ @Test
+ public void testLoadConfigurationFromFileName()
+ throws ConfigurationException
+ {
+ factory = new DefaultConfigurationBuilder(TEST_FILE.getAbsolutePath());
+ checkConfiguration();
+ }
+
+ /**
+ * Tests the URL constructor.
+ */
+ @Test
+ public void testLoadConfigurationFromURL() throws Exception
+ {
+ factory = new DefaultConfigurationBuilder(TEST_FILE.toURI().toURL());
+ checkConfiguration();
+ }
+
+ /**
+ * Tests if the configuration was correctly created by the factory.
+ */
+ private void checkConfiguration() throws ConfigurationException
+ {
+ CombinedConfiguration compositeConfiguration = (CombinedConfiguration) factory
+ .getConfiguration();
+
+ assertEquals("Number of configurations", 3, compositeConfiguration
+ .getNumberOfConfigurations());
+ assertEquals(PropertiesConfiguration.class, compositeConfiguration
+ .getConfiguration(0).getClass());
+ assertEquals(XMLPropertiesConfiguration.class, compositeConfiguration
+ .getConfiguration(1).getClass());
+ assertEquals(XMLConfiguration.class, compositeConfiguration
+ .getConfiguration(2).getClass());
+
+ // check the first configuration
+ PropertiesConfiguration pc = (PropertiesConfiguration) compositeConfiguration
+ .getConfiguration(0);
+ assertNotNull("Make sure we have a fileName: " + pc.getFileName(), pc
+ .getFileName());
+
+ // check some properties
+ checkProperties(compositeConfiguration);
+ }
+
+ /**
+ * Checks if the passed in configuration contains the expected properties.
+ *
+ * @param compositeConfiguration the configuration to check
+ */
+ private void checkProperties(Configuration compositeConfiguration)
+ {
+ assertTrue("Make sure we have loaded our key", compositeConfiguration
+ .getBoolean("test.boolean"));
+ assertEquals("I'm complex!", compositeConfiguration
+ .getProperty("element2.subelement.subsubelement"));
+ assertEquals("property in the XMLPropertiesConfiguration", "value1",
+ compositeConfiguration.getProperty("key1"));
+ }
+
+ /**
+ * Tests loading a configuration definition file with an additional section.
+ */
+ @Test
+ public void testLoadAdditional() throws ConfigurationException
+ {
+ factory.setFile(ADDITIONAL_FILE);
+ CombinedConfiguration compositeConfiguration = (CombinedConfiguration) factory
+ .getConfiguration();
+ assertEquals("Verify how many configs", 2, compositeConfiguration
+ .getNumberOfConfigurations());
+
+ // Test if union was constructed correctly
+ Object prop = compositeConfiguration.getProperty("tables.table.name");
+ assertTrue(prop instanceof Collection);
+ assertEquals(3, ((Collection<?>) prop).size());
+ assertEquals("users", compositeConfiguration
+ .getProperty("tables.table(0).name"));
+ assertEquals("documents", compositeConfiguration
+ .getProperty("tables.table(1).name"));
+ assertEquals("tasks", compositeConfiguration
+ .getProperty("tables.table(2).name"));
+
+ prop = compositeConfiguration
+ .getProperty("tables.table.fields.field.name");
+ assertTrue(prop instanceof Collection);
+ assertEquals(17, ((Collection<?>) prop).size());
+
+ assertEquals("smtp.mydomain.org", compositeConfiguration
+ .getString("mail.host.smtp"));
+ assertEquals("pop3.mydomain.org", compositeConfiguration
+ .getString("mail.host.pop"));
+
+ // This was overridden
+ assertEquals("masterOfPost", compositeConfiguration
+ .getString("mail.account.user"));
+ assertEquals("topsecret", compositeConfiguration
+ .getString("mail.account.psswd"));
+
+ // This was overridden, too, but not in additional section
+ assertEquals("enhanced factory", compositeConfiguration
+ .getString("test.configuration"));
+ }
+
+ /**
+ * Tests whether a default log error listener is registered at the builder
+ * instance.
+ */
+ @Test
+ public void testLogErrorListener()
+ {
+ assertEquals("No default error listener registered", 1,
+ new DefaultConfigurationBuilder().getErrorListeners().size());
+ }
+
+ /**
+ * Tests loading a definition file that contains optional configurations.
+ */
+ @Test
+ public void testLoadOptional() throws Exception
+ {
+ factory.setURL(OPTIONAL_FILE.toURI().toURL());
+ Configuration config = factory.getConfiguration();
+ assertTrue(config.getBoolean("test.boolean"));
+ assertEquals("value", config.getProperty("element"));
+ }
+
+ /**
+ * Tests whether loading a failing optional configuration causes an error
+ * event.
+ */
+ @Test
+ public void testLoadOptionalErrorEvent() throws Exception
+ {
+ factory.clearErrorListeners();
+ ConfigurationErrorListenerImpl listener = new ConfigurationErrorListenerImpl();
+ factory.addErrorListener(listener);
+ prepareOptionalTest("configuration", false);
+ listener.verify(DefaultConfigurationBuilder.EVENT_ERR_LOAD_OPTIONAL,
+ OPTIONAL_NAME, null);
+ }
+
+ /**
+ * Tests loading a definition file with optional and non optional
+ * configuration sources. One non optional does not exist, so this should
+ * cause an exception.
+ */
+ @Test(expected = ConfigurationException.class)
+ public void testLoadOptionalWithException() throws ConfigurationException
+ {
+ factory.setFile(OPTIONALEX_FILE);
+ factory.getConfiguration();
+ }
+
+ /**
+ * Tries to load a configuration file with an optional, non file-based
+ * configuration. The optional attribute should work for other configuration
+ * classes, too.
+ */
+ @Test
+ public void testLoadOptionalNonFileBased() throws ConfigurationException
+ {
+ CombinedConfiguration config = prepareOptionalTest("configuration", false);
+ assertTrue("Configuration not empty", config.isEmpty());
+ assertEquals("Wrong number of configurations", 0, config
+ .getNumberOfConfigurations());
+ }
+
+ /**
+ * Tests an optional, non existing configuration with the forceCreate
+ * attribute. This configuration should be added to the resulting
+ * configuration.
+ */
+ @Test
+ public void testLoadOptionalForceCreate() throws ConfigurationException
+ {
+ factory.setBasePath(TEST_FILE.getParent());
+ CombinedConfiguration config = prepareOptionalTest("xml", true);
+ assertEquals("Wrong number of configurations", 1, config
+ .getNumberOfConfigurations());
+ FileConfiguration fc = (FileConfiguration) config
+ .getConfiguration(OPTIONAL_NAME);
+ assertNotNull("Optional config not found", fc);
+ assertEquals("File name was not set", "nonExisting.xml", fc
+ .getFileName());
+ assertNotNull("Base path was not set", fc.getBasePath());
+ }
+
+ /**
+ * Tests loading an embedded optional configuration builder with the force
+ * create attribute.
+ */
+ @Test
+ public void testLoadOptionalBuilderForceCreate()
+ throws ConfigurationException
+ {
+ CombinedConfiguration config = prepareOptionalTest("configuration",
+ true);
+ assertEquals("Wrong number of configurations", 1, config
+ .getNumberOfConfigurations());
+ assertTrue(
+ "Wrong optional configuration type",
+ config.getConfiguration(OPTIONAL_NAME) instanceof CombinedConfiguration);
+ }
+
+ /**
+ * Tests loading an optional configuration with the force create attribute
+ * set. The provider will always throw an exception. In this case the
+ * configuration will not be added to the resulting combined configuration.
+ */
+ @Test
+ public void testLoadOptionalForceCreateWithException()
+ throws ConfigurationException
+ {
+ factory.addConfigurationProvider("test",
+ new DefaultConfigurationBuilder.ConfigurationBuilderProvider()
+ {
+ // Throw an exception here, too
+ @Override
+ public AbstractConfiguration getEmptyConfiguration(
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl) throws Exception
+ {
+ throw new Exception("Unable to create configuration!");
+ }
+ });
+ CombinedConfiguration config = prepareOptionalTest("test", true);
+ assertEquals("Optional configuration could be created", 0, config
+ .getNumberOfConfigurations());
+ }
+
+ /**
+ * Prepares a test for loading a configuration definition file with an
+ * optional configuration declaration.
+ *
+ * @param tag the tag name with the optional configuration
+ * @param force the forceCreate attribute
+ * @return the combined configuration obtained from the builder
+ * @throws org.apache.commons.configuration.ConfigurationException if an error occurs
+ */
+ private CombinedConfiguration prepareOptionalTest(String tag, boolean force)
+ throws ConfigurationException
+ {
+ String prefix = "override." + tag;
+ factory.addProperty(prefix + "[@fileName]", "nonExisting.xml");
+ factory.addProperty(prefix + "[@config-optional]", Boolean.TRUE);
+ factory.addProperty(prefix + "[@config-name]", OPTIONAL_NAME);
+ if (force)
+ {
+ factory.addProperty(prefix + "[@config-forceCreate]", Boolean.TRUE);
+ }
+ return factory.getConfiguration(false);
+ }
+
+ /**
+ * Tests loading a definition file with multiple different sources.
+ */
+ @Test
+ public void testLoadDifferentSources() throws ConfigurationException
+ {
+ factory.setFile(MULTI_FILE);
+ Configuration config = factory.getConfiguration();
+ assertFalse(config.isEmpty());
+ assertTrue(config instanceof CombinedConfiguration);
+ CombinedConfiguration cc = (CombinedConfiguration) config;
+ assertEquals("Wrong number of configurations", 1, cc
+ .getNumberOfConfigurations());
+
+ assertNotNull(config
+ .getProperty("tables.table(0).fields.field(2).name"));
+ assertNotNull(config.getProperty("element2.subelement.subsubelement"));
+ assertEquals("value", config.getProperty("element3"));
+ assertEquals("foo", config.getProperty("element3[@name]"));
+ assertNotNull(config.getProperty("mail.account.user"));
+
+ // test JNDIConfiguration
+ assertNotNull(config.getProperty("test.onlyinjndi"));
+ assertTrue(config.getBoolean("test.onlyinjndi"));
+
+ Configuration subset = config.subset("test");
+ assertNotNull(subset.getProperty("onlyinjndi"));
+ assertTrue(subset.getBoolean("onlyinjndi"));
+
+ // test SystemConfiguration
+ assertNotNull(config.getProperty("java.version"));
+ assertEquals(System.getProperty("java.version"), config
+ .getString("java.version"));
+ }
+
+ /**
+ * Tests if the base path is correctly evaluated.
+ */
+ @Test
+ public void testSetConfigurationBasePath() throws ConfigurationException
+ {
+ factory.addProperty("properties[@fileName]", "test.properties");
+ File deepDir = new File(ConfigurationAssert.TEST_DIR, "config/deep");
+ factory.setConfigurationBasePath(deepDir.getAbsolutePath());
+
+ Configuration config = factory.getConfiguration(false);
+ assertEquals("Wrong property value", "somevalue", config
+ .getString("somekey"));
+ }
+
+ /**
+ * Tests reading a configuration definition file that contains complex
+ * initialization of properties of the declared configuration sources.
+ */
+ @Test
+ public void testComplexInitialization() throws ConfigurationException
+ {
+ factory.setFile(INIT_FILE);
+ CombinedConfiguration cc = (CombinedConfiguration) factory
+ .getConfiguration();
+
+ assertEquals("System property not found", "test.xml",
+ cc.getString("test_file_xml"));
+ PropertiesConfiguration c1 = (PropertiesConfiguration) cc
+ .getConfiguration(1);
+ assertTrue(
+ "Reloading strategy was not set",
+ c1.getReloadingStrategy() instanceof FileChangedReloadingStrategy);
+ assertEquals("Refresh delay was not set", 10000,
+ ((FileChangedReloadingStrategy) c1.getReloadingStrategy())
+ .getRefreshDelay());
+
+ Configuration xmlConf = cc.getConfiguration("xml");
+ assertEquals("Property not found", "I'm complex!", xmlConf
+ .getString("element2/subelement/subsubelement"));
+ assertEquals("List index not found", "two", xmlConf
+ .getString("list[0]/item[1]"));
+ assertEquals("Property in combiner file not found", "yellow", cc
+ .getString("/gui/selcolor"));
+
+ assertTrue("Delimiter flag was not set", cc
+ .isDelimiterParsingDisabled());
+ assertTrue("Expression engine was not set",
+ cc.getExpressionEngine() instanceof XPathExpressionEngine);
+ }
+
+ /**
+ * Tests if the returned combined configuration has the expected structure.
+ */
+ @Test
+ public void testCombinedConfiguration() throws ConfigurationException
+ {
+ factory.setFile(INIT_FILE);
+ CombinedConfiguration cc = (CombinedConfiguration) factory
+ .getConfiguration();
+ assertNotNull("Properties configuration not found", cc
+ .getConfiguration("properties"));
+ assertNotNull("XML configuration not found", cc.getConfiguration("xml"));
+ assertEquals("Wrong number of contained configs", 4, cc
+ .getNumberOfConfigurations());
+
+ CombinedConfiguration cc2 = (CombinedConfiguration) cc
+ .getConfiguration(DefaultConfigurationBuilder.ADDITIONAL_NAME);
+ assertNotNull("No additional configuration found", cc2);
+ Set<String> names = cc2.getConfigurationNames();
+ assertEquals("Wrong number of contained additional configs", 2, names
+ .size());
+ assertTrue("Config 1 not contained", names.contains("combiner1"));
+ assertTrue("Config 2 not contained", names.contains("combiner2"));
+ }
+
+ /**
+ * Tests the structure of the returned combined configuration if there is no
+ * additional section.
+ */
+ @Test
+ public void testCombinedConfigurationNoAdditional()
+ throws ConfigurationException
+ {
+ factory.setFile(TEST_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ assertNull("Additional configuration was found", cc
+ .getConfiguration(DefaultConfigurationBuilder.ADDITIONAL_NAME));
+ }
+
+ /**
+ * Tests whether the list node definition was correctly processed.
+ */
+ @Test
+ public void testCombinedConfigurationListNodes()
+ throws ConfigurationException
+ {
+ factory.setFile(INIT_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ Set<String> listNodes = cc.getNodeCombiner().getListNodes();
+ assertEquals("Wrong number of list nodes", 2, listNodes.size());
+ assertTrue("table node not a list node", listNodes.contains("table"));
+ assertTrue("list node not a list node", listNodes.contains("list"));
+
+ CombinedConfiguration cca = (CombinedConfiguration) cc
+ .getConfiguration(DefaultConfigurationBuilder.ADDITIONAL_NAME);
+ listNodes = cca.getNodeCombiner().getListNodes();
+ assertTrue("Found list nodes for additional combiner", listNodes
+ .isEmpty());
+ }
+
+ /**
+ * Tests whether a configuration builder can itself be declared in a
+ * configuration definition file.
+ */
+ @Test
+ public void testConfigurationBuilderProvider()
+ throws ConfigurationException
+ {
+ factory.addProperty("override.configuration[@fileName]", TEST_FILE
+ .getAbsolutePath());
+ CombinedConfiguration cc = factory.getConfiguration(false);
+ assertEquals("Wrong number of configurations", 1, cc
+ .getNumberOfConfigurations());
+ checkProperties(cc);
+ }
+
+ /**
+ * Tests whether XML settings can be inherited.
+ */
+ @Test
+ public void testLoadXMLWithSettings() throws Exception
+ {
+ File confDir = new File("conf");
+ File targetDir = new File("target");
+ File testXMLValidationSource = new File(confDir,
+ "testValidateInvalid.xml");
+ File testSavedXML = new File(targetDir, "testSave.xml");
+ File testSavedFactory = new File(targetDir, "testSaveFactory.xml");
+ URL dtdFile = getClass().getResource("/properties.dtd");
+ final String publicId = "http://commons.apache.org/test.dtd";
+
+ XMLConfiguration config = new XMLConfiguration("testDtd.xml");
+ config.setPublicID(publicId);
+ config.save(testSavedXML);
+ factory.addProperty("xml[@fileName]", testSavedXML.getAbsolutePath());
+ factory.addProperty("xml(0)[@validating]", "true");
+ factory.addProperty("xml(-1)[@fileName]", testXMLValidationSource
+ .getAbsolutePath());
+ factory.addProperty("xml(1)[@config-optional]", "true");
+ factory.addProperty("xml(1)[@validating]", "true");
+ factory.save(testSavedFactory);
+
+ factory = new DefaultConfigurationBuilder();
+ factory.setFile(testSavedFactory);
+ factory.registerEntityId(publicId, dtdFile);
+ factory.clearErrorListeners();
+ Configuration c = factory.getConfiguration();
+ assertEquals("Wrong property value", "value1", c.getString("entry(0)"));
+ assertFalse("Invalid XML source was loaded", c
+ .containsKey("table.name"));
+
+ testSavedXML.delete();
+ testSavedFactory.delete();
+ }
+
+ /**
+ * Tests loading a configuration definition file that defines a custom
+ * result class.
+ */
+ @Test
+ public void testExtendedClass() throws ConfigurationException
+ {
+ factory.setFile(CLASS_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ String prop = (String)cc.getProperty("test");
+ assertEquals("Expected 'Extended', actual '" + prop + "'", "Extended", prop);
+ assertTrue("Wrong result class: " + cc.getClass(),
+ cc instanceof TestDefaultConfigurationBuilder.ExtendedCombinedConfiguration);
+ }
+
+ /**
+ * Tests loading a configuration definition file that defines new providers.
+ */
+ @Test
+ public void testConfigurationProvider() throws ConfigurationException
+ {
+ factory.setFile(PROVIDER_FILE);
+ factory.getConfiguration(true);
+ DefaultConfigurationBuilder.ConfigurationProvider provider = factory
+ .providerForTag("test");
+ assertNotNull("Provider 'test' not registered", provider);
+ }
+
+ /**
+ * Tests loading a configuration definition file that defines new providers.
+ */
+ @Test
+ public void testExtendedXMLConfigurationProvider() throws ConfigurationException
+ {
+ factory.setFile(EXTENDED_PROVIDER_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ DefaultConfigurationBuilder.ConfigurationProvider provider = factory
+ .providerForTag("test");
+ assertNotNull("Provider 'test' not registered", provider);
+ Configuration config = cc.getConfiguration("xml");
+ assertNotNull("Test configuration not present", config);
+ assertTrue("Configuration is not ExtendedXMLConfiguration, is " +
+ config.getClass().getName(),
+ config instanceof TestDefaultConfigurationBuilder.ExtendedXMLConfiguration);
+ }
+
+ @Test
+ public void testGlobalLookup() throws Exception
+ {
+ factory.setFile(GLOBAL_LOOKUP_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ String value = cc.getInterpolator().lookup("test:test_key");
+ assertNotNull("The test key was not located", value);
+ assertEquals("Incorrect value retrieved","test.value",value);
+ }
+
+ @Test
+ public void testSystemProperties() throws Exception
+ {
+ factory.setFile(SYSTEM_PROPS_FILE);
+ factory.getConfiguration(true);
+ String value = System.getProperty("key1");
+ assertNotNull("The test key was not located", value);
+ assertEquals("Incorrect value retrieved","value1",value);
+ }
+
+ @Test
+ public void testValidation() throws Exception
+ {
+ factory.setFile(VALIDATION_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ String value = cc.getString("key1");
+ assertNotNull("The test key was not located", value);
+ assertEquals("Incorrect value retrieved","value1",value);
+ }
+
+ @Test
+ public void testValidation2() throws Exception
+ {
+ factory.setFile(VALIDATION2_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ String value = cc.getString("key1");
+ assertNotNull("The test key was not located", value);
+ assertEquals("Incorrect value retrieved","value1",value);
+ }
+
+ @Test
+ public void testMultiTenentConfiguration() throws Exception
+ {
+ factory.setFile(MULTI_TENENT_FILE);
+ System.getProperties().remove("Id");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
+
+ verify(null, config, 50);
+ verify("1001", config, 15);
+ verify("1002", config, 25);
+ verify("1003", config, 35);
+ verify("1004", config, 50);
+ verify("1005", config, 50);
+ }
+
+ @Test
+ public void testMultiTenentConfiguration2() throws Exception
+ {
+ factory.setFile(MULTI_TENENT_FILE);
+ System.setProperty("Id", "1004");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
+
+ verify("1001", config, 15);
+ verify("1002", config, 25);
+ verify("1003", config, 35);
+ verify("1004", config, 50);
+ verify("1005", config, 50);
+ }
+
+ @Test
+ public void testMultiTenentConfiguration3() throws Exception
+ {
+ factory.setFile(MULTI_TENENT_FILE);
+ System.setProperty("Id", "1005");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
+
+ verify("1001", config, 15);
+ verify("1002", config, 25);
+ verify("1003", config, 35);
+ verify("1004", config, 50);
+ verify("1005", config, 50);
+ }
+
+ @Test
+ public void testSetFileSystem() throws Exception
+ {
+ factory.setFile(PROVIDER_FILE);
+ FileSystem fs = new VFSFileSystem();
+ factory.setFileSystem(fs);
+ FileSystem.resetDefaultFileSystem();
+ System.getProperties().remove("Id");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ List<AbstractConfiguration> list = config.getConfigurations();
+ assertTrue("Incorrect number of configurations - " + list.size(), list.size() == 4);
+ Iterator<AbstractConfiguration> iter = list.iterator();
+ while (iter.hasNext())
+ {
+ Configuration conf = iter.next();
+ if (conf instanceof FileSystemBased)
+ {
+ assertTrue("Incorrect file system for Configuration " + conf,
+ ((FileSystemBased)conf).getFileSystem() == fs);
+ }
+ else if (conf instanceof CombinedConfiguration)
+ {
+ Iterator<AbstractConfiguration> it = ((CombinedConfiguration)conf).getConfigurations().iterator();
+ while (it.hasNext())
+ {
+ conf = it.next();
+ if (conf instanceof FileSystemBased)
+ {
+ assertTrue("Incorrect file system for Configuration " + conf,
+ ((FileSystemBased)conf).getFileSystem() == fs);
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testConfiguredFileSystem() throws Exception
+ {
+ factory.setFile(FILESYSTEM_FILE);
+ FileSystem.resetDefaultFileSystem();
+ System.getProperties().remove("Id");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ FileSystem fs = factory.getFileSystem();
+ assertNotNull("No File System",fs);
+ assertTrue("Incorrect File System", fs instanceof VFSFileSystem);
+ List<AbstractConfiguration> list = config.getConfigurations();
+ assertTrue("Incorrect number of configurations - " + list.size(), list.size() == 4);
+ Iterator<AbstractConfiguration> iter = list.iterator();
+ while (iter.hasNext())
+ {
+ Configuration conf = iter.next();
+ if (conf instanceof FileSystemBased)
+ {
+ assertTrue("Incorrect file system for Configuration " + conf,
+ ((FileSystemBased)conf).getFileSystem() == fs);
+ }
+ else if (conf instanceof CombinedConfiguration)
+ {
+ Iterator<AbstractConfiguration> it = ((CombinedConfiguration)conf).getConfigurations().iterator();
+ while (it.hasNext())
+ {
+ conf = it.next();
+ if (conf instanceof FileSystemBased)
+ {
+ assertTrue("Incorrect file system for Configuration " + conf,
+ ((FileSystemBased)conf).getFileSystem() == fs);
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testFileReload1() throws Exception
+ {
+ // create a new configuration
+ File input = new File("target/test-classes/testMultiConfiguration_1001.xml");
+ File output = new File("target/test-classes/testwrite/testMultiConfiguration_1001.xml");
+ output.delete();
+ output.getParentFile().mkdir();
+ copyFile(input, output);
+ // Sleep to make sure the file timestamp is not in the same second as "now".
+ Thread.sleep(1100);
+
+ factory.setFile(FILERELOAD_FILE);
+ FileSystem.resetDefaultFileSystem();
+ System.getProperties().remove("Id");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertNotNull(config);
+ verify("1001", config, 15);
+ Thread.sleep(1100);
+ XMLConfiguration x = new XMLConfiguration(output);
+ x.setProperty("rowsPerPage", "50");
+ x.save();
+ verify("1001", config, 50);
+ output.delete();
+ }
+
+ @Test
+ public void testFileReload2() throws Exception
+ {
+ // create a new configuration
+ File input = new File("target/test-classes/testMultiConfiguration_1002.xml");
+ File output = new File("target/test-classes/testwrite/testMultiConfiguration_1002.xml");
+ output.delete();
+
+ factory.setFile(FILERELOAD_FILE);
+ FileSystem.resetDefaultFileSystem();
+ System.getProperties().remove("Id");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertNotNull(config);
+
+ verify("1002", config, 50);
+ // Sleep to make sure the file timestamp is not in the same second as "now".
+ Thread.sleep(1100);
+ output.getParentFile().mkdir();
+ copyFile(input, output);
+ verify("1002", config, 25);
+ output.delete();
+ }
+
+ @Test
+ public void testFileReload3() throws Exception
+ {
+ // create a new configuration
+ File input = new File("target/test-classes/testMultiConfiguration_1001.xml");
+ File output = new File("target/test-classes/testwrite/testMultiConfiguration_1001.xml");
+ output.delete();
+ output.getParentFile().mkdir();
+
+ factory.setFile(FILERELOAD_FILE);
+ FileSystem.resetDefaultFileSystem();
+ System.getProperties().remove("Id");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertNotNull(config);
+ verify("1001", config, 50);
+ copyFile(input, output);
+ // Sleep to make sure the file timestamp is not in the same second as "now".
+ Thread.sleep(1100);
+ verify("1001", config, 15);
+ XMLConfiguration x = new XMLConfiguration(output);
+ x.setProperty("rowsPerPage", "25");
+ x.save();
+ // Sleep to make sure the file timestamp is not in the same second as "now".
+ Thread.sleep(1100);
+ verify("1001", config, 25);
+ output.delete();
+ }
+
+ @Test
+ public void testReloadDefault() throws Exception
+ {
+ // create a new configuration
+ String defaultName = "target/test-classes/testMultiConfiguration_default.xml";
+ File input = new File(defaultName);
+
+ System.getProperties().remove("Id");
+ factory.setFile(MULTI_RELOAD_FILE1);
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertNotNull(config);
+ verify("3001", config, 15);
+ verify("3002", config, 25);
+ System.setProperty("Id", "3002");
+ config.addProperty("/ TestProp", "Test");
+ assertTrue("Property not added", "Test".equals(config.getString("TestProp")));
+ System.getProperties().remove("Id");
+ //Sleep so refreshDelay elapses
+ Thread.sleep(600);
+ long time = System.currentTimeMillis();
+ long original = input.lastModified();
+ input.setLastModified(time);
+ File defaultFile = new File(defaultName);
+ long newTime = defaultFile.lastModified();
+ assertTrue("time mismatch", original != newTime);
+ Thread.sleep(600);
+ verify("3001", config, 15);
+ verify("3002", config, 25);
+ System.setProperty("Id", "3002");
+ String test = config.getString("TestProp");
+ assertNull("Property was not cleared by reload", test);
+ }
+
+ @Test
+ public void testFileReloadSchemaValidationError() throws Exception
+ {
+ System.getProperties().remove("Id");
+ factory.setFile(MULTI_RELOAD_FILE2);
+ CombinedConfiguration config = factory.getConfiguration(true);
+
+ // create a new configuration
+ File input = new File("target/test-classes/testMultiConfiguration_3001.xml");
+ File output = new File("target/test-classes/testwrite/testMultiConfiguration_3001.xml");
+ output.delete();
+ output.getParentFile().mkdir();
+ copyFile(input, output);
+
+ assertNotNull(config);
+ verify("3001", config, 15);
+ Thread.sleep(1100);
+ XMLConfiguration x = new XMLConfiguration();
+ x.setFile(output);
+ x.setAttributeSplittingDisabled(true);
+ x.setDelimiterParsingDisabled(true);
+ x.load();
+ x.setProperty("rowsPerPage", "test");
+ //Insure orginal timestamp and new timestamp aren't the same second.
+ Thread.sleep(1100);
+ x.save();
+ System.setProperty("Id", "3001");
+ try
+ {
+ config.getInt("rowsPerPage");
+ fail("No exception was thrown");
+ }
+ catch (Exception ex)
+ {
+
+ }
+
+ output.delete();
+ }
+
+ private void copyFile(File input, File output) throws IOException
+ {
+ Reader reader = new FileReader(input);
+ Writer writer = new FileWriter(output);
+ char[] buffer = new char[4096];
+ int n = 0;
+ while (-1 != (n = reader.read(buffer)))
+ {
+ writer.write(buffer, 0, n);
+ }
+ reader.close();
+ writer.close();
+ }
+
+ private void verify(String key, CombinedConfiguration config, int rows)
+ {
+ if (key == null)
+ {
+ System.getProperties().remove("Id");
+ }
+ else
+ {
+ System.setProperty("Id", key);
+ }
+ int actual = config.getInt("rowsPerPage");
+ assertTrue("expected: " + rows + " actual: " + actual, actual == rows);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/commons/configuration/TestWebdavConfigurationBuilder.java b/src/test/java/org/apache/commons/configuration/TestWebdavConfigurationBuilder.java
new file mode 100644
index 0000000..0347552
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestWebdavConfigurationBuilder.java
@@ -0,0 +1,1059 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.configuration.beanutils.BeanHelper;
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;
+import org.apache.commons.configuration.tree.DefaultConfigurationNode;
+import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemManager;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.VFS;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for DefaultConfigurationBuilder.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestWebdavConfigurationBuilder.java 1225331 2011-12-28 20:55:49Z oheger $
+ */
+public class TestWebdavConfigurationBuilder
+ implements FileOptionsProvider, ConfigurationListener
+{
+ /** Test configuration definition file. */
+ private static final String TEST_FILE =
+ "testDigesterConfiguration.xml";
+
+ private static final String ADDITIONAL_FILE =
+ "testDigesterConfiguration2.xml";
+
+ private static final String OPTIONAL_FILE =
+ "testDigesterOptionalConfiguration.xml";
+
+ private static final String OPTIONALEX_FILE =
+ "testDigesterOptionalConfigurationEx.xml";
+
+ private static final String MULTI_FILE =
+ "testDigesterConfiguration3.xml";
+
+ private static final String INIT_FILE =
+ "testComplexInitialization.xml";
+
+ private static final String CLASS_FILE =
+ "testExtendedClass.xml";
+
+ private static final String PROVIDER_FILE =
+ "testConfigurationProvider.xml";
+
+ private static final String EXTENDED_PROVIDER_FILE =
+ "testExtendedXMLConfigurationProvider.xml";
+
+ private static final String GLOBAL_LOOKUP_FILE =
+ "testGlobalLookup.xml";
+
+ private static final String SYSTEM_PROPS_FILE =
+ "testSystemProperties.xml";
+
+ private static final String VALIDATION_FILE =
+ "testValidation.xml";
+
+ private static final String MULTI_TENENT_FILE =
+ "testMultiTenentConfigurationBuilder.xml";
+
+ private static final String FILERELOAD2_FILE =
+ "testFileReloadConfigurationBuilder2.xml";
+
+ private static final String FILERELOAD_1001_FILE =
+ "testwrite/testMultiConfiguration_1001.xml";
+
+ private static final String FILERELOAD_1002_FILE =
+ "testwrite/testMultiConfiguration_1002.xml";
+
+ private static final String TEST_PROPERTIES = "test.properties.xml";
+
+ private static final String TEST_SAVE = "testsave.xml";
+
+ /** Constant for the name of an optional configuration.*/
+ private static final String OPTIONAL_NAME = "optionalConfig";
+
+ private Map<String, Object> options;
+
+ /** true when a file is changed */
+ private boolean configChanged = false;
+
+ /** Stores the object to be tested. */
+ DefaultConfigurationBuilder factory;
+
+ private String getBasePath()
+ {
+ String path = System.getProperty("test.webdav.base");
+ assertNotNull("No base url provided", path);
+ return path;
+ }
+
+ @Before
+ public void setUp() throws Exception
+ {
+ System.setProperty("java.naming.factory.initial",
+ "org.apache.commons.configuration.MockInitialContextFactory");
+ System.setProperty("test_file_xml", "test.xml");
+ System.setProperty("test_file_combine", "testcombine1.xml");
+ System.setProperty("basePath", getBasePath());
+ FileSystem fs = new VFSFileSystem();
+ fs.setFileOptionsProvider(this);
+ FileSystem.setDefaultFileSystem(fs);
+ factory = new DefaultConfigurationBuilder();
+ factory.setBasePath(getBasePath());
+ factory.clearErrorListeners(); // avoid exception messages
+ }
+
+ @After
+ public void tearDown() throws Exception
+ {
+ FileSystem.resetDefaultFileSystem();
+ }
+
+ /**
+ * Tests the isReservedNode() method of ConfigurationDeclaration.
+ */
+ @Test
+ public void testConfigurationDeclarationIsReserved()
+ {
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory);
+ DefaultConfigurationNode parent = new DefaultConfigurationNode();
+ DefaultConfigurationNode nd = new DefaultConfigurationNode("at");
+ parent.addAttribute(nd);
+ assertTrue("Attribute at not recognized", decl.isReservedNode(nd));
+ nd = new DefaultConfigurationNode("optional");
+ parent.addAttribute(nd);
+ assertTrue("Attribute optional not recognized", decl.isReservedNode(nd));
+ nd = new DefaultConfigurationNode("config-class");
+ parent.addAttribute(nd);
+ assertTrue("Inherited attribute not recognized", decl
+ .isReservedNode(nd));
+ nd = new DefaultConfigurationNode("different");
+ parent.addAttribute(nd);
+ assertFalse("Wrong reserved attribute", decl.isReservedNode(nd));
+ nd = new DefaultConfigurationNode("at");
+ parent.addChild(nd);
+ assertFalse("Node type not evaluated", decl.isReservedNode(nd));
+ }
+
+ /**
+ * Tests if the at attribute is correctly detected as reserved attribute.
+ */
+ @Test
+ public void testConfigurationDeclarationIsReservedAt()
+ {
+ checkOldReservedAttribute("at");
+ }
+
+ /**
+ * Tests if the optional attribute is correctly detected as reserved
+ * attribute.
+ */
+ @Test
+ public void testConfigurationDeclarationIsReservedOptional()
+ {
+ checkOldReservedAttribute("optional");
+ }
+
+ /**
+ * Tests if special reserved attributes are recognized by the
+ * isReservedNode() method. For compatibility reasons the attributes "at"
+ * and "optional" are also treated as reserved attributes, but only if there
+ * are no corresponding attributes with the "config-" prefix.
+ *
+ * @param name the attribute name
+ */
+ private void checkOldReservedAttribute(String name)
+ {
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory);
+ DefaultConfigurationNode parent = new DefaultConfigurationNode();
+ DefaultConfigurationNode nd = new DefaultConfigurationNode("config-"
+ + name);
+ parent.addAttribute(nd);
+ assertTrue("config-" + name + " attribute not recognized", decl
+ .isReservedNode(nd));
+ DefaultConfigurationNode nd2 = new DefaultConfigurationNode(name);
+ parent.addAttribute(nd2);
+ assertFalse(name + " is reserved though config- exists", decl
+ .isReservedNode(nd2));
+ assertTrue("config- attribute not recognized when " + name + " exists",
+ decl.isReservedNode(nd));
+ }
+
+ /**
+ * Tests access to certain reserved attributes of a
+ * ConfigurationDeclaration.
+ */
+ @Test
+ public void testConfigurationDeclarationGetAttributes()
+ {
+ factory.addProperty("xml.fileName", "test.xml");
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory.configurationAt("xml"));
+ assertNull("Found an at attribute", decl.getAt());
+ assertFalse("Found an optional attribute", decl.isOptional());
+ factory.addProperty("xml[@config-at]", "test1");
+ assertEquals("Wrong value of at attribute", "test1", decl.getAt());
+ factory.addProperty("xml[@at]", "test2");
+ assertEquals("Wrong value of config-at attribute", "test1", decl.getAt());
+ factory.clearProperty("xml[@config-at]");
+ assertEquals("Old at attribute not detected", "test2", decl.getAt());
+ factory.addProperty("xml[@config-optional]", "true");
+ assertTrue("Wrong value of optional attribute", decl.isOptional());
+ factory.addProperty("xml[@optional]", "false");
+ assertTrue("Wrong value of config-optional attribute", decl.isOptional());
+ factory.clearProperty("xml[@config-optional]");
+ factory.setProperty("xml[@optional]", Boolean.TRUE);
+ assertTrue("Old optional attribute not detected", decl.isOptional());
+ }
+
+ /**
+ * Tests whether an invalid value of an optional attribute is detected.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testConfigurationDeclarationOptionalAttributeInvalid()
+ {
+ factory.addProperty("xml.fileName", "test.xml");
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory.configurationAt("xml"));
+ factory.setProperty("xml[@optional]", "invalid value");
+ decl.isOptional();
+ }
+
+ /**
+ * Tests adding a new configuration provider.
+ */
+ @Test
+ public void testAddConfigurationProvider()
+ {
+ DefaultConfigurationBuilder.ConfigurationProvider provider = new DefaultConfigurationBuilder.ConfigurationProvider();
+ assertNull("Provider already registered", factory
+ .providerForTag("test"));
+ factory.addConfigurationProvider("test", provider);
+ assertSame("Provider not registered", provider, factory
+ .providerForTag("test"));
+ }
+
+ /**
+ * Tries to register a null configuration provider. This should cause an
+ * exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddConfigurationProviderNull()
+ {
+ factory.addConfigurationProvider("test", null);
+ }
+
+ /**
+ * Tries to register a configuration provider for a null tag. This should
+ * cause an exception to be thrown.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddConfigurationProviderNullTag()
+ {
+ factory.addConfigurationProvider(null,
+ new DefaultConfigurationBuilder.ConfigurationProvider());
+ }
+
+ /**
+ * Tests removing configuration providers.
+ */
+ @Test
+ public void testRemoveConfigurationProvider()
+ {
+ assertNull("Removing unknown provider", factory
+ .removeConfigurationProvider("test"));
+ assertNull("Removing provider for null tag", factory
+ .removeConfigurationProvider(null));
+ DefaultConfigurationBuilder.ConfigurationProvider provider = new DefaultConfigurationBuilder.ConfigurationProvider();
+ factory.addConfigurationProvider("test", provider);
+ assertSame("Failed to remove provider", provider, factory
+ .removeConfigurationProvider("test"));
+ assertNull("Provider still registered", factory.providerForTag("test"));
+ }
+
+ /**
+ * Tests creating a configuration object from a configuration declaration.
+ */
+ @Test
+ public void testConfigurationBeanFactoryCreateBean()
+ {
+ factory.addConfigurationProvider("test",
+ new DefaultConfigurationBuilder.ConfigurationProvider(
+ PropertiesConfiguration.class));
+ factory.addProperty("test[@throwExceptionOnMissing]", "true");
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory.configurationAt("test"));
+ PropertiesConfiguration conf = (PropertiesConfiguration) BeanHelper
+ .createBean(decl);
+ assertTrue("Property was not initialized", conf
+ .isThrowExceptionOnMissing());
+ }
+
+ /**
+ * Tests creating a configuration object from an unknown tag. This should
+ * cause an exception.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testConfigurationBeanFactoryCreateUnknownTag()
+ {
+ factory.addProperty("test[@throwExceptionOnMissing]", "true");
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
+ factory, factory.configurationAt("test"));
+ BeanHelper.createBean(decl);
+ }
+
+ /**
+ * Tests loading a simple configuration definition file.
+ */
+ @Test
+ public void testLoadConfiguration() throws ConfigurationException
+ {
+ factory.setFileName(TEST_FILE);
+ checkConfiguration();
+ }
+
+ /**
+ * Tests the file constructor.
+ */
+ @Test
+ public void testLoadConfigurationFromFile() throws ConfigurationException
+ {
+ factory = new DefaultConfigurationBuilder(getBasePath() + TEST_FILE);
+ checkConfiguration();
+ }
+
+ /**
+ * This test doesn't test DefaultConfigurationBuilder. It tests saving a file
+ * using Webdav file options.
+ */
+ @Test
+ public void testSaveConfiguration() throws ConfigurationException
+ {
+ options = new HashMap<String, Object>();
+ options.put(FileOptionsProvider.VERSIONING, Boolean.TRUE);
+ options.put(FileOptionsProvider.CURRENT_USER, "TestUser");
+ XMLConfiguration conf = new XMLConfiguration();
+ conf.setFileName(getBasePath() + TEST_PROPERTIES);
+ conf.load();
+ conf.save(getBasePath() + TEST_SAVE);
+ XMLConfiguration checkConfig = new XMLConfiguration();
+ checkConfig.setFileName(getBasePath() + TEST_SAVE);
+ checkSavedConfig(conf, checkConfig);
+ options = null;
+ }
+
+ /**
+ * Tests if the configuration was correctly created by the factory.
+ */
+ private void checkConfiguration() throws ConfigurationException
+ {
+ CombinedConfiguration compositeConfiguration = (CombinedConfiguration) factory
+ .getConfiguration();
+
+ assertEquals("Number of configurations", 3, compositeConfiguration
+ .getNumberOfConfigurations());
+ assertEquals(PropertiesConfiguration.class, compositeConfiguration
+ .getConfiguration(0).getClass());
+ assertEquals(XMLPropertiesConfiguration.class, compositeConfiguration
+ .getConfiguration(1).getClass());
+ assertEquals(XMLConfiguration.class, compositeConfiguration
+ .getConfiguration(2).getClass());
+
+ // check the first configuration
+ PropertiesConfiguration pc = (PropertiesConfiguration) compositeConfiguration
+ .getConfiguration(0);
+ assertNotNull("Make sure we have a fileName: " + pc.getFileName(), pc
+ .getFileName());
+
+ // check some properties
+ checkProperties(compositeConfiguration);
+ }
+
+ /**
+ * Checks if the passed in configuration contains the expected properties.
+ *
+ * @param compositeConfiguration the configuration to check
+ */
+ private void checkProperties(Configuration compositeConfiguration)
+ {
+ assertTrue("Make sure we have loaded our key", compositeConfiguration
+ .getBoolean("test.boolean"));
+ assertEquals("I'm complex!", compositeConfiguration
+ .getProperty("element2.subelement.subsubelement"));
+ assertEquals("property in the XMLPropertiesConfiguration", "value1",
+ compositeConfiguration.getProperty("key1"));
+ }
+
+ /**
+ * Tests loading a configuration definition file with an additional section.
+ */
+ @Test
+ public void testLoadAdditional() throws ConfigurationException
+ {
+ factory.setFileName(ADDITIONAL_FILE);
+ CombinedConfiguration compositeConfiguration = (CombinedConfiguration) factory
+ .getConfiguration();
+ assertEquals("Verify how many configs", 2, compositeConfiguration
+ .getNumberOfConfigurations());
+
+ // Test if union was constructed correctly
+ Object prop = compositeConfiguration.getProperty("tables.table.name");
+ assertTrue(prop instanceof Collection);
+ assertEquals(3, ((Collection<?>) prop).size());
+ assertEquals("users", compositeConfiguration
+ .getProperty("tables.table(0).name"));
+ assertEquals("documents", compositeConfiguration
+ .getProperty("tables.table(1).name"));
+ assertEquals("tasks", compositeConfiguration
+ .getProperty("tables.table(2).name"));
+
+ prop = compositeConfiguration
+ .getProperty("tables.table.fields.field.name");
+ assertTrue(prop instanceof Collection);
+ assertEquals(17, ((Collection<?>) prop).size());
+
+ assertEquals("smtp.mydomain.org", compositeConfiguration
+ .getString("mail.host.smtp"));
+ assertEquals("pop3.mydomain.org", compositeConfiguration
+ .getString("mail.host.pop"));
+
+ // This was overridden
+ assertEquals("masterOfPost", compositeConfiguration
+ .getString("mail.account.user"));
+ assertEquals("topsecret", compositeConfiguration
+ .getString("mail.account.psswd"));
+
+ // This was overridden, too, but not in additional section
+ assertEquals("enhanced factory", compositeConfiguration
+ .getString("test.configuration"));
+ }
+
+ /**
+ * Tests whether a default log error listener is registered at the builder
+ * instance.
+ */
+ @Test
+ public void testLogErrorListener()
+ {
+ assertEquals("No default error listener registered", 1,
+ new DefaultConfigurationBuilder().getErrorListeners().size());
+ }
+
+ /**
+ * Tests loading a definition file that contains optional configurations.
+ */
+ @Test
+ public void testLoadOptional() throws Exception
+ {
+ factory.setFileName(OPTIONAL_FILE);
+ Configuration config = factory.getConfiguration();
+ assertTrue(config.getBoolean("test.boolean"));
+ assertEquals("value", config.getProperty("element"));
+ }
+
+ /**
+ * Tests whether loading a failing optional configuration causes an error
+ * event.
+ */
+ @Test
+ public void testLoadOptionalErrorEvent() throws Exception
+ {
+ factory.clearErrorListeners();
+ ConfigurationErrorListenerImpl listener = new ConfigurationErrorListenerImpl();
+ factory.addErrorListener(listener);
+ prepareOptionalTest("configuration", false);
+ listener.verify(DefaultConfigurationBuilder.EVENT_ERR_LOAD_OPTIONAL,
+ OPTIONAL_NAME, null);
+ }
+
+ /**
+ * Tests loading a definition file with optional and non optional
+ * configuration sources. One non optional does not exist, so this should
+ * cause an exception.
+ */
+ @Test(expected = ConfigurationException.class)
+ public void testLoadOptionalWithException() throws ConfigurationException
+ {
+ factory.setFileName(OPTIONALEX_FILE);
+ factory.getConfiguration();
+ }
+
+ /**
+ * Tries to load a configuration file with an optional, non file-based
+ * configuration. The optional attribute should work for other configuration
+ * classes, too.
+ */
+ @Test
+ public void testLoadOptionalNonFileBased() throws ConfigurationException
+ {
+ CombinedConfiguration config = prepareOptionalTest("configuration", false);
+ assertTrue("Configuration not empty", config.isEmpty());
+ assertEquals("Wrong number of configurations", 0, config
+ .getNumberOfConfigurations());
+ }
+
+ /**
+ * Tests loading an embedded optional configuration builder with the force
+ * create attribute.
+ */
+ @Test
+ public void testLoadOptionalBuilderForceCreate()
+ throws ConfigurationException
+ {
+ CombinedConfiguration config = prepareOptionalTest("configuration",
+ true);
+ assertEquals("Wrong number of configurations", 1, config
+ .getNumberOfConfigurations());
+ assertTrue(
+ "Wrong optional configuration type",
+ config.getConfiguration(OPTIONAL_NAME) instanceof CombinedConfiguration);
+ }
+
+ /**
+ * Tests loading an optional configuration with the force create attribute
+ * set. The provider will always throw an exception. In this case the
+ * configuration will not be added to the resulting combined configuration.
+ */
+ @Test
+ public void testLoadOptionalForceCreateWithException()
+ throws ConfigurationException
+ {
+ factory.addConfigurationProvider("test",
+ new DefaultConfigurationBuilder.ConfigurationBuilderProvider()
+ {
+ // Throw an exception here, too
+ @Override
+ public AbstractConfiguration getEmptyConfiguration(
+ DefaultConfigurationBuilder.ConfigurationDeclaration decl) throws Exception
+ {
+ throw new Exception("Unable to create configuration!");
+ }
+ });
+ CombinedConfiguration config = prepareOptionalTest("test", true);
+ assertEquals("Optional configuration could be created", 0, config
+ .getNumberOfConfigurations());
+ }
+
+ /**
+ * Prepares a test for loading a configuration definition file with an
+ * optional configuration declaration.
+ *
+ * @param tag the tag name with the optional configuration
+ * @param force the forceCreate attribute
+ * @return the combined configuration obtained from the builder
+ * @throws ConfigurationException if an error occurs
+ */
+ private CombinedConfiguration prepareOptionalTest(String tag, boolean force)
+ throws ConfigurationException
+ {
+ String prefix = "override." + tag;
+ factory.addProperty(prefix + "[@fileName]", "nonExisting.xml");
+ factory.addProperty(prefix + "[@config-optional]", Boolean.TRUE);
+ factory.addProperty(prefix + "[@config-name]", OPTIONAL_NAME);
+ if (force)
+ {
+ factory.addProperty(prefix + "[@config-forceCreate]", Boolean.TRUE);
+ }
+ return factory.getConfiguration(false);
+ }
+
+ /**
+ * Tests loading a definition file with multiple different sources.
+ */
+ @Test
+ public void testLoadDifferentSources() throws ConfigurationException
+ {
+ factory.setFileName(MULTI_FILE);
+ Configuration config = factory.getConfiguration();
+ assertFalse(config.isEmpty());
+ assertTrue(config instanceof CombinedConfiguration);
+ CombinedConfiguration cc = (CombinedConfiguration) config;
+ assertEquals("Wrong number of configurations", 1, cc
+ .getNumberOfConfigurations());
+
+ assertNotNull(config
+ .getProperty("tables.table(0).fields.field(2).name"));
+ assertNotNull(config.getProperty("element2.subelement.subsubelement"));
+ assertEquals("value", config.getProperty("element3"));
+ assertEquals("foo", config.getProperty("element3[@name]"));
+ assertNotNull(config.getProperty("mail.account.user"));
+
+ // test JNDIConfiguration
+ assertNotNull(config.getProperty("test.onlyinjndi"));
+ assertTrue(config.getBoolean("test.onlyinjndi"));
+
+ Configuration subset = config.subset("test");
+ assertNotNull(subset.getProperty("onlyinjndi"));
+ assertTrue(subset.getBoolean("onlyinjndi"));
+
+ // test SystemConfiguration
+ assertNotNull(config.getProperty("java.version"));
+ assertEquals(System.getProperty("java.version"), config
+ .getString("java.version"));
+ }
+
+ /**
+ * Tests if the base path is correctly evaluated.
+ */
+ @Test
+ public void testSetConfigurationBasePath() throws ConfigurationException
+ {
+ factory.addProperty("properties[@fileName]", "test.properties");
+ File deepDir = new File("conf/config/deep");
+ factory.setConfigurationBasePath(deepDir.getAbsolutePath());
+
+ Configuration config = factory.getConfiguration(false);
+ assertEquals("Wrong property value", "somevalue", config
+ .getString("somekey"));
+ }
+
+ /**
+ * Tests reading a configuration definition file that contains complex
+ * initialization of properties of the declared configuration sources.
+ */
+ @Test
+ public void testComplexInitialization() throws ConfigurationException
+ {
+ factory.setFileName(INIT_FILE);
+ CombinedConfiguration cc = (CombinedConfiguration) factory
+ .getConfiguration();
+
+ assertEquals("System property not found", "test.xml",
+ cc.getString("test_file_xml"));
+ PropertiesConfiguration c1 = (PropertiesConfiguration) cc
+ .getConfiguration(1);
+ assertTrue(
+ "Reloading strategy was not set",
+ c1.getReloadingStrategy() instanceof FileChangedReloadingStrategy);
+ assertEquals("Refresh delay was not set", 10000,
+ ((FileChangedReloadingStrategy) c1.getReloadingStrategy())
+ .getRefreshDelay());
+
+ Configuration xmlConf = cc.getConfiguration("xml");
+ assertEquals("Property not found", "I'm complex!", xmlConf
+ .getString("element2/subelement/subsubelement"));
+ assertEquals("List index not found", "two", xmlConf
+ .getString("list[0]/item[1]"));
+ assertEquals("Property in combiner file not found", "yellow", cc
+ .getString("/gui/selcolor"));
+
+ assertTrue("Delimiter flag was not set", cc
+ .isDelimiterParsingDisabled());
+ assertTrue("Expression engine was not set",
+ cc.getExpressionEngine() instanceof XPathExpressionEngine);
+ }
+
+ /**
+ * Tests if the returned combined configuration has the expected structure.
+ */
+ @Test
+ public void testCombinedConfiguration() throws ConfigurationException
+ {
+ factory.setFileName(INIT_FILE);
+ CombinedConfiguration cc = (CombinedConfiguration) factory
+ .getConfiguration();
+ assertNotNull("Properties configuration not found", cc
+ .getConfiguration("properties"));
+ assertNotNull("XML configuration not found", cc.getConfiguration("xml"));
+ assertEquals("Wrong number of contained configs", 4, cc
+ .getNumberOfConfigurations());
+
+ CombinedConfiguration cc2 = (CombinedConfiguration) cc
+ .getConfiguration(DefaultConfigurationBuilder.ADDITIONAL_NAME);
+ assertNotNull("No additional configuration found", cc2);
+ Set<String> names = cc2.getConfigurationNames();
+ assertEquals("Wrong number of contained additional configs", 2, names
+ .size());
+ assertTrue("Config 1 not contained", names.contains("combiner1"));
+ assertTrue("Config 2 not contained", names.contains("combiner2"));
+ }
+
+ /**
+ * Tests the structure of the returned combined configuration if there is no
+ * additional section.
+ */
+ @Test
+ public void testCombinedConfigurationNoAdditional()
+ throws ConfigurationException
+ {
+ factory.setFileName(TEST_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ assertNull("Additional configuration was found", cc
+ .getConfiguration(DefaultConfigurationBuilder.ADDITIONAL_NAME));
+ }
+
+ /**
+ * Tests whether the list node definition was correctly processed.
+ */
+ @Test
+ public void testCombinedConfigurationListNodes()
+ throws ConfigurationException
+ {
+ factory.setFileName(INIT_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ Set<String> listNodes = cc.getNodeCombiner().getListNodes();
+ assertEquals("Wrong number of list nodes", 2, listNodes.size());
+ assertTrue("table node not a list node", listNodes.contains("table"));
+ assertTrue("list node not a list node", listNodes.contains("list"));
+
+ CombinedConfiguration cca = (CombinedConfiguration) cc
+ .getConfiguration(DefaultConfigurationBuilder.ADDITIONAL_NAME);
+ listNodes = cca.getNodeCombiner().getListNodes();
+ assertTrue("Found list nodes for additional combiner", listNodes
+ .isEmpty());
+ }
+
+ /**
+ * Tests whether XML settings can be inherited.
+ */
+ @Test
+ public void testLoadXMLWithSettings() throws ConfigurationException,
+ IOException
+ {
+ File confDir = new File("conf");
+ File targetDir = new File("target");
+ File testXMLSource = new File(confDir, "testDtd.xml");
+ File testXMLValidationSource = new File(confDir,
+ "testValidateInvalid.xml");
+ File testSavedXML = new File(targetDir, "testSave.xml");
+ File testSavedFactory = new File(targetDir, "testSaveFactory.xml");
+ File dtdFile = new File(confDir, "properties.dtd");
+ final String publicId = "http://commons.apache.org/test.dtd";
+
+ XMLConfiguration config = new XMLConfiguration(testXMLSource);
+ config.setPublicID(publicId);
+ config.save(testSavedXML);
+ factory.addProperty("xml[@fileName]", testSavedXML.getAbsolutePath());
+ factory.addProperty("xml(0)[@validating]", "true");
+ factory.addProperty("xml(-1)[@fileName]", testXMLValidationSource
+ .getAbsolutePath());
+ factory.addProperty("xml(1)[@config-optional]", "true");
+ factory.addProperty("xml(1)[@validating]", "true");
+ factory.save(testSavedFactory);
+
+ factory = new DefaultConfigurationBuilder();
+ factory.setFile(testSavedFactory);
+ factory.registerEntityId(publicId, dtdFile.toURI().toURL());
+ factory.clearErrorListeners();
+ Configuration c = factory.getConfiguration();
+ assertEquals("Wrong property value", "value1", c.getString("entry(0)"));
+ assertFalse("Invalid XML source was loaded", c
+ .containsKey("table.name"));
+
+ testSavedXML.delete();
+ testSavedFactory.delete();
+ }
+
+ /**
+ * Tests loading a configuration definition file that defines a custom
+ * result class.
+ */
+ @Test
+ public void testExtendedClass() throws ConfigurationException
+ {
+ factory.setFileName(CLASS_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ String prop = (String)cc.getProperty("test");
+ assertEquals("Expected 'Extended', actual '" + prop + "'", "Extended", prop);
+ assertTrue("Wrong result class: " + cc.getClass(),
+ cc instanceof TestDefaultConfigurationBuilder.ExtendedCombinedConfiguration);
+ }
+
+ /**
+ * Tests loading a configuration definition file that defines new providers.
+ */
+ @Test
+ public void testConfigurationProvider() throws ConfigurationException
+ {
+ factory.setFileName(PROVIDER_FILE);
+ factory.getConfiguration(true);
+ DefaultConfigurationBuilder.ConfigurationProvider provider = factory
+ .providerForTag("test");
+ assertNotNull("Provider 'test' not registered", provider);
+ }
+
+ /**
+ * Tests loading a configuration definition file that defines new providers.
+ */
+ @Test
+ public void testExtendedXMLConfigurationProvider() throws ConfigurationException
+ {
+ factory.setFileName(EXTENDED_PROVIDER_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ DefaultConfigurationBuilder.ConfigurationProvider provider = factory
+ .providerForTag("test");
+ assertNotNull("Provider 'test' not registered", provider);
+ Configuration config = cc.getConfiguration("xml");
+ assertNotNull("Test configuration not present", config);
+ assertTrue("Configuration is not ExtendedXMLConfiguration, is " +
+ config.getClass().getName(),
+ config instanceof TestDefaultConfigurationBuilder.ExtendedXMLConfiguration);
+ }
+
+ @Test
+ public void testGlobalLookup() throws Exception
+ {
+ factory.setFileName(GLOBAL_LOOKUP_FILE);
+ CombinedConfiguration cc = factory.getConfiguration(true);
+ String value = cc.getInterpolator().lookup("test:test_key");
+ assertNotNull("The test key was not located", value);
+ assertEquals("Incorrect value retrieved","test.value",value);
+ }
+
+ @Test
+ public void testSystemProperties() throws Exception
+ {
+ factory.setFileName(SYSTEM_PROPS_FILE);
+ factory.getConfiguration(true);
+ String value = System.getProperty("key1");
+ assertNotNull("The test key was not located", value);
+ assertEquals("Incorrect value retrieved","value1",value);
+ }
+
+ @Test
+ public void testValidation() throws Exception
+ {
+ factory.setFileName(VALIDATION_FILE);
+ factory.getConfiguration(true);
+ String value = System.getProperty("key1");
+ assertNotNull("The test key was not located", value);
+ assertEquals("Incorrect value retrieved","value1",value);
+ }
+
+ @Test
+ public void testMultiTenentConfiguration() throws Exception
+ {
+ factory.setFileName(MULTI_TENENT_FILE);
+ System.getProperties().remove("Id");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
+
+ verify("1001", config, 15);
+ verify("1002", config, 25);
+ verify("1003", config, 35);
+ verify("1004", config, 50);
+ verify("1005", config, 50);
+ }
+
+ @Test
+ public void testMultiTenentConfiguration2() throws Exception
+ {
+ factory.setFileName(MULTI_TENENT_FILE);
+ System.setProperty("Id", "1004");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
+
+ verify("1001", config, 15);
+ verify("1002", config, 25);
+ verify("1003", config, 35);
+ verify("1004", config, 50);
+ verify("1005", config, 50);
+ }
+
+ @Test
+ public void testMultiTenentConfiguration3() throws Exception
+ {
+ factory.setFileName(MULTI_TENENT_FILE);
+ System.setProperty("Id", "1005");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
+
+ verify("1001", config, 15);
+ verify("1002", config, 25);
+ verify("1003", config, 35);
+ verify("1004", config, 50);
+ verify("1005", config, 50);
+ }
+
+ @Test
+ public void testFileChanged() throws Exception
+ {
+ // create a new configuration
+ File input = new File("target/test-classes/testMultiConfiguration_1001.xml");
+ FileObject output = getFile(getBasePath() + FILERELOAD_1001_FILE);
+ output.delete();
+ output.getParent().createFolder();
+ copyFile(input, output);
+
+ factory.setFileName(getBasePath() + FILERELOAD2_FILE);
+ System.getProperties().remove("Id");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertNotNull(config);
+ config.addConfigurationListener(this);
+ verify("1001", config, 15);
+
+ // Allow time for FileMonitor to set up.
+ Thread.sleep(1000);
+ XMLConfiguration x = new XMLConfiguration(getBasePath() + FILERELOAD_1001_FILE);
+ x.setProperty("rowsPerPage", "50");
+ x.save();
+ // Let FileMonitor detect the change.
+ //Thread.sleep(2000);
+ waitForChange();
+ verify("1001", config, 50);
+ output.delete();
+ }
+
+ @Test
+ public void testFileChanged2() throws Exception
+ {
+ // create a new configuration
+ File input = new File("target/test-classes/testMultiConfiguration_1002.xml");
+ FileObject output = getFile(getBasePath() + FILERELOAD_1002_FILE);
+ output.delete();
+
+ factory.setFileName(getBasePath() + FILERELOAD2_FILE);
+ System.getProperties().remove("Id");
+
+ CombinedConfiguration config = factory.getConfiguration(true);
+ assertNotNull(config);
+ config.addConfigurationListener(this);
+
+ verify("1002", config, 50);
+ Thread.sleep(1000);
+
+ output.getParent().createFolder();
+ copyFile(input, output);
+
+ // Allow time for the monitor to notice the change.
+ //Thread.sleep(2000);
+ waitForChange();
+ verify("1002", config, 25);
+ output.delete();
+ }
+
+ private void verify(String key, CombinedConfiguration config, int rows)
+ {
+ System.setProperty("Id", key);
+ int actual = config.getInt("rowsPerPage");
+ assertTrue("expected: " + rows + " actual: " + actual, actual == rows);
+ }
+
+ public Map<String, Object> getOptions()
+ {
+ return this.options;
+ }
+
+ /**
+ * Helper method for checking if a save operation was successful. Loads a
+ * saved configuration and then tests against a reference configuration.
+ * @param conf the original configuration
+ * @param newConfig the configuration to check
+ * @throws ConfigurationException if an error occurs
+ */
+ private void checkSavedConfig(XMLConfiguration conf, FileConfiguration newConfig)
+ throws ConfigurationException
+ {
+ newConfig.load();
+ ConfigurationAssert.assertEquals(conf, newConfig);
+ }
+
+ private FileObject getFile(String fileName) throws Exception
+ {
+ FileSystemManager manager = VFS.getManager();
+ FileName file = manager.resolveURI(fileName);
+ FileName base = file.getParent();
+ FileName path = manager.resolveName(base, file.getBaseName());
+ FileSystemOptions opts = new FileSystemOptions();
+ return manager.resolveFile(path.getURI(), opts);
+ }
+
+ private void copyFile(File input, FileObject output) throws IOException
+ {
+ Reader reader = new FileReader(input);
+ Writer writer = new OutputStreamWriter(output.getContent().getOutputStream());
+ char[] buffer = new char[4096];
+ int n = 0;
+ while (-1 != (n = reader.read(buffer)))
+ {
+ writer.write(buffer, 0, n);
+ }
+ reader.close();
+ writer.close();
+ }
+
+ private void waitForChange()
+ {
+ synchronized(this)
+ {
+ try
+ {
+ int count = 0;
+ while (!configChanged && count++ <= 3)
+ {
+ this.wait(5000);
+ }
+ }
+ catch (InterruptedException ie)
+ {
+ throw new IllegalStateException("wait timed out");
+ }
+ finally
+ {
+ configChanged = false;
+ }
+ }
+ }
+
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ if (event.getType() == AbstractFileConfiguration.EVENT_CONFIG_CHANGED)
+ {
+ synchronized(this)
+ {
+ configChanged = true;
+ this.notify();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/commons/configuration/TestXMLConfiguration.java b/src/test/java/org/apache/commons/configuration/TestXMLConfiguration.java
new file mode 100644
index 0000000..da1552e
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestXMLConfiguration.java
@@ -0,0 +1,1948 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.commons.configuration.reloading.FileAlwaysReloadingStrategy;
+import org.apache.commons.configuration.reloading.InvariantReloadingStrategy;
+import org.apache.commons.configuration.resolver.CatalogResolver;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * test for loading and saving xml properties files
+ *
+ * @version $Id: TestXMLConfiguration.java 1534371 2013-10-21 21:07:12Z henning $
+ */
+public class TestXMLConfiguration
+{
+ /** XML Catalog */
+ private static final String CATALOG_FILES = ConfigurationAssert
+ .getTestFile("catalog.xml").getAbsolutePath();
+
+ /** Constant for the used encoding.*/
+ static final String ENCODING = "ISO-8859-1";
+
+ /** Constant for the test system ID.*/
+ static final String SYSTEM_ID = "properties.dtd";
+
+ /** Constant for the test public ID.*/
+ static final String PUBLIC_ID = "-//Commons Configuration//DTD Test Configuration 1.3//EN";
+
+ /** Constant for the DOCTYPE declaration.*/
+ static final String DOCTYPE_DECL = " PUBLIC \"" + PUBLIC_ID + "\" \"" + SYSTEM_ID + "\">";
+
+ /** Constant for the DOCTYPE prefix.*/
+ static final String DOCTYPE = "<!DOCTYPE ";
+
+ /** Constant for the transformer factory property.*/
+ static final String PROP_FACTORY = "javax.xml.transform.TransformerFactory";
+
+ /** The File that we test with */
+ private String testProperties = ConfigurationAssert.getTestFile("test.xml").getAbsolutePath();
+ private String testProperties2 = ConfigurationAssert.getTestFile("testDigesterConfigurationInclude1.xml").getAbsolutePath();
+ private String testBasePath = ConfigurationAssert.TEST_DIR.getAbsolutePath();
+ private File testSaveConf = ConfigurationAssert.getOutFile("testsave.xml");
+ private File testSaveFile = ConfigurationAssert.getOutFile("testsample2.xml");
+ private String testFile2 = ConfigurationAssert.getTestFile("sample.xml").getAbsolutePath();
+
+ /** Constant for the number of test threads. */
+ private static final int THREAD_COUNT = 5;
+
+ /** Constant for the number of loops in tests with multiple threads. */
+ private static final int LOOP_COUNT = 100;
+
+ private XMLConfiguration conf;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ conf = new XMLConfiguration();
+ conf.setFile(new File(testProperties));
+ conf.load();
+ removeTestFile();
+ }
+
+ @Test
+ public void testGetProperty()
+ {
+ assertEquals("value", conf.getProperty("element"));
+ }
+
+ @Test
+ public void testGetCommentedProperty()
+ {
+ assertEquals("", conf.getProperty("test.comment"));
+ }
+
+ @Test
+ public void testGetPropertyWithXMLEntity()
+ {
+ assertEquals("1<2", conf.getProperty("test.entity"));
+ }
+
+ @Test
+ public void testClearProperty() throws Exception
+ {
+ // test non-existent element
+ String key = "clearly";
+ conf.clearProperty(key);
+ assertNull(key, conf.getProperty(key));
+ assertNull(key, conf.getProperty(key));
+
+ // test single element
+ conf.load();
+ key = "clear.element";
+ conf.clearProperty(key);
+ assertNull(key, conf.getProperty(key));
+ assertNull(key, conf.getProperty(key));
+
+ // test single element with attribute
+ conf.load();
+ key = "clear.element2";
+ conf.clearProperty(key);
+ assertNull(key, conf.getProperty(key));
+ assertNull(key, conf.getProperty(key));
+ key = "clear.element2[@id]";
+ assertNotNull(key, conf.getProperty(key));
+ assertNotNull(key, conf.getProperty(key));
+
+ // test non-text/cdata element
+ conf.load();
+ key = "clear.comment";
+ conf.clearProperty(key);
+ assertNull(key, conf.getProperty(key));
+ assertNull(key, conf.getProperty(key));
+
+ // test cdata element
+ conf.load();
+ key = "clear.cdata";
+ conf.clearProperty(key);
+ assertNull(key, conf.getProperty(key));
+ assertNull(key, conf.getProperty(key));
+
+ // test multiple sibling elements
+ conf.load();
+ key = "clear.list.item";
+ conf.clearProperty(key);
+ assertNull(key, conf.getProperty(key));
+ assertNull(key, conf.getProperty(key));
+ key = "clear.list.item[@id]";
+ assertNotNull(key, conf.getProperty(key));
+ assertNotNull(key, conf.getProperty(key));
+
+ // test multiple, disjoined elements
+ conf.load();
+ key = "list.item";
+ conf.clearProperty(key);
+ assertNull(key, conf.getProperty(key));
+ assertNull(key, conf.getProperty(key));
+ }
+
+ @Test
+ public void testgetProperty() {
+ // test non-leaf element
+ Object property = conf.getProperty("clear");
+ assertNull(property);
+
+ // test non-existent element
+ property = conf.getProperty("e");
+ assertNull(property);
+
+ // test non-existent element
+ property = conf.getProperty("element3[@n]");
+ assertNull(property);
+
+ // test single element
+ property = conf.getProperty("element");
+ assertNotNull(property);
+ assertTrue(property instanceof String);
+ assertEquals("value", property);
+
+ // test single attribute
+ property = conf.getProperty("element3[@name]");
+ assertNotNull(property);
+ assertTrue(property instanceof String);
+ assertEquals("foo", property);
+
+ // test non-text/cdata element
+ property = conf.getProperty("test.comment");
+ assertEquals("", property);
+
+ // test cdata element
+ property = conf.getProperty("test.cdata");
+ assertNotNull(property);
+ assertTrue(property instanceof String);
+ assertEquals("<cdata value>", property);
+
+ // test multiple sibling elements
+ property = conf.getProperty("list.sublist.item");
+ assertNotNull(property);
+ assertTrue(property instanceof List);
+ List<?> list = (List<?>) property;
+ assertEquals(2, list.size());
+ assertEquals("five", list.get(0));
+ assertEquals("six", list.get(1));
+
+ // test multiple, disjoined elements
+ property = conf.getProperty("list.item");
+ assertNotNull(property);
+ assertTrue(property instanceof List);
+ list = (List<?>) property;
+ assertEquals(4, list.size());
+ assertEquals("one", list.get(0));
+ assertEquals("two", list.get(1));
+ assertEquals("three", list.get(2));
+ assertEquals("four", list.get(3));
+
+ // test multiple, disjoined attributes
+ property = conf.getProperty("list.item[@name]");
+ assertNotNull(property);
+ assertTrue(property instanceof List);
+ list = (List<?>) property;
+ assertEquals(2, list.size());
+ assertEquals("one", list.get(0));
+ assertEquals("three", list.get(1));
+ }
+
+ @Test
+ public void testGetAttribute()
+ {
+ assertEquals("element3[@name]", "foo", conf.getProperty("element3[@name]"));
+ }
+
+ @Test
+ public void testClearAttribute() throws Exception
+ {
+ // test non-existent attribute
+ String key = "clear[@id]";
+ conf.clearProperty(key);
+ assertNull(key, conf.getProperty(key));
+ assertNull(key, conf.getProperty(key));
+
+ // test single attribute
+ conf.load();
+ key = "clear.element2[@id]";
+ conf.clearProperty(key);
+ assertNull(key, conf.getProperty(key));
+ assertNull(key, conf.getProperty(key));
+ key = "clear.element2";
+ assertNotNull(key, conf.getProperty(key));
+ assertNotNull(key, conf.getProperty(key));
+
+ // test multiple, disjoined attributes
+ conf.load();
+ key = "clear.list.item[@id]";
+ conf.clearProperty(key);
+ assertNull(key, conf.getProperty(key));
+ assertNull(key, conf.getProperty(key));
+ key = "clear.list.item";
+ assertNotNull(key, conf.getProperty(key));
+ assertNotNull(key, conf.getProperty(key));
+ }
+
+ @Test
+ public void testSetAttribute()
+ {
+ // replace an existing attribute
+ conf.setProperty("element3[@name]", "bar");
+ assertEquals("element3[@name]", "bar", conf.getProperty("element3[@name]"));
+
+ // set a new attribute
+ conf.setProperty("foo[@bar]", "value");
+ assertEquals("foo[@bar]", "value", conf.getProperty("foo[@bar]"));
+
+ conf.setProperty("name1","value1");
+ assertEquals("value1",conf.getProperty("name1"));
+ }
+
+ @Test
+ public void testAddAttribute()
+ {
+ conf.addProperty("element3[@name]", "bar");
+
+ List<Object> list = conf.getList("element3[@name]");
+ assertNotNull("null list", list);
+ assertTrue("'foo' element missing", list.contains("foo"));
+ assertTrue("'bar' element missing", list.contains("bar"));
+ assertEquals("list size", 2, list.size());
+ }
+
+ @Test
+ public void testAddObjectAttribute()
+ {
+ conf.addProperty("test.boolean[@value]", Boolean.TRUE);
+ assertTrue("test.boolean[@value]", conf.getBoolean("test.boolean[@value]"));
+ }
+
+ /**
+ * Tests setting an attribute on the root element.
+ */
+ @Test
+ public void testSetRootAttribute() throws ConfigurationException
+ {
+ conf.setProperty("[@test]", "true");
+ assertEquals("Root attribute not set", "true", conf
+ .getString("[@test]"));
+ conf.save(testSaveConf);
+ XMLConfiguration checkConf = new XMLConfiguration();
+ checkConf.setFile(testSaveConf);
+ checkSavedConfig(checkConf);
+ assertTrue("Attribute not found after save", checkConf
+ .containsKey("[@test]"));
+ checkConf.setProperty("[@test]", "newValue");
+ checkConf.save();
+ conf = checkConf;
+ checkConf = new XMLConfiguration();
+ checkConf.setFile(testSaveConf);
+ checkSavedConfig(checkConf);
+ assertEquals("Attribute not modified after save", "newValue", checkConf
+ .getString("[@test]"));
+ }
+
+ /**
+ * Tests whether the configuration's root node is initialized with a
+ * reference to the corresponding XML element.
+ */
+ @Test
+ public void testGetRootReference()
+ {
+ assertNotNull("Root node has no reference", conf.getRootNode()
+ .getReference());
+ }
+
+ @Test
+ public void testAddList()
+ {
+ conf.addProperty("test.array", "value1");
+ conf.addProperty("test.array", "value2");
+
+ List<Object> list = conf.getList("test.array");
+ assertNotNull("null list", list);
+ assertTrue("'value1' element missing", list.contains("value1"));
+ assertTrue("'value2' element missing", list.contains("value2"));
+ assertEquals("list size", 2, list.size());
+ }
+
+ @Test
+ public void testGetComplexProperty()
+ {
+ assertEquals("I'm complex!", conf.getProperty("element2.subelement.subsubelement"));
+ }
+
+ @Test
+ public void testSettingFileNames()
+ {
+ conf = new XMLConfiguration();
+ conf.setFileName(testProperties);
+ assertEquals(testProperties.toString(), conf.getFileName());
+
+ conf.setBasePath(testBasePath);
+ conf.setFileName("hello.xml");
+ assertEquals("hello.xml", conf.getFileName());
+ assertEquals(testBasePath.toString(), conf.getBasePath());
+ assertEquals(new File(testBasePath, "hello.xml"), conf.getFile());
+
+ conf.setBasePath(testBasePath);
+ conf.setFileName("subdir/hello.xml");
+ assertEquals("subdir/hello.xml", conf.getFileName());
+ assertEquals(testBasePath.toString(), conf.getBasePath());
+ assertEquals(new File(testBasePath, "subdir/hello.xml"), conf.getFile());
+ }
+
+ @Test
+ public void testLoad() throws Exception
+ {
+ conf = new XMLConfiguration();
+ conf.setFileName(testProperties);
+ conf.load();
+
+ assertEquals("I'm complex!", conf.getProperty("element2.subelement.subsubelement"));
+ }
+
+ @Test
+ public void testLoadWithBasePath() throws Exception
+ {
+ conf = new XMLConfiguration();
+
+ conf.setFileName("test.xml");
+ conf.setBasePath(testBasePath);
+ conf.load();
+
+ assertEquals("I'm complex!", conf.getProperty("element2.subelement.subsubelement"));
+ }
+
+ /**
+ * Tests constructing an XMLConfiguration from a non existing file and
+ * later saving to this file.
+ */
+ @Test
+ public void testLoadAndSaveFromFile() throws Exception
+ {
+ // If the file does not exist, an empty config is created
+ conf = new XMLConfiguration(testSaveConf);
+ assertTrue(conf.isEmpty());
+ conf.addProperty("test", "yes");
+ conf.save();
+
+ conf = new XMLConfiguration(testSaveConf);
+ assertEquals("yes", conf.getString("test"));
+ }
+
+ /**
+ * Tests loading a configuration from a URL.
+ */
+ @Test
+ public void testLoadFromURL() throws Exception
+ {
+ URL url = new File(testProperties).toURI().toURL();
+ conf = new XMLConfiguration(url);
+ assertEquals("value", conf.getProperty("element"));
+ assertEquals(url, conf.getURL());
+ }
+
+ /**
+ * Tests loading from a stream.
+ */
+ @Test
+ public void testLoadFromStream() throws Exception
+ {
+ String xml = "<?xml version=\"1.0\"?><config><test>1</test></config>";
+ conf = new XMLConfiguration();
+ conf.load(new ByteArrayInputStream(xml.getBytes()));
+ assertEquals(1, conf.getInt("test"));
+
+ conf = new XMLConfiguration();
+ conf.load(new ByteArrayInputStream(xml.getBytes()), "UTF8");
+ assertEquals(1, conf.getInt("test"));
+ }
+
+ /**
+ * Tests loading a non well formed XML from a string.
+ */
+ @Test(expected = ConfigurationException.class)
+ public void testLoadInvalidXML() throws Exception
+ {
+ String xml = "<?xml version=\"1.0\"?><config><test>1</rest></config>";
+ conf = new XMLConfiguration();
+ conf.load(new StringReader(xml));
+ }
+
+ @Test
+ public void testSetProperty() throws Exception
+ {
+ conf.setProperty("element.string", "hello");
+
+ assertEquals("'element.string'", "hello", conf.getString("element.string"));
+ assertEquals("XML value of element.string", "hello", conf.getProperty("element.string"));
+ }
+
+ @Test
+ public void testAddProperty()
+ {
+ // add a property to a non initialized xml configuration
+ XMLConfiguration config = new XMLConfiguration();
+ config.addProperty("test.string", "hello");
+
+ assertEquals("'test.string'", "hello", config.getString("test.string"));
+ }
+
+ @Test
+ public void testAddObjectProperty()
+ {
+ // add a non string property
+ conf.addProperty("test.boolean", Boolean.TRUE);
+ assertTrue("'test.boolean'", conf.getBoolean("test.boolean"));
+ }
+
+ @Test
+ public void testSave() throws Exception
+ {
+ // add an array of strings to the configuration
+ conf.addProperty("string", "value1");
+ for (int i = 1; i < 5; i++)
+ {
+ conf.addProperty("test.array", "value" + i);
+ }
+
+ // add an array of strings in an attribute
+ for (int i = 1; i < 5; i++)
+ {
+ conf.addProperty("test.attribute[@array]", "value" + i);
+ }
+
+ // add comma delimited lists with escaped delimiters
+ conf.addProperty("split.list5", "a\\,b\\,c");
+ conf.setProperty("element3", "value\\,value1\\,value2");
+ conf.setProperty("element3[@name]", "foo\\,bar");
+
+ // save the configuration
+ conf.save(testSaveConf.getAbsolutePath());
+
+ // read the configuration and compare the properties
+ XMLConfiguration checkConfig = new XMLConfiguration();
+ checkConfig.setFileName(testSaveConf.getAbsolutePath());
+ checkSavedConfig(checkConfig);
+ }
+
+ /**
+ * Tests saving to a URL.
+ */
+ @Test
+ public void testSaveToURL() throws Exception
+ {
+ conf.save(testSaveConf.toURI().toURL());
+ XMLConfiguration checkConfig = new XMLConfiguration();
+ checkConfig.setFile(testSaveConf);
+ checkSavedConfig(checkConfig);
+ }
+
+ /**
+ * Tests saving to a stream.
+ */
+ @Test
+ public void testSaveToStream() throws Exception
+ {
+ assertNull(conf.getEncoding());
+ conf.setEncoding("UTF8");
+ FileOutputStream out = null;
+ try
+ {
+ out = new FileOutputStream(testSaveConf);
+ conf.save(out);
+ }
+ finally
+ {
+ if(out != null)
+ {
+ out.close();
+ }
+ }
+
+ XMLConfiguration checkConfig = new XMLConfiguration();
+ checkConfig.setFile(testSaveConf);
+ checkSavedConfig(checkConfig);
+
+ try
+ {
+ out = new FileOutputStream(testSaveConf);
+ conf.save(out, "UTF8");
+ }
+ finally
+ {
+ if(out != null)
+ {
+ out.close();
+ }
+ }
+
+ checkConfig.clear();
+ checkSavedConfig(checkConfig);
+ }
+
+ @Test
+ public void testAutoSave() throws Exception
+ {
+ conf.setFile(testSaveConf);
+ assertFalse(conf.isAutoSave());
+ conf.setAutoSave(true);
+ assertTrue(conf.isAutoSave());
+ conf.setProperty("autosave", "ok");
+
+ // reload the configuration
+ XMLConfiguration conf2 = new XMLConfiguration(conf.getFile());
+ assertEquals("'autosave' property", "ok", conf2.getString("autosave"));
+
+ conf.clearTree("clear");
+ conf2 = new XMLConfiguration(conf.getFile());
+ Configuration sub = conf2.subset("clear");
+ assertTrue(sub.isEmpty());
+ }
+
+ /**
+ * Tests if a second file can be appended to a first.
+ */
+ @Test
+ public void testAppend() throws Exception
+ {
+ conf = new XMLConfiguration();
+ conf.setFileName(testProperties);
+ conf.load();
+ conf.load(testProperties2);
+ assertEquals("value", conf.getString("element"));
+ assertEquals("tasks", conf.getString("table.name"));
+
+ conf.save(testSaveConf);
+ conf = new XMLConfiguration(testSaveConf);
+ assertEquals("value", conf.getString("element"));
+ assertEquals("tasks", conf.getString("table.name"));
+ assertEquals("application", conf.getString("table[@tableType]"));
+ }
+
+ /**
+ * Tests saving attributes (related to issue 34442).
+ */
+ @Test
+ public void testSaveAttributes() throws Exception
+ {
+ conf.clear();
+ conf.load();
+ conf.save(testSaveConf);
+ conf = new XMLConfiguration();
+ conf.load(testSaveConf);
+ assertEquals("foo", conf.getString("element3[@name]"));
+ }
+
+ /**
+ * Tests collaboration between XMLConfiguration and a reloading strategy.
+ */
+ @Test
+ public void testReloading() throws Exception
+ {
+ assertNotNull(conf.getReloadingStrategy());
+ assertTrue(conf.getReloadingStrategy() instanceof InvariantReloadingStrategy);
+ PrintWriter out = null;
+
+ try
+ {
+ out = new PrintWriter(new FileWriter(testSaveConf));
+ out.println("<?xml version=\"1.0\"?><config><test>1</test></config>");
+ out.close();
+ out = null;
+ conf.setFile(testSaveConf);
+ FileAlwaysReloadingStrategy strategy = new FileAlwaysReloadingStrategy();
+ strategy.setRefreshDelay(100);
+ conf.setReloadingStrategy(strategy);
+ assertEquals(strategy, conf.getReloadingStrategy());
+ assertEquals("Wrong file monitored", testSaveConf.getAbsolutePath(),
+ strategy.getMonitoredFile().getAbsolutePath());
+ conf.load();
+ assertEquals(1, conf.getInt("test"));
+
+ out = new PrintWriter(new FileWriter(testSaveConf));
+ out.println("<?xml version=\"1.0\"?><config><test>2</test></config>");
+ out.close();
+ out = null;
+
+ int value = conf.getInt("test");
+ assertEquals("No reloading performed", 2, value);
+ }
+ finally
+ {
+ if (out != null)
+ {
+ out.close();
+ }
+ }
+ }
+
+ @Test
+ public void testReloadingOOM() throws Exception
+ {
+ assertNotNull(conf.getReloadingStrategy());
+ assertTrue(conf.getReloadingStrategy() instanceof InvariantReloadingStrategy);
+ PrintWriter out = null;
+
+ try
+ {
+ out = new PrintWriter(new FileWriter(testSaveConf));
+ out.println("<?xml version=\"1.0\"?><config><test>1</test></config>");
+ out.close();
+ out = null;
+ conf.setFile(testSaveConf);
+ FileAlwaysReloadingStrategy strategy = new FileAlwaysReloadingStrategy();
+ strategy.setRefreshDelay(100);
+ conf.setReloadingStrategy(strategy);
+ conf.load();
+ assertEquals(1, conf.getInt("test"));
+
+ for (int i = 1; i < LOOP_COUNT; ++i)
+ {
+ assertEquals(1, conf.getInt("test"));
+ }
+ }
+ finally
+ {
+ if (out != null)
+ {
+ out.close();
+ }
+ }
+ }
+
+ /**
+ * Tests the refresh() method.
+ */
+ @Test
+ public void testRefresh() throws ConfigurationException
+ {
+ conf.setProperty("element", "anotherValue");
+ conf.refresh();
+ assertEquals("Wrong property after refresh", "value",
+ conf.getString("element"));
+ }
+
+ /**
+ * Tries to call refresh() when the configuration is not associated with a
+ * file.
+ */
+ @Test(expected = ConfigurationException.class)
+ public void testRefreshNoFile() throws ConfigurationException
+ {
+ conf = new XMLConfiguration();
+ conf.refresh();
+ }
+
+ /**
+ * Tests access to tag names with delimiter characters.
+ */
+ @Test
+ public void testComplexNames()
+ {
+ assertEquals("Name with dot", conf.getString("complexNames.my..elem"));
+ assertEquals("Another dot", conf.getString("complexNames.my..elem.sub..elem"));
+ }
+
+ /**
+ * Creates a validating document builder.
+ * @return the document builder
+ * @throws ParserConfigurationException if an error occurs
+ */
+ private DocumentBuilder createValidatingDocBuilder()
+ throws ParserConfigurationException
+ {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setValidating(true);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ builder.setErrorHandler(new DefaultHandler() {
+ @Override
+ public void error(SAXParseException ex) throws SAXException
+ {
+ throw ex;
+ }
+ });
+ return builder;
+ }
+
+ /**
+ * Tests setting a custom document builder.
+ */
+ @Test
+ public void testCustomDocBuilder() throws Exception
+ {
+ // Load an invalid XML file with the default (non validating)
+ // doc builder. This should work...
+ conf = new XMLConfiguration();
+ conf.load(ConfigurationAssert.getTestFile("testValidateInvalid.xml"));
+ assertEquals("customers", conf.getString("table.name"));
+ assertFalse(conf.containsKey("table.fields.field(1).type"));
+ }
+
+ /**
+ * Tests whether a validating document builder detects a validation error.
+ */
+ @Test(expected = ConfigurationException.class)
+ public void testCustomDocBuilderValidationError() throws Exception
+ {
+ DocumentBuilder builder = createValidatingDocBuilder();
+ conf = new XMLConfiguration();
+ conf.setDocumentBuilder(builder);
+ conf.load(new File("conf/testValidateInvalid.xml"));
+ }
+
+ /**
+ * Tests whether a valid document can be loaded with a validating document builder.
+ */
+ @Test
+ public void testCustomDocBuilderValidationSuccess() throws Exception
+ {
+ DocumentBuilder builder = createValidatingDocBuilder();
+ conf = new XMLConfiguration();
+ conf.setDocumentBuilder(builder);
+ conf.load(ConfigurationAssert.getTestFile("testValidateValid.xml"));
+ assertTrue(conf.containsKey("table.fields.field(1).type"));
+ }
+
+ /**
+ * Tests the clone() method.
+ */
+ @Test
+ public void testClone()
+ {
+ Configuration c = (Configuration) conf.clone();
+ assertTrue(c instanceof XMLConfiguration);
+ XMLConfiguration copy = (XMLConfiguration) c;
+ assertNotNull(conf.getDocument());
+ assertNull(copy.getDocument());
+ assertNotNull(conf.getFileName());
+ assertNull(copy.getFileName());
+
+ copy.setProperty("element3", "clonedValue");
+ assertEquals("value", conf.getString("element3"));
+ conf.setProperty("element3[@name]", "originalFoo");
+ assertEquals("foo", copy.getString("element3[@name]"));
+ }
+
+ /**
+ * Tests saving a configuration after cloning to ensure that the clone and
+ * the original are completely detached.
+ */
+ @Test
+ public void testCloneWithSave() throws ConfigurationException
+ {
+ XMLConfiguration c = (XMLConfiguration) conf.clone();
+ c.addProperty("test.newProperty", Boolean.TRUE);
+ conf.addProperty("test.orgProperty", Boolean.TRUE);
+ c.save(testSaveConf);
+ XMLConfiguration c2 = new XMLConfiguration(testSaveConf);
+ assertTrue("New property after clone() was not saved", c2
+ .getBoolean("test.newProperty"));
+ assertFalse("Property of original config was saved", c2
+ .containsKey("test.orgProperty"));
+ }
+
+ /**
+ * Tests the subset() method. There was a bug that calling subset() had
+ * undesired side effects.
+ */
+ @Test
+ public void testSubset() throws ConfigurationException
+ {
+ conf = new XMLConfiguration();
+ conf.load(ConfigurationAssert.getTestFile("testHierarchicalXMLConfiguration.xml"));
+ conf.subset("tables.table(0)");
+ conf.save(testSaveConf);
+
+ conf = new XMLConfiguration(testSaveConf);
+ assertEquals("users", conf.getString("tables.table(0).name"));
+ }
+
+ /**
+ * Tests string properties with list delimiters and escaped delimiters.
+ */
+ @Test
+ public void testSplitLists()
+ {
+ assertEquals("a", conf.getString("split.list3[@values]"));
+ assertEquals(2, conf.getMaxIndex("split.list3[@values]"));
+ assertEquals("a,b,c", conf.getString("split.list4[@values]"));
+ assertEquals("a", conf.getString("split.list1"));
+ assertEquals(2, conf.getMaxIndex("split.list1"));
+ assertEquals("a,b,c", conf.getString("split.list2"));
+ }
+
+ /**
+ * Tests string properties with list delimiters when delimiter parsing
+ * is disabled
+ */
+ @Test
+ public void testDelimiterParsingDisabled() throws ConfigurationException {
+ XMLConfiguration conf2 = new XMLConfiguration();
+ conf2.setDelimiterParsingDisabled(true);
+ conf2.setFile(new File(testProperties));
+ conf2.load();
+
+ assertEquals("a,b,c", conf2.getString("split.list3[@values]"));
+ assertEquals(0, conf2.getMaxIndex("split.list3[@values]"));
+ assertEquals("a\\,b\\,c", conf2.getString("split.list4[@values]"));
+ assertEquals("a,b,c", conf2.getString("split.list1"));
+ assertEquals(0, conf2.getMaxIndex("split.list1"));
+ assertEquals("a\\,b\\,c", conf2.getString("split.list2"));
+ conf2 = new XMLConfiguration();
+ conf2.setExpressionEngine(new XPathExpressionEngine());
+ conf2.setDelimiterParsingDisabled(true);
+ conf2.setFile(new File(testProperties));
+ conf2.load();
+
+ assertEquals("a,b,c", conf2.getString("split/list3/@values"));
+ assertEquals(0, conf2.getMaxIndex("split/list3/@values"));
+ assertEquals("a\\,b\\,c", conf2.getString("split/list4/@values"));
+ assertEquals("a,b,c", conf2.getString("split/list1"));
+ assertEquals(0, conf2.getMaxIndex("split/list1"));
+ assertEquals("a\\,b\\,c", conf2.getString("split/list2"));
+ }
+
+ /**
+ * Tests string properties with list delimiters when delimiter parsing
+ * is disabled
+ */
+ @Test
+ public void testSaveWithDelimiterParsingDisabled() throws ConfigurationException {
+ XMLConfiguration conf = new XMLConfiguration();
+ conf.setExpressionEngine(new XPathExpressionEngine());
+ conf.setDelimiterParsingDisabled(true);
+ conf.setAttributeSplittingDisabled(true);
+ conf.setFile(new File(testProperties));
+ conf.load();
+
+ assertEquals("a,b,c", conf.getString("split/list3/@values"));
+ assertEquals(0, conf.getMaxIndex("split/list3/@values"));
+ assertEquals("a\\,b\\,c", conf.getString("split/list4/@values"));
+ assertEquals("a,b,c", conf.getString("split/list1"));
+ assertEquals(0, conf.getMaxIndex("split/list1"));
+ assertEquals("a\\,b\\,c", conf.getString("split/list2"));
+ // save the configuration
+ conf.save(testSaveConf.getAbsolutePath());
+
+ // read the configuration and compare the properties
+ XMLConfiguration checkConfig = new XMLConfiguration();
+ checkConfig.setFileName(testSaveConf.getAbsolutePath());
+ checkSavedConfig(checkConfig);
+ XMLConfiguration config = new XMLConfiguration();
+ config.setFileName(testFile2);
+ //config.setExpressionEngine(new XPathExpressionEngine());
+ config.setDelimiterParsingDisabled(true);
+ config.setAttributeSplittingDisabled(true);
+ config.load();
+ config.setProperty("Employee[@attr1]", "3,2,1");
+ assertEquals("3,2,1", config.getString("Employee[@attr1]"));
+ config.save(testSaveFile.getAbsolutePath());
+ config = new XMLConfiguration();
+ config.setFileName(testSaveFile.getAbsolutePath());
+ //config.setExpressionEngine(new XPathExpressionEngine());
+ config.setDelimiterParsingDisabled(true);
+ config.setAttributeSplittingDisabled(true);
+ config.load();
+ config.setProperty("Employee[@attr1]", "1,2,3");
+ assertEquals("1,2,3", config.getString("Employee[@attr1]"));
+ config.setProperty("Employee[@attr2]", "one, two, three");
+ assertEquals("one, two, three", config.getString("Employee[@attr2]"));
+ config.setProperty("Employee.text", "a,b,d");
+ assertEquals("a,b,d", config.getString("Employee.text"));
+ config.setProperty("Employee.Salary", "100,000");
+ assertEquals("100,000", config.getString("Employee.Salary"));
+ config.save(testSaveFile.getAbsolutePath());
+ checkConfig = new XMLConfiguration();
+ checkConfig.setFileName(testSaveFile.getAbsolutePath());
+ checkConfig.setExpressionEngine(new XPathExpressionEngine());
+ checkConfig.setDelimiterParsingDisabled(true);
+ checkConfig.setAttributeSplittingDisabled(true);
+ checkConfig.load();
+ assertEquals("1,2,3", checkConfig.getString("Employee/@attr1"));
+ assertEquals("one, two, three", checkConfig.getString("Employee/@attr2"));
+ assertEquals("a,b,d", checkConfig.getString("Employee/text"));
+ assertEquals("100,000", checkConfig.getString("Employee/Salary"));
+ }
+
+ /**
+ * Tests whether a DTD can be accessed.
+ */
+ @Test
+ public void testDtd() throws ConfigurationException
+ {
+ conf = new XMLConfiguration("testDtd.xml");
+ assertEquals("value1", conf.getString("entry(0)"));
+ assertEquals("test2", conf.getString("entry(1)[@key]"));
+ }
+
+ /**
+ * Tests DTD validation using the setValidating() method.
+ */
+ @Test
+ public void testValidating() throws ConfigurationException
+ {
+ File nonValidFile = ConfigurationAssert.getTestFile("testValidateInvalid.xml");
+ conf = new XMLConfiguration();
+ assertFalse(conf.isValidating());
+
+ // Load a non valid XML document. Should work for isValidating() == false
+ conf.load(nonValidFile);
+ assertEquals("customers", conf.getString("table.name"));
+ assertFalse(conf.containsKey("table.fields.field(1).type"));
+
+ // Now set the validating flag to true
+ conf.setValidating(true);
+ try
+ {
+ conf.load(nonValidFile);
+ fail("Validation was not performed!");
+ }
+ catch(ConfigurationException cex)
+ {
+ //ok
+ }
+ }
+
+ /**
+ * Tests handling of empty elements.
+ */
+ @Test
+ public void testEmptyElements() throws ConfigurationException
+ {
+ assertTrue(conf.containsKey("empty"));
+ assertEquals("", conf.getString("empty"));
+ conf.addProperty("empty2", "");
+ conf.setProperty("empty", "no more empty");
+ conf.save(testSaveConf);
+
+ conf = new XMLConfiguration(testSaveConf);
+ assertEquals("no more empty", conf.getString("empty"));
+ assertEquals("", conf.getProperty("empty2"));
+ }
+
+ /**
+ * Tests the isEmpty() method for an empty configuration that was reloaded.
+ */
+ @Test
+ public void testEmptyReload() throws ConfigurationException
+ {
+ XMLConfiguration config = new XMLConfiguration();
+ assertTrue("Newly created configuration not empty", config.isEmpty());
+ config.save(testSaveConf);
+ config.load(testSaveConf);
+ assertTrue("Reloaded configuration not empty", config.isEmpty());
+ }
+
+ /**
+ * Tests whether the encoding is correctly detected by the XML parser. This
+ * is done by loading an XML file with the encoding "UTF-16". If this
+ * encoding is not detected correctly, an exception will be thrown that
+ * "Content is not allowed in prolog". This test case is related to issue
+ * 34204.
+ */
+ @Test
+ public void testLoadWithEncoding() throws ConfigurationException
+ {
+ File file = ConfigurationAssert.getTestFile("testEncoding.xml");
+ conf = new XMLConfiguration();
+ conf.load(file);
+ assertEquals("test3_yoge", conf.getString("yoge"));
+ }
+
+ /**
+ * Tests whether the encoding is written to the generated XML file.
+ */
+ @Test
+ public void testSaveWithEncoding() throws ConfigurationException
+ {
+ conf = new XMLConfiguration();
+ conf.setProperty("test", "a value");
+ conf.setEncoding(ENCODING);
+
+ StringWriter out = new StringWriter();
+ conf.save(out);
+ assertTrue("Encoding was not written to file", out.toString().indexOf(
+ "encoding=\"" + ENCODING + "\"") >= 0);
+ }
+
+ /**
+ * Tests whether a default encoding is used if no specific encoding is set.
+ * According to the XSLT specification (http://www.w3.org/TR/xslt#output)
+ * this should be either UTF-8 or UTF-16.
+ */
+ @Test
+ public void testSaveWithNullEncoding() throws ConfigurationException
+ {
+ conf = new XMLConfiguration();
+ conf.setProperty("testNoEncoding", "yes");
+ conf.setEncoding(null);
+
+ StringWriter out = new StringWriter();
+ conf.save(out);
+ assertTrue("Encoding was written to file", out.toString().indexOf(
+ "encoding=\"UTF-") >= 0);
+ }
+
+ /**
+ * Tests whether the DOCTYPE survives a save operation.
+ */
+ @Test
+ public void testSaveWithDoctype() throws ConfigurationException
+ {
+ String content = "<?xml version=\"1.0\"?>"
+ + DOCTYPE
+ + "properties"
+ + DOCTYPE_DECL
+ + "<properties version=\"1.0\"><entry key=\"test\">value</entry></properties>";
+ StringReader in = new StringReader(content);
+ conf = new XMLConfiguration();
+ conf.setFileName("testDtd.xml");
+ conf.load();
+ conf.clear();
+ conf.load(in);
+
+ assertEquals("Wrong public ID", PUBLIC_ID, conf.getPublicID());
+ assertEquals("Wrong system ID", SYSTEM_ID, conf.getSystemID());
+ StringWriter out = new StringWriter();
+ conf.save(out);
+ assertTrue("Did not find DOCTYPE", out.toString().indexOf(DOCTYPE) >= 0);
+ }
+
+ /**
+ * Tests setting public and system IDs for the D'OCTYPE and then saving the
+ * configuration. This should generate a DOCTYPE declaration.
+ */
+ @Test
+ public void testSaveWithDoctypeIDs() throws ConfigurationException
+ {
+ assertNull("A public ID was found", conf.getPublicID());
+ assertNull("A system ID was found", conf.getSystemID());
+ conf.setPublicID(PUBLIC_ID);
+ conf.setSystemID(SYSTEM_ID);
+ StringWriter out = new StringWriter();
+ conf.save(out);
+ assertTrue("Did not find DOCTYPE", out.toString().indexOf(
+ DOCTYPE + "testconfig" + DOCTYPE_DECL) >= 0);
+ }
+
+ /**
+ * Tests saving a configuration when an invalid transformer factory is
+ * specified. In this case the error thrown by the TransformerFactory class
+ * should be caught and re-thrown as a ConfigurationException.
+ */
+ @Test
+ public void testSaveWithInvalidTransformerFactory()
+ {
+ System.setProperty(PROP_FACTORY, "an.invalid.Class");
+ try
+ {
+ conf.save(testSaveConf);
+ fail("Could save with invalid TransformerFactory!");
+ }
+ catch (ConfigurationException cex)
+ {
+ // ok
+ }
+ finally
+ {
+ System.getProperties().remove(PROP_FACTORY);
+ }
+ }
+
+ /**
+ * Tests if reloads are recognized by subset().
+ */
+ @Test
+ public void testSubsetWithReload() throws ConfigurationException
+ {
+ XMLConfiguration c = setUpReloadTest();
+ Configuration sub = c.subset("test");
+ assertEquals("New value not read", "newValue", sub.getString("entity"));
+ }
+
+ /**
+ * Tests if reloads are recognized by configurationAt().
+ */
+ @Test
+ public void testConfigurationAtWithReload() throws ConfigurationException
+ {
+ XMLConfiguration c = setUpReloadTest();
+ HierarchicalConfiguration sub = c.configurationAt("test(0)");
+ assertEquals("New value not read", "newValue", sub.getString("entity"));
+ }
+
+ /**
+ * Tests if reloads are recognized by configurationsAt().
+ */
+ @Test
+ public void testConfigurationsAtWithReload() throws ConfigurationException
+ {
+ XMLConfiguration c = setUpReloadTest();
+ List<HierarchicalConfiguration> configs = c.configurationsAt("test");
+ assertEquals("New value not read", "newValue",
+ configs.get(0).getString("entity"));
+ }
+
+ /**
+ * Tests whether reloads are recognized when querying the configuration's
+ * keys.
+ */
+ @Test
+ public void testGetKeysWithReload() throws ConfigurationException
+ {
+ XMLConfiguration c = setUpReloadTest();
+ conf.addProperty("aNewKey", "aNewValue");
+ conf.save(testSaveConf);
+ boolean found = false;
+ for (Iterator<String> it = c.getKeys(); it.hasNext();)
+ {
+ if ("aNewKey".equals(it.next()))
+ {
+ found = true;
+ }
+ }
+ assertTrue("Reload not performed", found);
+ }
+
+ /**
+ * Tests accessing properties when the XPATH expression engine is set.
+ */
+ @Test
+ public void testXPathExpressionEngine()
+ {
+ conf.setExpressionEngine(new XPathExpressionEngine());
+ assertEquals("Wrong attribute value", "foo\"bar", conf
+ .getString("test[1]/entity/@name"));
+ conf.clear();
+ assertNull(conf.getString("test[1]/entity/@name"));
+ }
+
+ /**
+ * Tests the copy constructor.
+ */
+ @Test
+ public void testInitCopy() throws ConfigurationException
+ {
+ XMLConfiguration copy = new XMLConfiguration(conf);
+ assertEquals("value", copy.getProperty("element"));
+ assertNull("Document was copied, too", copy.getDocument());
+ ConfigurationNode root = copy.getRootNode();
+ for (ConfigurationNode node : root.getChildren())
+ {
+ assertNull("Reference was not cleared", node.getReference());
+ }
+
+ removeTestFile();
+ copy.setFile(testSaveConf);
+ copy.save();
+ copy.clear();
+ checkSavedConfig(copy);
+ }
+
+ /**
+ * Tests setting text of the root element.
+ */
+ @Test
+ public void testSetTextRootElement() throws ConfigurationException
+ {
+ conf.setProperty("", "Root text");
+ conf.save(testSaveConf);
+ XMLConfiguration copy = new XMLConfiguration();
+ copy.setFile(testSaveConf);
+ checkSavedConfig(copy);
+ }
+
+ /**
+ * Tests removing the text of the root element.
+ */
+ @Test
+ public void testClearTextRootElement() throws ConfigurationException
+ {
+ final String xml = "<e a=\"v\">text</e>";
+ conf.clear();
+ StringReader in = new StringReader(xml);
+ conf.load(in);
+ assertEquals("Wrong text of root", "text", conf.getString(""));
+
+ conf.clearProperty("");
+ conf.save(testSaveConf);
+ XMLConfiguration copy = new XMLConfiguration();
+ copy.setFile(testSaveConf);
+ checkSavedConfig(copy);
+ }
+
+ /**
+ * Tests list nodes with multiple values and attributes.
+ */
+ @Test
+ public void testListWithAttributes()
+ {
+ assertEquals("Wrong number of <a> elements", 6, conf.getList(
+ "attrList.a").size());
+ assertEquals("Wrong value of first element", "ABC", conf
+ .getString("attrList.a(0)"));
+ assertEquals("Wrong value of first name attribute", "x", conf
+ .getString("attrList.a(0)[@name]"));
+ assertEquals("Wrong number of name attributes", 6, conf.getList(
+ "attrList.a[@name]").size());
+ }
+
+ /**
+ * Tests a list node with attributes that has multiple values separated by
+ * the list delimiter. In this scenario the attribute should be added to all
+ * list nodes.
+ */
+ @Test
+ public void testListWithAttributesMultiValue()
+ {
+ assertEquals("Wrong value of 2nd element", "1",
+ conf.getString("attrList.a(1)"));
+ assertEquals("Wrong value of 2nd name attribute", "y",
+ conf.getString("attrList.a(1)[@name]"));
+ for (int i = 1; i <= 3; i++)
+ {
+ assertEquals("Wrong value of element " + (i + 1), i,
+ conf.getInt("attrList.a(" + i + ")"));
+ assertEquals("Wrong name attribute for element " + (i), "y",
+ conf.getString("attrList.a(" + i + ")[@name]"));
+ }
+ }
+
+ /**
+ * Tests a list node with multiple values and multiple attributes. All
+ * attribute values should be assigned to all list nodes.
+ */
+ @Test
+ public void testListWithMultipleAttributesMultiValue()
+ {
+ for (int i = 1; i <= 2; i++)
+ {
+ String idxStr = String.format("(%d)", Integer.valueOf(i + 3));
+ String nodeKey = "attrList.a" + idxStr;
+ assertEquals("Wrong value of multi-valued node", "value" + i,
+ conf.getString(nodeKey));
+ assertEquals("Wrong name attribute at " + i, "u",
+ conf.getString(nodeKey + "[@name]"));
+ assertEquals("Wrong test attribute at " + i, "yes",
+ conf.getString(nodeKey + "[@test]"));
+ }
+ }
+
+ /**
+ * Tests whether the auto save mechanism is triggered by changes at a
+ * subnode configuration.
+ */
+ @Test
+ public void testAutoSaveWithSubnodeConfig() throws ConfigurationException
+ {
+ final String newValue = "I am autosaved";
+ conf.setFile(testSaveConf);
+ conf.setAutoSave(true);
+ Configuration sub = conf.configurationAt("element2.subelement");
+ sub.setProperty("subsubelement", newValue);
+ assertEquals("Change not visible to parent", newValue, conf
+ .getString("element2.subelement.subsubelement"));
+ XMLConfiguration conf2 = new XMLConfiguration(testSaveConf);
+ assertEquals("Change was not saved", newValue, conf2
+ .getString("element2.subelement.subsubelement"));
+ }
+
+ /**
+ * Tests whether a subnode configuration created from another subnode
+ * configuration of a XMLConfiguration can trigger the auto save mechanism.
+ */
+ @Test
+ public void testAutoSaveWithSubSubnodeConfig() throws ConfigurationException
+ {
+ final String newValue = "I am autosaved";
+ conf.setFile(testSaveConf);
+ conf.setAutoSave(true);
+ SubnodeConfiguration sub1 = conf.configurationAt("element2");
+ SubnodeConfiguration sub2 = sub1.configurationAt("subelement");
+ sub2.setProperty("subsubelement", newValue);
+ assertEquals("Change not visible to parent", newValue, conf
+ .getString("element2.subelement.subsubelement"));
+ XMLConfiguration conf2 = new XMLConfiguration(testSaveConf);
+ assertEquals("Change was not saved", newValue, conf2
+ .getString("element2.subelement.subsubelement"));
+ }
+
+ /**
+ * Tests saving and loading a configuration when delimiter parsing is
+ * disabled.
+ */
+ @Test
+ public void testSaveDelimiterParsingDisabled()
+ throws ConfigurationException
+ {
+ checkSaveDelimiterParsingDisabled("list.delimiter.test");
+ }
+
+ /**
+ * Tests saving and loading a configuration when delimiter parsing is
+ * disabled and attributes are involved.
+ */
+ @Test
+ public void testSaveDelimiterParsingDisabledAttrs()
+ throws ConfigurationException
+ {
+ checkSaveDelimiterParsingDisabled("list.delimiter.test[@attr]");
+ }
+
+ /**
+ * Helper method for testing saving and loading a configuration when
+ * delimiter parsing is disabled.
+ *
+ * @param key the key to be checked
+ * @throws ConfigurationException if an error occurs
+ */
+ private void checkSaveDelimiterParsingDisabled(String key)
+ throws ConfigurationException
+ {
+ conf.clear();
+ conf.setDelimiterParsingDisabled(true);
+ conf.load();
+ conf.setProperty(key, "C:\\Temp\\,C:\\Data\\");
+ conf.addProperty(key, "a,b,c");
+ conf.save(testSaveConf);
+ XMLConfiguration checkConf = new XMLConfiguration();
+ checkConf.setDelimiterParsingDisabled(true);
+ checkConf.setFile(testSaveConf);
+ checkSavedConfig(checkConf);
+ }
+
+ /**
+ * Tests multiple attribute values in delimiter parsing disabled mode.
+ */
+ @Test
+ public void testDelimiterParsingDisabledMultiAttrValues() throws ConfigurationException
+ {
+ conf.clear();
+ conf.setDelimiterParsingDisabled(true);
+ conf.load();
+ List<Object> expr = conf.getList("expressions[@value]");
+ assertEquals("Wrong list size", 2, expr.size());
+ assertEquals("Wrong element 1", "a || (b && c)", expr.get(0));
+ assertEquals("Wrong element 2", "!d", expr.get(1));
+ }
+
+ /**
+ * Tests using multiple attribute values, which are partly escaped when
+ * delimiter parsing is not disabled.
+ */
+ @Test
+ public void testMultipleAttrValuesEscaped() throws ConfigurationException
+ {
+ conf.addProperty("test.dir[@name]", "C:\\Temp\\");
+ conf.addProperty("test.dir[@name]", "C:\\Data\\");
+ conf.save(testSaveConf);
+ XMLConfiguration checkConf = new XMLConfiguration();
+ checkConf.setFile(testSaveConf);
+ checkSavedConfig(checkConf);
+ }
+
+ /**
+ * Tests a combination of auto save = true and an associated reloading
+ * strategy.
+ */
+ @Test
+ public void testAutoSaveWithReloadingStrategy() throws ConfigurationException
+ {
+ conf.setFile(testSaveConf);
+ conf.save();
+ conf.setReloadingStrategy(new FileAlwaysReloadingStrategy());
+ conf.setAutoSave(true);
+ assertEquals("Value not found", "value", conf.getProperty("element"));
+ }
+
+ /**
+ * Tests adding nodes from another configuration.
+ */
+ @Test
+ public void testAddNodesCopy() throws ConfigurationException
+ {
+ XMLConfiguration c2 = new XMLConfiguration(testProperties2);
+ conf.addNodes("copiedProperties", c2.getRootNode().getChildren());
+ conf.save(testSaveConf);
+ XMLConfiguration checkConf = new XMLConfiguration();
+ checkConf.setFile(testSaveConf);
+ checkSavedConfig(checkConf);
+ }
+
+ /**
+ * Tests whether the addNodes() method triggers an auto save.
+ */
+ @Test
+ public void testAutoSaveAddNodes() throws ConfigurationException
+ {
+ conf.setFile(testSaveConf);
+ conf.setAutoSave(true);
+ HierarchicalConfiguration.Node node = new HierarchicalConfiguration.Node(
+ "addNodesTest", Boolean.TRUE);
+ Collection<ConfigurationNode> nodes = new ArrayList<ConfigurationNode>(1);
+ nodes.add(node);
+ conf.addNodes("test.autosave", nodes);
+ XMLConfiguration c2 = new XMLConfiguration(testSaveConf);
+ assertTrue("Added nodes are not saved", c2
+ .getBoolean("test.autosave.addNodesTest"));
+ }
+
+ /**
+ * Tests saving a configuration after a node was added. Test for
+ * CONFIGURATION-294.
+ */
+ @Test
+ public void testAddNodesAndSave() throws ConfigurationException
+ {
+ ConfigurationNode node = new HierarchicalConfiguration.Node("test");
+ ConfigurationNode child = new HierarchicalConfiguration.Node("child");
+ node.addChild(child);
+ ConfigurationNode attr = new HierarchicalConfiguration.Node("attr");
+ node.addAttribute(attr);
+ ConfigurationNode node2 = conf.createNode("test2");
+ Collection<ConfigurationNode> nodes = new ArrayList<ConfigurationNode>(2);
+ nodes.add(node);
+ nodes.add(node2);
+ conf.addNodes("add.nodes", nodes);
+ conf.setFile(testSaveConf);
+ conf.save();
+ conf.setProperty("add.nodes.test", "true");
+ conf.setProperty("add.nodes.test.child", "yes");
+ conf.setProperty("add.nodes.test[@attr]", "existing");
+ conf.setProperty("add.nodes.test2", "anotherValue");
+ conf.save();
+ XMLConfiguration c2 = new XMLConfiguration(testSaveConf);
+ assertEquals("Value was not saved", "true", c2
+ .getString("add.nodes.test"));
+ assertEquals("Child value was not saved", "yes", c2
+ .getString("add.nodes.test.child"));
+ assertEquals("Attr value was not saved", "existing", c2
+ .getString("add.nodes.test[@attr]"));
+ assertEquals("Node2 not saved", "anotherValue", c2
+ .getString("add.nodes.test2"));
+ }
+
+ /**
+ * Tests registering the publicId of a DTD.
+ */
+ @Test
+ public void testRegisterEntityId() throws Exception
+ {
+ URL dtdURL = getClass().getResource("/properties.dtd");
+ final String publicId = "http://commons.apache.org/test/properties.dtd";
+ conf = new XMLConfiguration("testDtd.xml");
+ conf.setPublicID(publicId);
+ conf.save(testSaveConf);
+ XMLConfiguration checkConfig = new XMLConfiguration();
+ checkConfig.setFile(testSaveConf);
+ checkConfig.registerEntityId(publicId, dtdURL);
+ checkConfig.setValidating(true);
+ checkSavedConfig(checkConfig);
+ }
+
+ /**
+ * Tries to register a null public ID. This should cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testRegisterEntityIdNull() throws IOException
+ {
+ conf.registerEntityId(null, new URL("http://commons.apache.org"));
+ }
+
+ /**
+ * Tests saving a configuration that was created from a hierarchical
+ * configuration. This test exposes bug CONFIGURATION-301.
+ */
+ @Test
+ public void testSaveAfterCreateWithCopyConstructor()
+ throws ConfigurationException
+ {
+ HierarchicalConfiguration hc = conf.configurationAt("element2");
+ conf = new XMLConfiguration(hc);
+ conf.save(testSaveConf);
+ XMLConfiguration checkConfig = new XMLConfiguration();
+ checkConfig.setFile(testSaveConf);
+ checkSavedConfig(checkConfig);
+ assertEquals("Wrong name of root element", "element2", checkConfig
+ .getRootElementName());
+ }
+
+ /**
+ * Tests whether the name of the root element is copied when a configuration
+ * is created using the copy constructor.
+ */
+ @Test
+ public void testCopyRootName() throws ConfigurationException
+ {
+ final String rootName = "rootElement";
+ final String xml = "<" + rootName + "><test>true</test></" + rootName
+ + ">";
+ conf.clear();
+ conf.load(new StringReader(xml));
+ XMLConfiguration copy = new XMLConfiguration(conf);
+ assertEquals("Wrong name of root element", rootName, copy
+ .getRootElementName());
+ copy.save(testSaveConf);
+ copy = new XMLConfiguration(testSaveConf);
+ assertEquals("Wrong name of root element after save", rootName, copy
+ .getRootElementName());
+ }
+
+ /**
+ * Tests whether the name of the root element is copied for a configuration
+ * for which not yet a document exists.
+ */
+ @Test
+ public void testCopyRootNameNoDocument() throws ConfigurationException
+ {
+ final String rootName = "rootElement";
+ conf = new XMLConfiguration();
+ conf.setRootElementName(rootName);
+ conf.setProperty("test", Boolean.TRUE);
+ XMLConfiguration copy = new XMLConfiguration(conf);
+ assertEquals("Wrong name of root element", rootName, copy
+ .getRootElementName());
+ copy.save(testSaveConf);
+ copy = new XMLConfiguration(testSaveConf);
+ assertEquals("Wrong name of root element after save", rootName, copy
+ .getRootElementName());
+ }
+
+ /**
+ * Tests adding an attribute node using the addNodes() method.
+ */
+ @Test
+ public void testAddNodesAttributeNode()
+ {
+ conf.addProperty("testAddNodes.property[@name]", "prop1");
+ conf.addProperty("testAddNodes.property(0).value", "value1");
+ conf.addProperty("testAddNodes.property(-1)[@name]", "prop2");
+ conf.addProperty("testAddNodes.property(1).value", "value2");
+ Collection<ConfigurationNode> nodes = new ArrayList<ConfigurationNode>();
+ nodes.add(new HierarchicalConfiguration.Node("property"));
+ conf.addNodes("testAddNodes", nodes);
+ nodes.clear();
+ ConfigurationNode nd = new HierarchicalConfiguration.Node("name",
+ "prop3");
+ nd.setAttribute(true);
+ nodes.add(nd);
+ conf.addNodes("testAddNodes.property(2)", nodes);
+ assertEquals("Attribute not added", "prop3", conf
+ .getString("testAddNodes.property(2)[@name]"));
+ }
+
+ /**
+ * Tests whether spaces are preserved when the xml:space attribute is set.
+ */
+ @Test
+ public void testPreserveSpace()
+ {
+ assertEquals("Wrong value of blanc", " ", conf.getString("space.blanc"));
+ assertEquals("Wrong value of stars", " * * ", conf
+ .getString("space.stars"));
+ }
+
+ /**
+ * Tests whether the xml:space attribute works directly on the current
+ * element. This test is related to CONFIGURATION-555.
+ */
+ @Test
+ public void testPreserveSpaceOnElement()
+ {
+ assertEquals("Wrong value", " preserved ", conf.getString("spaceElement"));
+ }
+
+ /**
+ * Tests whether the xml:space attribute can be overridden in nested
+ * elements.
+ */
+ @Test
+ public void testPreserveSpaceOverride()
+ {
+ assertEquals("Not trimmed", "Some text", conf
+ .getString("space.description"));
+ }
+
+ /**
+ * Tests an xml:space attribute with an invalid value. This will be
+ * interpreted as default.
+ */
+ @Test
+ public void testPreserveSpaceInvalid()
+ {
+ assertEquals("Invalid not trimmed", "Some other text", conf
+ .getString("space.testInvalid"));
+ }
+
+ /**
+ * Tests whether attribute splitting can be disabled.
+ */
+ @Test
+ public void testAttributeSplittingDisabled() throws ConfigurationException
+ {
+ List<Object> values = conf.getList("expressions[@value2]");
+ assertEquals("Wrong number of attribute values", 2, values.size());
+ assertEquals("Wrong value 1", "a", values.get(0));
+ assertEquals("Wrong value 2", "b|c", values.get(1));
+ XMLConfiguration conf2 = new XMLConfiguration();
+ conf2.setAttributeSplittingDisabled(true);
+ conf2.setFile(conf.getFile());
+ conf2.load();
+ assertEquals("Attribute was split", "a,b|c", conf2
+ .getString("expressions[@value2]"));
+ }
+
+ /**
+ * Tests disabling both delimiter parsing and attribute splitting.
+ */
+ @Test
+ public void testAttributeSplittingAndDelimiterParsingDisabled()
+ throws ConfigurationException
+ {
+ conf.clear();
+ conf.setDelimiterParsingDisabled(true);
+ conf.load();
+ List<Object> values = conf.getList("expressions[@value2]");
+ assertEquals("Wrong number of attribute values", 2, values.size());
+ assertEquals("Wrong value 1", "a,b", values.get(0));
+ assertEquals("Wrong value 2", "c", values.get(1));
+ XMLConfiguration conf2 = new XMLConfiguration();
+ conf2.setAttributeSplittingDisabled(true);
+ conf2.setDelimiterParsingDisabled(true);
+ conf2.setFile(conf.getFile());
+ conf2.load();
+ assertEquals("Attribute was split", "a,b|c", conf2
+ .getString("expressions[@value2]"));
+ }
+
+ /**
+ * Tests modifying an XML document and saving it with schema validation enabled.
+ */
+ @Test
+ public void testSaveWithValidation() throws Exception
+ {
+ CatalogResolver resolver = new CatalogResolver();
+ resolver.setCatalogFiles(CATALOG_FILES);
+ conf = new XMLConfiguration();
+ conf.setEntityResolver(resolver);
+ conf.setFileName(testFile2);
+ conf.setSchemaValidation(true);
+ conf.load();
+ conf.setProperty("Employee.SSN", "123456789");
+ conf.validate();
+ conf.save(testSaveConf);
+ conf = new XMLConfiguration(testSaveConf);
+ assertEquals("123456789", conf.getString("Employee.SSN"));
+ }
+
+ /**
+ * Tests modifying an XML document and validating it against the schema.
+ */
+ @Test
+ public void testSaveWithValidationFailure() throws Exception
+ {
+ CatalogResolver resolver = new CatalogResolver();
+ resolver.setCatalogFiles(CATALOG_FILES);
+ conf = new XMLConfiguration();
+ conf.setEntityResolver(resolver);
+ conf.setFileName(testFile2);
+ conf.setSchemaValidation(true);
+ conf.load();
+ conf.setProperty("Employee.Email", "JohnDoe at apache.org");
+ try
+ {
+ conf.validate();
+ fail("No validation failure on save");
+ }
+ catch (Exception e)
+ {
+ Throwable cause = e.getCause();
+ assertNotNull("No cause for exception on save", cause);
+ assertTrue("Incorrect exception on save", cause instanceof SAXParseException);
+ }
+ }
+
+ @Test
+ public void testConcurrentGetAndReload() throws Exception
+ {
+ //final FileConfiguration config = new PropertiesConfiguration("test.properties");
+ final FileConfiguration config = new XMLConfiguration("test.xml");
+ config.setReloadingStrategy(new FileAlwaysReloadingStrategy());
+
+ assertTrue("Property not found", config.getProperty("test.short") != null);
+
+ Thread testThreads[] = new Thread[THREAD_COUNT];
+
+ for (int i = 0; i < testThreads.length; ++i)
+ {
+ testThreads[i] = new ReloadThread(config);
+ testThreads[i].start();
+ }
+
+ for (int i = 0; i < LOOP_COUNT; i++)
+ {
+ assertTrue("Property not found", config.getProperty("test.short") != null);
+ }
+
+ for (int i = 0; i < testThreads.length; ++i)
+ {
+ testThreads[i].join();
+ }
+ }
+
+ /**
+ * Tests whether a windows path can be saved correctly. This test is related
+ * to CONFIGURATION-428.
+ */
+ @Test
+ public void testSaveWindowsPath() throws ConfigurationException
+ {
+ conf.clear();
+ conf.addProperty("path", "C:\\Temp");
+ StringWriter writer = new StringWriter();
+ conf.save(writer);
+ String content = writer.toString();
+ assertTrue("Path not found: " + content,
+ content.indexOf("<path>C:\\Temp</path>") >= 0);
+ conf.save(testSaveFile);
+ XMLConfiguration conf2 = new XMLConfiguration(testSaveFile);
+ assertEquals("Wrong windows path", "C:\\Temp",
+ conf2.getString("path"));
+ }
+
+ /**
+ * Tests whether an attribute can be set to an empty string. This test is
+ * related to CONFIGURATION-446.
+ */
+ @Test
+ public void testEmptyAttribute() throws ConfigurationException
+ {
+ String key = "element3[@value]";
+ conf.setProperty(key, "");
+ assertTrue("Key not found", conf.containsKey(key));
+ assertEquals("Wrong value", "", conf.getString(key));
+ conf.save(testSaveConf);
+ conf = new XMLConfiguration();
+ conf.load(testSaveConf);
+ assertTrue("Key not found after save", conf.containsKey(key));
+ assertEquals("Wrong value after save", "", conf.getString(key));
+ }
+
+ /**
+ * Tests whether it is possible to add nodes to a XMLConfiguration through a
+ * SubnodeConfiguration and whether these nodes have the correct type. This
+ * test is related to CONFIGURATION-472.
+ */
+ @Test
+ public void testAddNodesToSubnodeConfiguration() throws Exception
+ {
+ SubnodeConfiguration sub = conf.configurationAt("element2");
+ sub.addProperty("newKey", "newvalue");
+ ConfigurationNode root = conf.getRootNode();
+ ConfigurationNode elem = root.getChildren("element2").get(0);
+ ConfigurationNode newNode = elem.getChildren("newKey").get(0);
+ assertTrue("Wrong node type: " + newNode,
+ newNode instanceof XMLConfiguration.XMLNode);
+ }
+
+ /**
+ * Tests whether list properties are set correctly if delimiter
+ * parsing is disabled. This test is related to CONFIGURATION-495.
+ */
+ @Test
+ public void testSetPropertyListWithDelimiterParsingDisabled()
+ throws ConfigurationException
+ {
+ String prop = "delimiterListProp";
+ conf.setDelimiterParsingDisabled(true);
+ List<String> list = Arrays.asList("val", "val2", "val3");
+ conf.setProperty(prop, list);
+ conf.setFile(testSaveFile);
+ conf.save();
+ conf.clear();
+ conf.load();
+ assertEquals("Wrong list property", list, conf.getProperty(prop));
+ }
+
+ /**
+ * Tests whether list properties are added correctly if delimiter parsing is
+ * disabled. This test is related to CONFIGURATION-495.
+ */
+ @Test
+ public void testAddPropertyListWithDelimiterParsingDisabled()
+ throws ConfigurationException
+ {
+ String prop = "delimiterListProp";
+ conf.setDelimiterParsingDisabled(true);
+ List<String> list = Arrays.asList("val", "val2", "val3");
+ conf.addProperty(prop, list);
+ conf.setFile(testSaveFile);
+ conf.save();
+ conf.clear();
+ conf.load();
+ assertEquals("Wrong list property", list, conf.getProperty(prop));
+ }
+
+ /**
+ * Prepares a configuration object for testing a reload operation.
+ *
+ * @return the initialized configuration
+ * @throws ConfigurationException if an error occurs
+ */
+ private XMLConfiguration setUpReloadTest() throws ConfigurationException
+ {
+ removeTestFile();
+ conf.save(testSaveConf);
+ XMLConfiguration c = new XMLConfiguration(testSaveConf);
+ c.setReloadingStrategy(new FileAlwaysReloadingStrategy());
+ conf.setProperty("test(0).entity", "newValue");
+ conf.save(testSaveConf);
+ return c;
+ }
+
+ /**
+ * Removes the test output file if it exists.
+ */
+ private void removeTestFile()
+ {
+ if (testSaveConf.exists())
+ {
+ assertTrue(testSaveConf.delete());
+ }
+ }
+
+ /**
+ * Helper method for checking if a save operation was successful. Loads a
+ * saved configuration and then tests against a reference configuration.
+ * @param checkConfig the configuration to check
+ * @throws ConfigurationException if an error occurs
+ */
+ private void checkSavedConfig(FileConfiguration checkConfig) throws ConfigurationException
+ {
+ checkConfig.load();
+ ConfigurationAssert.assertEquals(conf, checkConfig);
+ }
+
+ private class ReloadThread extends Thread
+ {
+ FileConfiguration config;
+
+ ReloadThread(FileConfiguration config)
+ {
+ this.config = config;
+ }
+ @Override
+ public void run()
+ {
+ for (int i = 0; i < LOOP_COUNT; i++)
+ {
+ config.reload();
+ }
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/TestXMLPropertiesConfiguration.java b/src/test/java/org/apache/commons/configuration/TestXMLPropertiesConfiguration.java
new file mode 100644
index 0000000..8006fe2
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/TestXMLPropertiesConfiguration.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.net.URL;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.Result;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.junit.Test;
+import org.w3c.dom.Document;
+
+/**
+ * @author Emmanuel Bourg
+ * @version $Id: TestXMLPropertiesConfiguration.java 1534399 2013-10-21 22:25:03Z henning $
+ */
+public class TestXMLPropertiesConfiguration
+{
+ @Test
+ public void testLoad() throws Exception
+ {
+ XMLPropertiesConfiguration conf = new XMLPropertiesConfiguration("test.properties.xml");
+
+ assertEquals("header", "Description of the property list", conf.getHeader());
+
+ assertFalse("The configuration is empty", conf.isEmpty());
+ assertEquals("'key1' property", "value1", conf.getProperty("key1"));
+ assertEquals("'key2' property", "value2", conf.getProperty("key2"));
+ assertEquals("'key3' property", "value3", conf.getProperty("key3"));
+ }
+
+ @Test
+ public void testDOMLoad() throws Exception
+ {
+ URL location = ConfigurationUtils.locate(FileSystem.getDefaultFileSystem(), null, "test.properties.xml");
+ DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
+ File file = new File(location.toURI());
+ Document doc = dBuilder.parse(file);
+ XMLPropertiesConfiguration conf = new XMLPropertiesConfiguration(doc.getDocumentElement());
+
+ assertEquals("header", "Description of the property list", conf.getHeader());
+
+ assertFalse("The configuration is empty", conf.isEmpty());
+ assertEquals("'key1' property", "value1", conf.getProperty("key1"));
+ assertEquals("'key2' property", "value2", conf.getProperty("key2"));
+ assertEquals("'key3' property", "value3", conf.getProperty("key3"));
+ }
+
+ @Test
+ public void testSave() throws Exception
+ {
+ // load the configuration
+ XMLPropertiesConfiguration conf = new XMLPropertiesConfiguration("test.properties.xml");
+
+ // update the configuration
+ conf.addProperty("key4", "value4");
+ conf.clearProperty("key2");
+ conf.setHeader("Description of the new property list");
+
+ // save the configuration
+ File saveFile = new File("target/test2.properties.xml");
+ if (saveFile.exists())
+ {
+ assertTrue(saveFile.delete());
+ }
+ conf.save(saveFile);
+
+ // reload the configuration
+ XMLPropertiesConfiguration conf2 = new XMLPropertiesConfiguration(saveFile);
+
+ // test the configuration
+ assertEquals("header", "Description of the new property list", conf2.getHeader());
+
+ assertFalse("The configuration is empty", conf2.isEmpty());
+ assertEquals("'key1' property", "value1", conf2.getProperty("key1"));
+ assertEquals("'key3' property", "value3", conf2.getProperty("key3"));
+ assertEquals("'key4' property", "value4", conf2.getProperty("key4"));
+ }
+
+ @Test
+ public void testDOMSave() throws Exception
+ {
+ // load the configuration
+ XMLPropertiesConfiguration conf = new XMLPropertiesConfiguration("test.properties.xml");
+
+ // update the configuration
+ conf.addProperty("key4", "value4");
+ conf.clearProperty("key2");
+ conf.setHeader("Description of the new property list");
+
+ // save the configuration
+ File saveFile = new File("target/test2.properties.xml");
+ if (saveFile.exists())
+ {
+ assertTrue(saveFile.delete());
+ }
+
+ // save as DOM into saveFile
+ DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
+ Document document = dBuilder.newDocument();
+ conf.save(document, document);
+ TransformerFactory tFactory = TransformerFactory.newInstance();
+ Transformer transformer = tFactory.newTransformer();
+ DOMSource source = new DOMSource(document);
+ Result result = new StreamResult(saveFile);
+ transformer.transform(source, result);
+
+ // reload the configuration
+ XMLPropertiesConfiguration conf2 = new XMLPropertiesConfiguration(saveFile);
+
+ // test the configuration
+ assertEquals("header", "Description of the new property list", conf2.getHeader());
+
+ assertFalse("The configuration is empty", conf2.isEmpty());
+ assertEquals("'key1' property", "value1", conf2.getProperty("key1"));
+ assertEquals("'key3' property", "value3", conf2.getProperty("key3"));
+ assertEquals("'key4' property", "value4", conf2.getProperty("key4"));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/beanutils/BeanCreationTestBean.java b/src/test/java/org/apache/commons/configuration/beanutils/BeanCreationTestBean.java
new file mode 100644
index 0000000..d8b29f6
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/beanutils/BeanCreationTestBean.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.beanutils;
+
+/**
+ * A simple bean class used for testing bean creation operations.
+ *
+ * @version $Id: BeanCreationTestBean.java 1534393 2013-10-21 22:02:27Z henning $
+ */
+public class BeanCreationTestBean
+{
+ private String stringValue;
+
+ private int intValue;
+
+ private BeanCreationTestBean buddy;
+
+ public BeanCreationTestBean getBuddy()
+ {
+ return buddy;
+ }
+
+ public void setBuddy(BeanCreationTestBean buddy)
+ {
+ this.buddy = buddy;
+ }
+
+ public int getIntValue()
+ {
+ return intValue;
+ }
+
+ public void setIntValue(int intValue)
+ {
+ this.intValue = intValue;
+ }
+
+ public String getStringValue()
+ {
+ return stringValue;
+ }
+
+ public void setStringValue(String stringValue)
+ {
+ this.stringValue = stringValue;
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/beanutils/BeanCreationTestBeanWithListChild.java b/src/test/java/org/apache/commons/configuration/beanutils/BeanCreationTestBeanWithListChild.java
new file mode 100644
index 0000000..40d3113
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/beanutils/BeanCreationTestBeanWithListChild.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.beanutils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A simple bean class used for testing bean creation operations that has
+ * a list of children of a different bean type.
+ *
+ * @version $Id: BeanCreationTestBeanWithListChild.java 1534410 2013-10-21 23:13:34Z henning $
+ */
+public class BeanCreationTestBeanWithListChild
+{
+ private String stringValue;
+
+ private int intValue;
+
+ private List<BeanCreationTestBean> children = new ArrayList<BeanCreationTestBean>();
+
+ public List<BeanCreationTestBean> getChildren()
+ {
+ return children;
+ }
+
+ public void setChildren(List<BeanCreationTestBean> buddies)
+ {
+ this.children.clear();
+ this.children.addAll(buddies);
+ }
+
+ public int getIntValue()
+ {
+ return intValue;
+ }
+
+ public void setIntValue(int intValue)
+ {
+ this.intValue = intValue;
+ }
+
+ public String getStringValue()
+ {
+ return stringValue;
+ }
+
+ public void setStringValue(String stringValue)
+ {
+ this.stringValue = stringValue;
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/beanutils/TestBeanHelper.java b/src/test/java/org/apache/commons/configuration/beanutils/TestBeanHelper.java
new file mode 100644
index 0000000..0ff1032
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/beanutils/TestBeanHelper.java
@@ -0,0 +1,613 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.beanutils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.configuration.ConfigurationRuntimeException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for BeanHelper.
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestBeanHelper.java 1534393 2013-10-21 22:02:27Z henning $
+ */
+public class TestBeanHelper
+{
+ /** Constant for the test value of the string property. */
+ private static final String TEST_STRING = "testString";
+
+ /** Constant for the test value of the numeric property. */
+ private static final int TEST_INT = 42;
+
+ /** Constant for the name of the test bean factory. */
+ private static final String TEST_FACTORY = "testFactory";
+
+ /**
+ * Stores the default bean factory. Because this is a static field in
+ * BeanHelper it is temporarily stored and reset after the tests.
+ */
+ private BeanFactory tempDefaultBeanFactory;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ tempDefaultBeanFactory = BeanHelper.getDefaultBeanFactory();
+ deregisterFactories();
+ }
+
+ @After
+ public void tearDown() throws Exception
+ {
+ deregisterFactories();
+
+ // Reset old default bean factory
+ BeanHelper.setDefaultBeanFactory(tempDefaultBeanFactory);
+ }
+
+ /**
+ * Removes all bean factories that might have been registered during a test.
+ */
+ private void deregisterFactories()
+ {
+ for (String name : BeanHelper.registeredFactoryNames())
+ {
+ BeanHelper.deregisterBeanFactory(name);
+ }
+ assertTrue("Remaining registered bean factories", BeanHelper
+ .registeredFactoryNames().isEmpty());
+ }
+
+ /**
+ * Tests registering a new bean factory.
+ */
+ @Test
+ public void testRegisterBeanFactory()
+ {
+ assertTrue("List of registered factories is not empty", BeanHelper
+ .registeredFactoryNames().isEmpty());
+ BeanHelper.registerBeanFactory(TEST_FACTORY, new TestBeanFactory());
+ assertEquals("Wrong number of registered factories", 1, BeanHelper
+ .registeredFactoryNames().size());
+ assertTrue("Test factory is not contained", BeanHelper
+ .registeredFactoryNames().contains(TEST_FACTORY));
+ }
+
+ /**
+ * Tries to register a null factory. This should cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testRegisterBeanFactoryNull()
+ {
+ BeanHelper.registerBeanFactory(TEST_FACTORY, null);
+ }
+
+ /**
+ * Tries to register a bean factory with a null name. This should cause an
+ * exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testRegisterBeanFactoryNullName()
+ {
+ BeanHelper.registerBeanFactory(null, new TestBeanFactory());
+ }
+
+ /**
+ * Tests to deregister a bean factory.
+ */
+ @Test
+ public void testDeregisterBeanFactory()
+ {
+ assertNull("deregistering non existing factory", BeanHelper
+ .deregisterBeanFactory(TEST_FACTORY));
+ assertNull("deregistering null factory", BeanHelper
+ .deregisterBeanFactory(null));
+ BeanFactory factory = new TestBeanFactory();
+ BeanHelper.registerBeanFactory(TEST_FACTORY, factory);
+ assertSame("Could not deregister factory", factory, BeanHelper
+ .deregisterBeanFactory(TEST_FACTORY));
+ assertTrue("List of factories is not empty", BeanHelper
+ .registeredFactoryNames().isEmpty());
+ }
+
+ /**
+ * Tests whether the default bean factory is correctly initialized.
+ */
+ @Test
+ public void testGetDefaultBeanFactory()
+ {
+ assertSame("Incorrect default bean factory",
+ DefaultBeanFactory.INSTANCE, tempDefaultBeanFactory);
+ }
+
+ /**
+ * Tests setting the default bean factory to null. This should caus an
+ * exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetDefaultBeanFactoryNull()
+ {
+ BeanHelper.setDefaultBeanFactory(null);
+ }
+
+ /**
+ * Tests initializing a bean.
+ */
+ @Test
+ public void testInitBean()
+ {
+ BeanHelper.setDefaultBeanFactory(new TestBeanFactory());
+ TestBeanDeclaration data = setUpBeanDeclaration();
+ TestBean bean = new TestBean();
+ BeanHelper.initBean(bean, data);
+ checkBean(bean);
+ }
+
+ /**
+ * Tests initializing a bean when the bean declaration does not contain any
+ * data.
+ */
+ @Test
+ public void testInitBeanWithNoData()
+ {
+ TestBeanDeclaration data = new TestBeanDeclaration();
+ TestBean bean = new TestBean();
+ BeanHelper.initBean(bean, data);
+ assertNull("Wrong string property", bean.getStringValue());
+ assertEquals("Wrong int property", 0, bean.getIntValue());
+ assertNull("Buddy was set", bean.getBuddy());
+ }
+
+ /**
+ * Tries to initialize a bean with a bean declaration that contains an
+ * invalid property value. This should cause an exception.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testInitBeanWithInvalidProperty()
+ {
+ TestBeanDeclaration data = setUpBeanDeclaration();
+ data.getBeanProperties().put("nonExistingProperty", Boolean.TRUE);
+ BeanHelper.initBean(new TestBean(), data);
+ }
+
+ /**
+ * Tests creating a bean. All necessary information is stored in the bean
+ * declaration.
+ */
+ @Test
+ public void testCreateBean()
+ {
+ TestBeanFactory factory = new TestBeanFactory();
+ BeanHelper.registerBeanFactory(TEST_FACTORY, factory);
+ TestBeanDeclaration data = setUpBeanDeclaration();
+ data.setBeanFactoryName(TEST_FACTORY);
+ data.setBeanClassName(TestBean.class.getName());
+ checkBean((TestBean) BeanHelper.createBean(data, null));
+ assertNull("A parameter was passed", factory.parameter);
+ }
+
+ @Test
+ public void testCreateBeanWithListChildBean()
+ {
+ TestBeanFactory factory = new TestBeanFactory();
+ BeanHelper.registerBeanFactory(TEST_FACTORY, factory);
+ TestBeanDeclaration data = setUpBeanDeclarationWithListChild();
+ data.setBeanFactoryName(TEST_FACTORY);
+ data.setBeanClassName(BeanCreationTestBeanWithListChild.class.getName());
+ checkBean((BeanCreationTestBeanWithListChild) BeanHelper.createBean(data, null));
+ assertNull("A parameter was passed", factory.parameter);
+ }
+
+ /**
+ * Tests creating a bean when no bean declaration is provided. This should
+ * cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateBeanWithNullDeclaration()
+ {
+ BeanHelper.createBean(null);
+ }
+
+ /**
+ * Tests creating a bean. The bean's class is specified as the default class
+ * argument.
+ */
+ @Test
+ public void testCreateBeanWithDefaultClass()
+ {
+ BeanHelper.registerBeanFactory(TEST_FACTORY, new TestBeanFactory());
+ TestBeanDeclaration data = setUpBeanDeclaration();
+ data.setBeanFactoryName(TEST_FACTORY);
+ checkBean((TestBean) BeanHelper.createBean(data, TestBean.class));
+ }
+
+ /**
+ * Tests creating a bean when the bean's class is specified as the default
+ * class of the bean factory.
+ */
+ @Test
+ public void testCreateBeanWithFactoryDefaultClass()
+ {
+ TestBeanFactory factory = new TestBeanFactory();
+ factory.supportsDefaultClass = true;
+ BeanHelper.registerBeanFactory(TEST_FACTORY, factory);
+ TestBeanDeclaration data = setUpBeanDeclaration();
+ data.setBeanFactoryName(TEST_FACTORY);
+ checkBean((TestBean) BeanHelper.createBean(data, null));
+ }
+
+ /**
+ * Tries to create a bean when no class is provided. This should cause an
+ * exception.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testCreateBeanWithNoClass()
+ {
+ BeanHelper.registerBeanFactory(TEST_FACTORY, new TestBeanFactory());
+ TestBeanDeclaration data = setUpBeanDeclaration();
+ data.setBeanFactoryName(TEST_FACTORY);
+ BeanHelper.createBean(data, null);
+ }
+
+ /**
+ * Tries to create a bean with a non existing class. This should cause an
+ * exception.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testCreateBeanWithInvalidClass()
+ {
+ BeanHelper.registerBeanFactory(TEST_FACTORY, new TestBeanFactory());
+ TestBeanDeclaration data = setUpBeanDeclaration();
+ data.setBeanFactoryName(TEST_FACTORY);
+ data.setBeanClassName("non.existing.ClassName");
+ BeanHelper.createBean(data, null);
+ }
+
+ /**
+ * Tests creating a bean using the default bean factory.
+ */
+ @Test
+ public void testCreateBeanWithDefaultFactory()
+ {
+ BeanHelper.setDefaultBeanFactory(new TestBeanFactory());
+ TestBeanDeclaration data = setUpBeanDeclaration();
+ data.setBeanClassName(TestBean.class.getName());
+ checkBean((TestBean) BeanHelper.createBean(data, null));
+ }
+
+ /**
+ * Tests creating a bean using a non registered factory.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testCreateBeanWithUnknownFactory()
+ {
+ TestBeanDeclaration data = setUpBeanDeclaration();
+ data.setBeanFactoryName(TEST_FACTORY);
+ data.setBeanClassName(TestBean.class.getName());
+ BeanHelper.createBean(data, null);
+ }
+
+ /**
+ * Tests creating a bean when the factory throws an exception.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testCreateBeanWithException()
+ {
+ BeanHelper.registerBeanFactory(TEST_FACTORY, new TestBeanFactory());
+ TestBeanDeclaration data = setUpBeanDeclaration();
+ data.setBeanFactoryName(TEST_FACTORY);
+ data.setBeanClassName(getClass().getName());
+ BeanHelper.createBean(data, null);
+ }
+
+ /**
+ * Tests if a parameter is correctly passed to the bean factory.
+ */
+ @Test
+ public void testCreateBeanWithParameter()
+ {
+ Object param = new Integer(42);
+ TestBeanFactory factory = new TestBeanFactory();
+ BeanHelper.registerBeanFactory(TEST_FACTORY, factory);
+ TestBeanDeclaration data = setUpBeanDeclaration();
+ data.setBeanFactoryName(TEST_FACTORY);
+ data.setBeanClassName(TestBean.class.getName());
+ checkBean((TestBean) BeanHelper.createBean(data, null, param));
+ assertSame("Wrong parameter", param, factory.parameter);
+ }
+
+ /**
+ * Returns an initialized bean declaration.
+ *
+ * @return the bean declaration
+ */
+ private TestBeanDeclaration setUpBeanDeclaration()
+ {
+ TestBeanDeclaration data = new TestBeanDeclaration();
+ Map<String, Object> properties = new HashMap<String, Object>();
+ properties.put("stringValue", "testString");
+ properties.put("intValue", "42");
+ data.setBeanProperties(properties);
+ TestBeanDeclaration buddyData = new TestBeanDeclaration();
+ Map<String, Object> properties2 = new HashMap<String, Object>();
+ properties2.put("stringValue", "Another test string");
+ properties2.put("intValue", new Integer(100));
+ buddyData.setBeanProperties(properties2);
+ buddyData.setBeanClassName(TestBean.class.getName());
+ if (BeanHelper.getDefaultBeanFactory() == null)
+ {
+ buddyData.setBeanFactoryName(TEST_FACTORY);
+ }
+
+ Map<String, Object> nested = new HashMap<String, Object>();
+ nested.put("buddy", buddyData);
+ data.setNestedBeanDeclarations(nested);
+ return data;
+ }
+
+ /**
+ * Same as setUpBeanDeclaration, but returns a nested array of beans
+ * as a single property. Tests multi-value (Collection<BeanDeclaration>)
+ * children construction.
+ *
+ * @return The bean declaration with a list child bean proerty
+ */
+ private TestBeanDeclaration setUpBeanDeclarationWithListChild()
+ {
+ TestBeanDeclaration data = new TestBeanDeclaration();
+ Map<String, Object> properties = new HashMap<String, Object>();
+ properties.put("stringValue", TEST_STRING);
+ properties.put("intValue", String.valueOf(TEST_INT));
+ data.setBeanProperties(properties);
+
+ List<BeanDeclaration> childData = new ArrayList<BeanDeclaration>();
+ childData.add(createChildBean("child1"));
+ childData.add(createChildBean("child2"));
+ Map<String, Object> nested = new HashMap<String, Object>();
+ nested.put("children", childData);
+ data.setNestedBeanDeclarations(nested);
+ return data;
+ }
+
+ /**
+ * Create a simple bean declaration that has no children for testing
+ * of nested children bean declarations.
+ *
+ * @param name A name prefix that can be used to disambiguate the children
+ * @return A simple declaration
+ */
+ private TestBeanDeclaration createChildBean(String name)
+ {
+ TestBeanDeclaration childBean = new TestBeanDeclaration();
+ Map<String, Object> properties2 = new HashMap<String, Object>();
+ properties2.put("stringValue", name + " Another test string");
+ properties2.put("intValue", new Integer(100));
+ childBean.setBeanProperties(properties2);
+ childBean.setBeanClassName(BeanCreationTestBean.class.getName());
+ if (BeanHelper.getDefaultBeanFactory() == null)
+ {
+ childBean.setBeanFactoryName(TEST_FACTORY);
+ }
+
+ return childBean;
+ }
+
+ /**
+ * Tests if the bean was correctly initialized from the data of the test
+ * bean declaration.
+ *
+ * @param bean the bean to be checked
+ */
+ private void checkBean(TestBean bean)
+ {
+ assertEquals("Wrong string property", "testString", bean
+ .getStringValue());
+ assertEquals("Wrong int property", 42, bean.getIntValue());
+ TestBean buddy = bean.getBuddy();
+ assertNotNull("Buddy was not set", buddy);
+ assertEquals("Wrong string property in buddy", "Another test string",
+ buddy.getStringValue());
+ assertEquals("Wrong int property in buddy", 100, buddy.getIntValue());
+ }
+
+ /**
+ * A simple bean class used for testing creation operations.
+ */
+ public static class TestBean
+ {
+ private String stringValue;
+
+ private int intValue;
+
+ private TestBean buddy;
+
+ public TestBean getBuddy()
+ {
+ return buddy;
+ }
+
+ public void setBuddy(TestBean buddy)
+ {
+ this.buddy = buddy;
+ }
+
+ public int getIntValue()
+ {
+ return intValue;
+ }
+
+ public void setIntValue(int intValue)
+ {
+ this.intValue = intValue;
+ }
+
+ public String getStringValue()
+ {
+ return stringValue;
+ }
+
+ public void setStringValue(String stringValue)
+ {
+ this.stringValue = stringValue;
+ }
+ }
+
+ /**
+ * Tests if the bean was correctly initialized from the data of the test
+ * bean declaration.
+ *
+ * @param bean the bean to be checked
+ */
+ private void checkBean(BeanCreationTestBeanWithListChild bean)
+ {
+ assertEquals("Wrong string property", TEST_STRING, bean
+ .getStringValue());
+ assertEquals("Wrong int property", TEST_INT, bean.getIntValue());
+ List<BeanCreationTestBean> children = bean.getChildren();
+ assertNotNull("Children were not set", children);
+ assertEquals("Wrong number of children created", children.size(), 2);
+ assertNotNull("First child was set as null", children.get(0));
+ assertNotNull("Second child was set as null", children.get(1));
+ }
+
+ /**
+ * An implementation of the BeanFactory interface used for testing. This
+ * implementation is really simple: If the TestBean class is provided, a new
+ * instance will be created. Otherwise an exception is thrown.
+ */
+ static class TestBeanFactory implements BeanFactory
+ {
+ Object parameter;
+
+ boolean supportsDefaultClass;
+
+ public Object createBean(Class<?> beanClass, BeanDeclaration data, Object param)
+ throws Exception
+ {
+ parameter = param;
+ if (TestBean.class.equals(beanClass))
+ {
+ TestBean bean = new TestBean();
+ BeanHelper.initBean(bean, data);
+ return bean;
+ }
+ else if (BeanCreationTestBeanWithListChild.class.equals(beanClass))
+ {
+ BeanCreationTestBeanWithListChild bean = new BeanCreationTestBeanWithListChild();
+ BeanHelper.initBean(bean, data);
+ return bean;
+ }
+ else
+ {
+ throw new IllegalArgumentException("Unsupported class: "
+ + beanClass);
+ }
+ }
+
+ /**
+ * Returns the default class, but only if the supportsDefaultClass flag
+ * is set.
+ */
+ public Class<?> getDefaultBeanClass()
+ {
+ return supportsDefaultClass ? TestBean.class : null;
+ }
+ }
+
+ /**
+ * A test implementation of the BeanDeclaration interface. This
+ * implementation allows to set the values directly, which should be
+ * returned by the methods required by the BeanDeclaration interface.
+ */
+ static class TestBeanDeclaration implements BeanDeclaration
+ {
+ private String beanClassName;
+
+ private String beanFactoryName;
+
+ private Object beanFactoryParameter;
+
+ private Map<String, Object> beanProperties;
+
+ private Map<String, Object> nestedBeanDeclarations;
+
+ public String getBeanClassName()
+ {
+ return beanClassName;
+ }
+
+ public void setBeanClassName(String beanClassName)
+ {
+ this.beanClassName = beanClassName;
+ }
+
+ public String getBeanFactoryName()
+ {
+ return beanFactoryName;
+ }
+
+ public void setBeanFactoryName(String beanFactoryName)
+ {
+ this.beanFactoryName = beanFactoryName;
+ }
+
+ public Object getBeanFactoryParameter()
+ {
+ return beanFactoryParameter;
+ }
+
+ public void setBeanFactoryParameter(Object beanFactoryParameter)
+ {
+ this.beanFactoryParameter = beanFactoryParameter;
+ }
+
+ public Map<String, Object> getBeanProperties()
+ {
+ return beanProperties;
+ }
+
+ public void setBeanProperties(Map<String, Object> beanProperties)
+ {
+ this.beanProperties = beanProperties;
+ }
+
+ public Map<String, Object> getNestedBeanDeclarations()
+ {
+ return nestedBeanDeclarations;
+ }
+
+ public void setNestedBeanDeclarations(Map<String, Object> nestedBeanDeclarations)
+ {
+ this.nestedBeanDeclarations = nestedBeanDeclarations;
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/beanutils/TestConfigurationDynaBean.java b/src/test/java/org/apache/commons/configuration/beanutils/TestConfigurationDynaBean.java
new file mode 100644
index 0000000..40bfc27
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/beanutils/TestConfigurationDynaBean.java
@@ -0,0 +1,734 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.beanutils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+import junitx.framework.ObjectAssert;
+
+import org.apache.commons.beanutils.DynaProperty;
+import org.apache.commons.configuration.BaseConfiguration;
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.configuration.MapConfiguration;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * <p>Test Case for the <code>ConfigurationDynaBean</code> implementation class.
+ * These tests were based on the ones in <code>BasicDynaBeanTestCase</code>
+ * because the two classes provide similar levels of functionality.</p>
+ *
+ * @author <a href="mailto:ricardo.gladwell at btinternet.com">Ricardo Gladwell</a>
+ * @version $Id: TestConfigurationDynaBean.java 1225349 2011-12-28 21:36:59Z oheger $
+ */
+public class TestConfigurationDynaBean
+{
+ /**
+ * The basic test bean for each test.
+ */
+ private ConfigurationDynaBean bean;
+
+ /**
+ * The set of property names we expect to have returned when calling
+ * <code>getDynaProperties()</code>. You should update this list
+ * when new properties are added to TestBean.
+ */
+ String[] properties = {
+ "booleanProperty",
+ "booleanSecond",
+ "doubleProperty",
+ "floatProperty",
+ "intProperty",
+ "longProperty",
+ "mappedProperty.key1",
+ "mappedProperty.key2",
+ "mappedProperty.key3",
+ "mappedIntProperty.key1",
+ "shortProperty",
+ "stringProperty",
+ "byteProperty",
+ "charProperty"
+ };
+
+ Object[] values = {
+ Boolean.TRUE,
+ Boolean.TRUE,
+ new Double(Double.MAX_VALUE),
+ new Float(Float.MAX_VALUE),
+ new Integer(Integer.MAX_VALUE),
+ new Long(Long.MAX_VALUE),
+ "First Value",
+ "Second Value",
+ "Third Value",
+ new Integer(Integer.MAX_VALUE),
+ new Short(Short.MAX_VALUE),
+ "This is a string",
+ new Byte(Byte.MAX_VALUE),
+ new Character(Character.MAX_VALUE)
+ };
+
+ int[] intArray = {0, 10, 20, 30, 40};
+ boolean[] booleanArray = {true, false, true, false, true};
+ char[] charArray = {'a', 'b', 'c', 'd', 'e'};
+ byte[] byteArray = {0, 10, 20, 30, 40};
+ long[] longArray = {0, 10, 20, 30, 40};
+ short[] shortArray = {0, 10, 20, 30, 40};
+ float[] floatArray = {0, 10, 20, 30, 40};
+ double[] doubleArray = {0.0, 10.0, 20.0, 30.0, 40.0};
+ String[] stringArray = {"String 0", "String 1", "String 2", "String 3", "String 4"};
+
+
+ /**
+ * Set up instance variables required by this test case.
+ */
+ @Before
+ public void setUp() throws Exception
+ {
+ Configuration configuration = createConfiguration();
+
+ for (int i = 0; i < properties.length; i++)
+ {
+ configuration.setProperty(properties[i], values[i]);
+ }
+
+ for (int a = 0; a < intArray.length; a++)
+ {
+ configuration.addProperty("intIndexed", new Integer(intArray[a]));
+ }
+
+ for (int a = 0; a < stringArray.length; a++)
+ {
+ configuration.addProperty("stringIndexed", stringArray[a]);
+ }
+
+ List<String> list = Arrays.asList(stringArray);
+ configuration.addProperty("listIndexed", list);
+
+ bean = new ConfigurationDynaBean(configuration);
+
+ bean.set("listIndexed", list);
+ bean.set("intArray", intArray);
+ bean.set("booleanArray", booleanArray);
+ bean.set("charArray", charArray);
+ bean.set("longArray", longArray);
+ bean.set("shortArray", shortArray);
+ bean.set("floatArray", floatArray);
+ bean.set("doubleArray", doubleArray);
+ bean.set("byteArray", byteArray);
+ bean.set("stringArray", stringArray);
+ }
+
+ /**
+ * Creates the underlying configuration object for the dyna bean.
+ * @return the underlying configuration object
+ */
+ protected Configuration createConfiguration()
+ {
+ return new BaseConfiguration();
+ }
+
+ /**
+ * Corner cases on getDynaProperty invalid arguments.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetDescriptorArguments()
+ {
+ DynaProperty descriptor = bean.getDynaClass().getDynaProperty("unknown");
+ assertNull("Unknown property descriptor should be null", descriptor);
+ bean.getDynaClass().getDynaProperty(null);
+ }
+
+ /**
+ * Positive getDynaProperty on property <code>booleanProperty</code>.
+ */
+ @Test
+ public void testGetDescriptorBoolean()
+ {
+ testGetDescriptorBase("booleanProperty", Boolean.TYPE);
+ }
+
+ /**
+ * Positive getDynaProperty on property <code>doubleProperty</code>.
+ */
+ @Test
+ public void testGetDescriptorDouble()
+ {
+ testGetDescriptorBase("doubleProperty", Double.TYPE);
+ }
+
+ /**
+ * Positive getDynaProperty on property <code>floatProperty</code>.
+ */
+ @Test
+ public void testGetDescriptorFloat()
+ {
+ testGetDescriptorBase("floatProperty", Float.TYPE);
+ }
+
+ /**
+ * Positive getDynaProperty on property <code>intProperty</code>.
+ */
+ @Test
+ public void testGetDescriptorInt()
+ {
+ testGetDescriptorBase("intProperty", Integer.TYPE);
+ }
+
+ /**
+ * Positive getDynaProperty on property <code>longProperty</code>.
+ */
+ @Test
+ public void testGetDescriptorLong()
+ {
+ testGetDescriptorBase("longProperty", Long.TYPE);
+ }
+
+ /**
+ * Positive getDynaProperty on property <code>booleanSecond</code>
+ * that uses an "is" method as the getter.
+ */
+ @Test
+ public void testGetDescriptorSecond()
+ {
+ testGetDescriptorBase("booleanSecond", Boolean.TYPE);
+ }
+
+ /**
+ * Positive getDynaProperty on property <code>shortProperty</code>.
+ */
+ @Test
+ public void testGetDescriptorShort()
+ {
+ testGetDescriptorBase("shortProperty", Short.TYPE);
+ }
+
+ /**
+ * Positive getDynaProperty on property <code>stringProperty</code>.
+ */
+ @Test
+ public void testGetDescriptorString()
+ {
+ testGetDescriptorBase("stringProperty", String.class);
+ }
+
+ /**
+ * Positive test for getDynaPropertys(). Each property name
+ * listed in <code>properties</code> should be returned exactly once.
+ */
+ @Test
+ public void testGetDescriptors()
+ {
+ DynaProperty pd[] = bean.getDynaClass().getDynaProperties();
+ assertNotNull("Got descriptors", pd);
+ int count[] = new int[properties.length];
+ for (int i = 0; i < pd.length; i++)
+ {
+ String name = pd[i].getName();
+ for (int j = 0; j < properties.length; j++)
+ {
+ if (name.equals(properties[j]))
+ {
+ count[j]++;
+ }
+ }
+ }
+
+ for (int j = 0; j < properties.length; j++)
+ {
+ if (count[j] < 0)
+ {
+ fail("Missing property " + properties[j]);
+ }
+ else if (count[j] > 1)
+ {
+ fail("Duplicate property " + properties[j]);
+ }
+ }
+ }
+
+ /**
+ * Corner cases on getIndexedProperty invalid arguments.
+ */
+ @Test(expected = IndexOutOfBoundsException.class)
+ public void testGetIndexedArguments()
+ {
+ bean.get("intArray", -1);
+ }
+
+ /**
+ * Positive and negative tests on getIndexedProperty valid arguments.
+ */
+ @Test
+ public void testGetIndexedValues()
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ Object value = bean.get("intArray", i);
+
+ assertNotNull("intArray index " + i + " did not return value.", value);
+ ObjectAssert.assertInstanceOf("intArray index " + i, Integer.class, value);
+ assertEquals("intArray " + i + " returned incorrect value.", i * 10, ((Integer) value).intValue());
+
+ value = bean.get("intIndexed", i);
+
+ assertNotNull("intIndexed index " + i + "returned value " + i, value);
+ ObjectAssert.assertInstanceOf("intIndexed index " + i, Integer.class, value);
+ assertEquals("intIndexed index " + i + "returned correct " + i, i * 10, ((Integer) value).intValue());
+
+ value = bean.get("listIndexed", i);
+
+ assertNotNull("listIndexed index " + i + "returned value " + i, value);
+ ObjectAssert.assertInstanceOf("list index " + i, String.class, value);
+ assertEquals("listIndexed index " + i + "returned correct " + i, "String " + i, value);
+
+ value = bean.get("stringArray", i);
+
+ assertNotNull("stringArray index " + i + " returnde null.", value);
+ assertFalse("stringArray index " + i + " returned array instead of String.", value.getClass().isArray());
+ ObjectAssert.assertInstanceOf("stringArray index " + i, String.class, value);
+ assertEquals("stringArray returned correct " + i, "String " + i, value);
+
+ value = bean.get("stringIndexed", i);
+
+ assertNotNull("stringIndexed returned value " + i, value);
+ ObjectAssert.assertInstanceOf("stringIndexed", String.class, value);
+ assertEquals("stringIndexed returned correct " + i, "String " + i, value);
+ }
+ }
+
+ /**
+ * Corner cases on getMappedProperty invalid arguments.
+ */
+ @Test
+ public void testGetMappedArguments()
+ {
+ try
+ {
+ Object value = bean.get("mappedProperty", "unknown");
+ assertNull("Should not return a value", value);
+ }
+ catch (Throwable t)
+ {
+ fail("Threw " + t + " instead of returning null");
+ }
+ }
+
+ /**
+ * Positive and negative tests on getMappedProperty valid arguments.
+ */
+ @Test
+ public void testGetMappedValues()
+ {
+ Object value = bean.get("mappedProperty", "key1");
+ assertEquals("Can find first value", "First Value", value);
+
+ value = bean.get("mappedProperty", "key2");
+ assertEquals("Can find second value", "Second Value", value);
+
+ value = bean.get("mappedProperty", "key3");
+ assertNotNull("Cannot find third value", value);
+ }
+
+ /**
+ * Corner cases on getSimpleProperty invalid arguments.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetSimpleArguments()
+ {
+ bean.get("a non existing property");
+ }
+
+ /**
+ * Test getSimpleProperty on a boolean property.
+ */
+ @Test
+ public void testGetSimpleBoolean()
+ {
+ Object value = bean.get("booleanProperty");
+ assertNotNull("Got a value", value);
+ ObjectAssert.assertInstanceOf("Got correct type", Boolean.class, value);
+ assertTrue("Got correct value", ((Boolean) value).booleanValue());
+ }
+
+ /**
+ * Test getSimpleProperty on a double property.
+ */
+ @Test
+ public void testGetSimpleDouble()
+ {
+ Object value = bean.get("doubleProperty");
+ assertNotNull("Got a value", value);
+ ObjectAssert.assertInstanceOf("Got correct type", Double.class, value);
+ assertEquals("Got correct value", ((Double) value).doubleValue(), Double.MAX_VALUE, 0.005);
+ }
+
+ /**
+ * Test getSimpleProperty on a float property.
+ */
+ @Test
+ public void testGetSimpleFloat()
+ {
+ Object value = bean.get("floatProperty");
+ assertNotNull("Got a value", value);
+ ObjectAssert.assertInstanceOf("Got correct type", Float.class, value);
+ assertEquals("Got correct value", ((Float) value).floatValue(), Float.MAX_VALUE, 0.005f);
+ }
+
+ /**
+ * Test getSimpleProperty on a int property.
+ */
+ @Test
+ public void testGetSimpleInt()
+ {
+ Object value = bean.get("intProperty");
+ assertNotNull("Failed to get value", value);
+ ObjectAssert.assertInstanceOf("Incorrect type", Integer.class, value);
+ assertEquals("Incorrect value", ((Integer) value).intValue(), Integer.MAX_VALUE);
+ }
+
+ /**
+ * Test getSimpleProperty on a long property.
+ */
+ @Test
+ public void testGetSimpleLong()
+ {
+ Object value = bean.get("longProperty");
+ assertNotNull("Got a value", value);
+ ObjectAssert.assertInstanceOf("Returned incorrect type", Long.class, value);
+ assertEquals("Returned value of Incorrect value", ((Long) value).longValue(), Long.MAX_VALUE);
+ }
+
+ /**
+ * Test getSimpleProperty on a short property.
+ */
+ @Test
+ public void testGetSimpleShort()
+ {
+ Object value = bean.get("shortProperty");
+ assertNotNull("Got a value", value);
+ ObjectAssert.assertInstanceOf("Got correct type", Short.class, value);
+ assertEquals("Got correct value", ((Short) value).shortValue(), Short.MAX_VALUE);
+ }
+
+ /**
+ * Test getSimpleProperty on a String property.
+ */
+ @Test
+ public void testGetSimpleString()
+ {
+ Object value = bean.get("stringProperty");
+ assertNotNull("Got a value", value);
+ ObjectAssert.assertInstanceOf("Got correct type", String.class, value);
+ assertEquals("Got correct value", value, "This is a string");
+ }
+
+ /**
+ * Test <code>contains()</code> method for mapped properties.
+ */
+ @Test
+ public void testMappedContains()
+ {
+ assertTrue("Can't see first key", bean.contains("mappedProperty", "key1"));
+ assertTrue("Can see unknown key", !bean.contains("mappedProperty", "Unknown Key"));
+ }
+
+ /**
+ * Test <code>remove()</code> method for mapped properties.
+ */
+ @Test
+ public void testMappedRemove()
+ {
+ assertTrue("Can see first key", bean.contains("mappedProperty", "key1"));
+ bean.remove("mappedProperty", "key1");
+ assertTrue("Can not see first key", !bean.contains("mappedProperty", "key1"));
+
+ assertTrue("Can not see unknown key", !bean.contains("mappedProperty", "key4"));
+ bean.remove("mappedProperty", "key4");
+ assertTrue("Can not see unknown key", !bean.contains("mappedProperty", "key4"));
+ }
+
+ /**
+ * Corner cases on setIndexedProperty invalid arguments.
+ */
+ @Test(expected = IndexOutOfBoundsException.class)
+ public void testSetIndexedArguments()
+ {
+ bean.set("intArray", -1, new Integer(0));
+ }
+
+ /**
+ * Positive and negative tests on setIndexedProperty valid arguments.
+ */
+ @Test
+ public void testSetIndexedValues()
+ {
+ bean.set("intArray", 0, new Integer(1));
+ Object value = bean.get("intArray", 0);
+
+ assertNotNull("Returned new value 0", value);
+ ObjectAssert.assertInstanceOf("Returned Integer new value 0", Integer.class, value);
+ assertEquals("Returned correct new value 0", 1, ((Integer) value).intValue());
+
+
+ bean.set("intIndexed", 1, new Integer(11));
+ value = bean.get("intIndexed", 1);
+
+ assertNotNull("Returned new value 1", value);
+ ObjectAssert.assertInstanceOf("Returned Integer new value 1", Integer.class, value);
+ assertEquals("Returned correct new value 1", 11, ((Integer) value).intValue());
+
+
+ bean.set("listIndexed", 2, "New Value 2");
+ value = bean.get("listIndexed", 2);
+
+ assertNotNull("Returned new value 2", value);
+ ObjectAssert.assertInstanceOf("Returned String new value 2", String.class, value);
+ assertEquals("Returned correct new value 2", "New Value 2", value);
+
+
+ bean.set("stringArray", 3, "New Value 3");
+ value = bean.get("stringArray", 3);
+
+ assertNotNull("Returned new value 3", value);
+ ObjectAssert.assertInstanceOf("Returned String new value 3", String.class, value);
+ assertEquals("Returned correct new value 3", "New Value 3", value);
+
+
+ bean.set("stringIndexed", 4, "New Value 4");
+ value = bean.get("stringIndexed", 4);
+
+ assertNotNull("Returned new value 4", value);
+ ObjectAssert.assertInstanceOf("Returned String new value 4", String.class, value);
+ assertEquals("Returned correct new value 4", "New Value 4", value);
+ }
+
+ /**
+ * Test the modification of a configuration property stored internally as an array.
+ */
+ @Test
+ public void testSetArrayValue()
+ {
+ MapConfiguration configuration = new MapConfiguration(new HashMap<String, Object>());
+ configuration.getMap().put("objectArray", new Object[] {"value1", "value2", "value3"});
+
+ ConfigurationDynaBean bean = new ConfigurationDynaBean(configuration);
+
+ bean.set("objectArray", 1, "New Value 1");
+ Object value = bean.get("objectArray", 1);
+
+ assertNotNull("Returned new value 1", value);
+ ObjectAssert.assertInstanceOf("Returned String new value 1", String.class, value);
+ assertEquals("Returned correct new value 1", "New Value 1", value);
+ }
+
+ /**
+ * Positive and negative tests on setMappedProperty valid arguments.
+ */
+ @Test
+ public void testSetMappedValues()
+ {
+ bean.set("mappedProperty", "First Key", "New First Value");
+ assertEquals("Can replace old value", "New First Value", bean.get("mappedProperty", "First Key"));
+
+ bean.set("mappedProperty", "Fourth Key", "Fourth Value");
+ assertEquals("Can set new value", "Fourth Value", bean.get("mappedProperty", "Fourth Key"));
+ }
+
+ /**
+ * Test setSimpleProperty on a boolean property.
+ */
+ @Test
+ public void testSetSimpleBoolean()
+ {
+ boolean oldValue = ((Boolean) bean.get("booleanProperty")).booleanValue();
+ boolean newValue = !oldValue;
+ bean.set("booleanProperty", new Boolean(newValue));
+ assertTrue("Matched new value", newValue == ((Boolean) bean.get("booleanProperty")).booleanValue());
+ }
+
+ /**
+ * Test setSimpleProperty on a double property.
+ */
+ @Test
+ public void testSetSimpleDouble()
+ {
+ double oldValue = ((Double) bean.get("doubleProperty")).doubleValue();
+ double newValue = oldValue + 1.0;
+ bean.set("doubleProperty", new Double(newValue));
+ assertEquals("Matched new value", newValue, ((Double) bean.get("doubleProperty")).doubleValue(), 0.005);
+ }
+
+ /**
+ * Test setSimpleProperty on a float property.
+ */
+ @Test
+ public void testSetSimpleFloat()
+ {
+ float oldValue = ((Float) bean.get("floatProperty")).floatValue();
+ float newValue = oldValue + (float) 1.0;
+ bean.set("floatProperty", new Float(newValue));
+ assertEquals("Matched new value", newValue, ((Float) bean.get("floatProperty")).floatValue(), 0.005f);
+ }
+
+ /**
+ * Test setSimpleProperty on a int property.
+ */
+ @Test
+ public void testSetSimpleInt()
+ {
+ int oldValue = ((Integer) bean.get("intProperty")).intValue();
+ int newValue = oldValue + 1;
+ bean.set("intProperty", new Integer(newValue));
+ assertEquals("Matched new value", newValue, ((Integer) bean.get("intProperty")).intValue());
+ }
+
+ /**
+ * Test setSimpleProperty on a long property.
+ */
+ @Test
+ public void testSetSimpleLong()
+ {
+ long oldValue = ((Long) bean.get("longProperty")).longValue();
+ long newValue = oldValue + 1;
+ bean.set("longProperty", new Long(newValue));
+ assertEquals("Matched new value", newValue, ((Long) bean.get("longProperty")).longValue());
+ }
+
+ /**
+ * Test setSimpleProperty on a short property.
+ */
+ @Test
+ public void testSetSimpleShort()
+ {
+ short oldValue = ((Short) bean.get("shortProperty")).shortValue();
+ short newValue = (short) (oldValue + 1);
+ bean.set("shortProperty", new Short(newValue));
+ assertEquals("Matched new value", newValue, ((Short) bean.get("shortProperty")).shortValue());
+ }
+
+ /**
+ * Test setSimpleProperty on a String property.
+ */
+ @Test
+ public void testSetSimpleString()
+ {
+ String oldValue = (String) bean.get("stringProperty");
+ String newValue = oldValue + " Extra Value";
+ bean.set("stringProperty", newValue);
+ assertEquals("Matched new value", newValue, bean.get("stringProperty"));
+ }
+
+ /**
+ * Tests set on a null value: should throw NPE.
+ */
+ @Test(expected = NullPointerException.class)
+ public void testAddNullPropertyValue()
+ {
+ bean.set("nullProperty", null);
+ }
+
+ /**
+ * Test the retrieval of a non-existent property.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetNonExistentProperty()
+ {
+ bean.get("nonexistProperty");
+ }
+
+ /**
+ * Base for testGetDescriptorXxxxx() series of tests.
+ *
+ * @param name Name of the property to be retrieved
+ * @param type Expected class type of this property
+ */
+ protected void testGetDescriptorBase(String name, Class<?> type)
+ {
+ DynaProperty descriptor = bean.getDynaClass().getDynaProperty(name);
+
+ assertNotNull("Failed to get descriptor", descriptor);
+ assertEquals("Got incorrect type", type, descriptor.getType());
+ }
+
+ /**
+ * Tests whether nested properties can be accessed.
+ */
+ @Test
+ public void testNestedPropeties()
+ {
+ ConfigurationDynaBean nested = (ConfigurationDynaBean) bean.get("mappedProperty");
+
+ String value = (String) nested.get("key1");
+ assertEquals("Can find first value", "First Value", value);
+
+ nested.set("key1", "undefined");
+ assertEquals("Incorrect value returned", "undefined", bean.get("mappedProperty.key1"));
+ }
+
+ /**
+ * Tests if reading a non-indexed property using the index
+ * get method throws an IllegalArgumentException as it
+ * should.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetNonIndexedProperties()
+ {
+ bean.get("booleanProperty", 0);
+ }
+
+ /**
+ * Tests whether accessing a non-indexed string property using the index get
+ * method causes an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetIndexedString()
+ {
+ bean.set("stringProp", "value");
+ bean.get("stringProp", 0);
+ }
+
+ /**
+ * Tests whether an indexed access to a non-existing property causes an
+ * exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetIndexedNonExisting()
+ {
+ bean.get("Non existing property", 0);
+ }
+
+ /**
+ * Tests if writing a non-indexed property using the index
+ * set method with an index > 0 throws an IllegalArgumentException as it
+ * should.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetNonIndexedProperties()
+ {
+ bean.set("booleanProperty", 1, Boolean.TRUE);
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/beanutils/TestConfigurationDynaBeanXMLConfig.java b/src/test/java/org/apache/commons/configuration/beanutils/TestConfigurationDynaBeanXMLConfig.java
new file mode 100644
index 0000000..ccc7740
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/beanutils/TestConfigurationDynaBeanXMLConfig.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.beanutils;
+
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.configuration.XMLConfiguration;
+
+/**
+ * An additional test class for ConfigurationDynaBean. This test class performs
+ * the same tests as the default test class, but uses a XMLConfiguration as
+ * underlying configuration object.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestConfigurationDynaBeanXMLConfig.java 1301994 2012-03-17 20:21:23Z sebb $
+ */
+public class TestConfigurationDynaBeanXMLConfig extends
+ TestConfigurationDynaBean
+{
+ /**
+ * Creates the underlying configuration object. This implementation will
+ * create a XMLConfiguration.
+ * @return the underlying configuration
+ */
+ @Override
+ protected Configuration createConfiguration()
+ {
+ return new XMLConfiguration();
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/beanutils/TestDefaultBeanFactory.java b/src/test/java/org/apache/commons/configuration/beanutils/TestDefaultBeanFactory.java
new file mode 100644
index 0000000..d34e066
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/beanutils/TestDefaultBeanFactory.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.beanutils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.configuration.PropertiesConfiguration;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for DefaultBeanFactory.
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestDefaultBeanFactory.java 1225642 2011-12-29 20:31:38Z oheger $
+ */
+public class TestDefaultBeanFactory
+{
+ /** The object to be tested. */
+ DefaultBeanFactory factory;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ factory = new DefaultBeanFactory();
+ }
+
+ /**
+ * Tests obtaining the default class. This should be null.
+ */
+ @Test
+ public void testGetDefaultBeanClass()
+ {
+ assertNull("Default class is not null", factory.getDefaultBeanClass());
+ }
+
+ /**
+ * Tests creating a bean.
+ */
+ @Test
+ public void testCreateBean() throws Exception
+ {
+ Object bean = factory.createBean(PropertiesConfiguration.class,
+ new TestBeanDeclaration(), null);
+ assertNotNull("New bean is null", bean);
+ assertEquals("Bean is of wrong class", PropertiesConfiguration.class,
+ bean.getClass());
+ PropertiesConfiguration config = (PropertiesConfiguration) bean;
+ assertTrue("Bean was not initialized", config
+ .isThrowExceptionOnMissing());
+ }
+
+ /**
+ * A simple implementation of BeanDeclaration used for testing purposes.
+ */
+ static class TestBeanDeclaration implements BeanDeclaration
+ {
+ public String getBeanFactoryName()
+ {
+ return null;
+ }
+
+ public Object getBeanFactoryParameter()
+ {
+ return null;
+ }
+
+ public String getBeanClassName()
+ {
+ return null;
+ }
+
+ public Map<String, Object> getBeanProperties()
+ {
+ Map<String, Object> props = new HashMap<String, Object>();
+ props.put("throwExceptionOnMissing", Boolean.TRUE);
+ return props;
+ }
+
+ public Map<String, Object> getNestedBeanDeclarations()
+ {
+ return null;
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/beanutils/TestXMLBeanDeclaration.java b/src/test/java/org/apache/commons/configuration/beanutils/TestXMLBeanDeclaration.java
new file mode 100644
index 0000000..cf7da51
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/beanutils/TestXMLBeanDeclaration.java
@@ -0,0 +1,425 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.beanutils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.apache.commons.configuration.SubnodeConfiguration;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.junit.Test;
+
+/**
+ * Test class for XMLBeanDeclaration.
+ *
+ * @since 1.3
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestXMLBeanDeclaration.java 1225643 2011-12-29 20:37:36Z oheger $
+ */
+public class TestXMLBeanDeclaration
+{
+ /** An array with some test properties. */
+ static final String[] TEST_PROPS =
+ { "firstName", "lastName", "department", "age", "hobby"};
+
+ /** An array with the values for the test properties. */
+ static final String[] TEST_VALUES =
+ { "John", "Smith", "Engineering", "42", "TV"};
+
+ /** An array with the names of nested (complex) properties. */
+ static final String[] COMPLEX_PROPS =
+ { "address", "car"};
+
+ /** An array with the names of the classes of the complex properties. */
+ static final String[] COMPLEX_CLASSES =
+ { "org.apache.commons.configuration.test.AddressTest",
+ "org.apache.commons.configuration.test.CarTest"};
+
+ /** An array with the property names of the complex properties. */
+ static final String[][] COMPLEX_ATTRIBUTES =
+ {
+ { "street", "zip", "city", "country"},
+ { "brand", "color"}};
+
+ /** An array with the values of the complex properties. */
+ static final String[][] COMPLEX_VALUES =
+ {
+ { "Baker Street", "12354", "London", "UK"},
+ { "Bentley", "silver"}};
+
+ /** Constant for the key with the bean declaration. */
+ static final String KEY = "myBean";
+
+ /** Constant for the section with the variables.*/
+ static final String VARS = "variables.";
+
+ /** Stores the object to be tested. */
+ XMLBeanDeclaration decl;
+
+ /**
+ * Tests creating a declaration from a null node. This should cause an
+ * exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testInitFromNullNode()
+ {
+ decl = new XMLBeanDeclaration(new HierarchicalConfiguration().configurationAt(null),
+ (ConfigurationNode) null);
+ }
+
+ /**
+ * Tests creating a declaration from a null configuration. This should cause
+ * an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testInitFromNullConfiguration()
+ {
+ decl = new XMLBeanDeclaration((HierarchicalConfiguration) null);
+ }
+
+ /**
+ * Tests creating a declaration from a null configuration with a key. This
+ * should cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testInitFromNullConfigurationAndKey()
+ {
+ decl = new XMLBeanDeclaration(null, KEY);
+ }
+
+ /**
+ * Tests creating a declaration from a null configuration with a node. This
+ * should cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testInitFromNullConfigurationAndNode()
+ {
+ decl = new XMLBeanDeclaration(null, new HierarchicalConfiguration()
+ .getRoot());
+ }
+
+ /**
+ * Tests fetching the bean's class name.
+ */
+ @Test
+ public void testGetBeanClassName()
+ {
+ HierarchicalConfiguration config = new HierarchicalConfiguration();
+ config.addProperty(KEY + "[@config-class]", getClass().getName());
+ decl = new XMLBeanDeclaration(config, KEY);
+ assertEquals("Wrong class name", getClass().getName(), decl
+ .getBeanClassName());
+ }
+
+ /**
+ * Tests fetching the bean's class name if it is undefined.
+ */
+ @Test
+ public void testGetBeanClassNameUndefined()
+ {
+ decl = new XMLBeanDeclaration(new HierarchicalConfiguration());
+ assertNull(decl.getBeanClassName());
+ }
+
+ /**
+ * Tests fetching the name of the bean factory.
+ */
+ @Test
+ public void testGetBeanFactoryName()
+ {
+ HierarchicalConfiguration config = new HierarchicalConfiguration();
+ config.addProperty(KEY + "[@config-factory]", "myFactory");
+ decl = new XMLBeanDeclaration(config, KEY);
+ assertEquals("Wrong factory name", "myFactory", decl
+ .getBeanFactoryName());
+ }
+
+ /**
+ * Tests fetching the name of the bean factory if it is undefined.
+ */
+ @Test
+ public void testGetBeanFactoryNameUndefined()
+ {
+ decl = new XMLBeanDeclaration(new HierarchicalConfiguration());
+ assertNull(decl.getBeanFactoryName());
+ }
+
+ /**
+ * Tests fetching the parameter for the bean factory.
+ */
+ @Test
+ public void testGetBeanFactoryParameter()
+ {
+ HierarchicalConfiguration config = new HierarchicalConfiguration();
+ config
+ .addProperty(KEY + "[@config-factoryParam]",
+ "myFactoryParameter");
+ decl = new XMLBeanDeclaration(config, KEY);
+ assertEquals("Wrong factory parameter", "myFactoryParameter", decl
+ .getBeanFactoryParameter());
+ }
+
+ /**
+ * Tests fetching the parameter for the bean factory if it is undefined.
+ */
+ @Test
+ public void testGetBeanFactoryParameterUndefined()
+ {
+ decl = new XMLBeanDeclaration(new HierarchicalConfiguration());
+ assertNull(decl.getBeanFactoryParameter());
+ }
+
+ /**
+ * Tests if the bean's properties are correctly extracted from the
+ * configuration object.
+ */
+ @Test
+ public void testGetBeanProperties()
+ {
+ HierarchicalConfiguration config = new HierarchicalConfiguration();
+ setupBeanDeclaration(config, KEY, TEST_PROPS, TEST_VALUES);
+ decl = new XMLBeanDeclaration(config, KEY);
+ checkProperties(decl, TEST_PROPS, TEST_VALUES);
+ }
+
+ /**
+ * Tests obtaining the bean's properties when reserved attributes are
+ * involved. These should be ignored.
+ */
+ @Test
+ public void testGetBeanPropertiesWithReservedAttributes()
+ {
+ HierarchicalConfiguration config = new HierarchicalConfiguration();
+ setupBeanDeclaration(config, KEY, TEST_PROPS, TEST_VALUES);
+ config.addProperty(KEY + "[@config-testattr]", "yes");
+ config.addProperty(KEY + "[@config-anothertest]", "this, too");
+ decl = new XMLBeanDeclaration(config, KEY);
+ checkProperties(decl, TEST_PROPS, TEST_VALUES);
+ }
+
+ /**
+ * Tests fetching properties if none are defined.
+ */
+ @Test
+ public void testGetBeanPropertiesEmpty()
+ {
+ decl = new XMLBeanDeclaration(new HierarchicalConfiguration());
+ Map<String, Object> props = decl.getBeanProperties();
+ assertTrue("Properties found", props == null || props.isEmpty());
+ }
+
+ /**
+ * Creates a configuration with data for testing nested bean declarations.
+ * @return the initialized test configuration
+ */
+ private HierarchicalConfiguration prepareNestedBeanDeclarations()
+ {
+ HierarchicalConfiguration config = new HierarchicalConfiguration();
+ setupBeanDeclaration(config, KEY, TEST_PROPS, TEST_VALUES);
+ for (int i = 0; i < COMPLEX_PROPS.length; i++)
+ {
+ setupBeanDeclaration(config, KEY + '.' + COMPLEX_PROPS[i],
+ COMPLEX_ATTRIBUTES[i], COMPLEX_VALUES[i]);
+ config.addProperty(
+ KEY + '.' + COMPLEX_PROPS[i] + "[@config-class]",
+ COMPLEX_CLASSES[i]);
+ }
+ return config;
+ }
+
+ /**
+ * Tests fetching nested bean declarations.
+ */
+ @Test
+ public void testGetNestedBeanDeclarations()
+ {
+ HierarchicalConfiguration config = prepareNestedBeanDeclarations();
+ decl = new XMLBeanDeclaration(config, KEY);
+ checkProperties(decl, TEST_PROPS, TEST_VALUES);
+
+ Map<String, Object> nested = decl.getNestedBeanDeclarations();
+ assertEquals("Wrong number of nested declarations",
+ COMPLEX_PROPS.length, nested.size());
+ for (int i = 0; i < COMPLEX_PROPS.length; i++)
+ {
+ XMLBeanDeclaration d = (XMLBeanDeclaration) nested
+ .get(COMPLEX_PROPS[i]);
+ assertNotNull("No declaration found for " + COMPLEX_PROPS[i], d);
+ checkProperties(d, COMPLEX_ATTRIBUTES[i], COMPLEX_VALUES[i]);
+ assertEquals("Wrong bean class", COMPLEX_CLASSES[i], d
+ .getBeanClassName());
+ }
+ }
+
+ /**
+ * Tests whether the factory method for creating nested bean declarations
+ * gets called.
+ */
+ @Test
+ public void testGetNestedBeanDeclarationsFactoryMethod()
+ {
+ HierarchicalConfiguration config = prepareNestedBeanDeclarations();
+ decl = new XMLBeanDeclaration(config, KEY)
+ {
+ @Override
+ protected BeanDeclaration createBeanDeclaration(
+ ConfigurationNode node)
+ {
+ return new XMLBeanDeclarationTestImpl(getConfiguration()
+ .configurationAt(node.getName()), node);
+ }
+ };
+ Map<String, Object> nested = decl.getNestedBeanDeclarations();
+ for (int i = 0; i < COMPLEX_PROPS.length; i++)
+ {
+ Object d = nested.get(COMPLEX_PROPS[i]);
+ assertTrue("Wrong class for bean declaration: " + d,
+ d instanceof XMLBeanDeclarationTestImpl);
+ }
+ }
+
+ /**
+ * Tests fetching nested bean declarations if none are defined.
+ */
+ @Test
+ public void testGetNestedBeanDeclarationsEmpty()
+ {
+ HierarchicalConfiguration config = new HierarchicalConfiguration();
+ setupBeanDeclaration(config, KEY, TEST_PROPS, TEST_VALUES);
+ decl = new XMLBeanDeclaration(config, KEY);
+ Map<String, Object> nested = decl.getNestedBeanDeclarations();
+ assertTrue("Found nested declarations", nested == null
+ || nested.isEmpty());
+ }
+
+ /**
+ * Tests whether interpolation of bean properties works.
+ */
+ @Test
+ public void testGetInterpolatedBeanProperties()
+ {
+ HierarchicalConfiguration config = new HierarchicalConfiguration();
+ String[] varValues = new String[TEST_PROPS.length];
+ for(int i = 0; i < TEST_PROPS.length; i++)
+ {
+ varValues[i] = "${" + VARS + TEST_PROPS[i] + "}";
+ config.addProperty(VARS + TEST_PROPS[i], TEST_VALUES[i]);
+ }
+ setupBeanDeclaration(config, KEY, TEST_PROPS, varValues);
+ decl = new XMLBeanDeclaration(config, KEY);
+ checkProperties(decl, TEST_PROPS, TEST_VALUES);
+ }
+
+ /**
+ * Tests constructing a bean declaration from an undefined key. This should
+ * cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testInitFromUndefinedKey()
+ {
+ HierarchicalConfiguration config = new HierarchicalConfiguration();
+ setupBeanDeclaration(config, KEY, TEST_PROPS, TEST_VALUES);
+ decl = new XMLBeanDeclaration(config, "undefined_key");
+ }
+
+ /**
+ * Tests constructing a bean declaration from a key, which is undefined when
+ * the optional flag is set. In this case an empty declaration should be
+ * created, which can be used for creating beans as long as a default class
+ * is provided.
+ */
+ @Test
+ public void testInitFromUndefinedKeyOptional()
+ {
+ HierarchicalConfiguration config = new HierarchicalConfiguration();
+ setupBeanDeclaration(config, KEY, TEST_PROPS, TEST_VALUES);
+ decl = new XMLBeanDeclaration(config, "undefined_key", true);
+ assertNull("Found a bean class", decl.getBeanClassName());
+ }
+
+ /**
+ * Tests constructing a bean declaration from a key with multiple values.
+ * This should cause an exception because keys must be unique.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testInitFromMultiValueKey()
+ {
+ HierarchicalConfiguration config = new HierarchicalConfiguration();
+ config.addProperty(KEY, "myFirstKey");
+ config.addProperty(KEY, "mySecondKey");
+ decl = new XMLBeanDeclaration(config, KEY);
+ }
+
+ /**
+ * Initializes a configuration object with a bean declaration. Under the
+ * specified key the given properties will be added.
+ *
+ * @param config the configuration to initialize
+ * @param key the key of the bean declaration
+ * @param names an array with the names of the properties
+ * @param values an array with the corresponding values
+ */
+ private void setupBeanDeclaration(HierarchicalConfiguration config,
+ String key, String[] names, String[] values)
+ {
+ for (int i = 0; i < names.length; i++)
+ {
+ config.addProperty(key + "[@" + names[i] + "]", values[i]);
+ }
+ }
+
+ /**
+ * Checks the properties returned by a bean declaration.
+ *
+ * @param beanDecl the bean declaration
+ * @param names an array with the expected property names
+ * @param values an array with the expected property values
+ */
+ private void checkProperties(BeanDeclaration beanDecl, String[] names,
+ String[] values)
+ {
+ Map<String, Object> props = beanDecl.getBeanProperties();
+ assertEquals("Wrong number of properties", names.length, props.size());
+ for (int i = 0; i < names.length; i++)
+ {
+ assertTrue("Property " + names[i] + " not contained", props
+ .containsKey(names[i]));
+ assertEquals("Wrong value for property " + names[i], values[i],
+ props.get(names[i]));
+ }
+ }
+
+ /**
+ * A helper class used for testing the createBeanDeclaration() factory
+ * method.
+ */
+ private static class XMLBeanDeclarationTestImpl extends XMLBeanDeclaration
+ {
+ public XMLBeanDeclarationTestImpl(SubnodeConfiguration config,
+ ConfigurationNode node)
+ {
+ super(config, node);
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/event/AbstractTestConfigurationEvents.java b/src/test/java/org/apache/commons/configuration/event/AbstractTestConfigurationEvents.java
new file mode 100644
index 0000000..9c49f39
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/event/AbstractTestConfigurationEvents.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.event;
+
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Base class for testing events generated by configuration classes derived from
+ * AbstractConfiguration. This class implements a couple of tests related to
+ * event generation. Concrete sub classes only have to implement the
+ * {@code createConfiguration()} method for creating an instance of a
+ * specific configuration class. Because tests for detail events depend on a
+ * concrete implementation an exact sequence of events cannot be checked.
+ * Instead the corresponding test methods check whether the enclosing events
+ * (not the detail events) are of the expected type.
+ *
+ * @version $Id: AbstractTestConfigurationEvents.java 1225648 2011-12-29 20:55:07Z oheger $
+ */
+public abstract class AbstractTestConfigurationEvents
+{
+ /** Constant for a test property name. */
+ static final String TEST_PROPNAME = "event.test";
+
+ /** Constant for a test property value. */
+ static final String TEST_PROPVALUE = "a value";
+
+ /** Constant for an existing property. */
+ static final String EXIST_PROPERTY = "event.property";
+
+ /** The configuration to be tested. */
+ protected AbstractConfiguration config;
+
+ /** A test event listener. */
+ protected ConfigurationListenerTestImpl l;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ config = createConfiguration();
+ config.addProperty(EXIST_PROPERTY, "existing value");
+ l = new ConfigurationListenerTestImpl(config);
+ config.addConfigurationListener(l);
+ }
+
+ /**
+ * Creates the configuration instance to be tested.
+ *
+ * @return the configuration instance under test
+ */
+ protected abstract AbstractConfiguration createConfiguration();
+
+ /**
+ * Tests events generated by addProperty().
+ */
+ @Test
+ public void testAddPropertyEvent()
+ {
+ config.addProperty(TEST_PROPNAME, TEST_PROPVALUE);
+ l.checkEvent(AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_PROPNAME,
+ TEST_PROPVALUE, true);
+ l.checkEvent(AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_PROPNAME,
+ TEST_PROPVALUE, false);
+ l.done();
+ }
+
+ /**
+ * Tests events generated by addProperty() when detail events are enabled.
+ */
+ @Test
+ public void testAddPropertyEventWithDetails()
+ {
+ config.setDetailEvents(true);
+ config.addProperty(TEST_PROPNAME, TEST_PROPVALUE);
+ l.checkEventCount(2);
+ l.checkEvent(AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_PROPNAME,
+ TEST_PROPVALUE, true);
+ l.skipToLast(AbstractConfiguration.EVENT_ADD_PROPERTY);
+ l.checkEvent(AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_PROPNAME,
+ TEST_PROPVALUE, false);
+ l.done();
+ }
+
+ /**
+ * Tests events generated by clearProperty().
+ */
+ @Test
+ public void testClearPropertyEvent()
+ {
+ config.clearProperty(EXIST_PROPERTY);
+ l.checkEvent(AbstractConfiguration.EVENT_CLEAR_PROPERTY,
+ EXIST_PROPERTY, null, true);
+ l.checkEvent(AbstractConfiguration.EVENT_CLEAR_PROPERTY,
+ EXIST_PROPERTY, null, false);
+ l.done();
+ }
+
+ /**
+ * Tests events generated by clearProperty() when detail events are enabled.
+ */
+ @Test
+ public void testClearPropertyEventWithDetails()
+ {
+ config.setDetailEvents(true);
+ config.clearProperty(EXIST_PROPERTY);
+ l.checkEventCount(2);
+ l.checkEvent(AbstractConfiguration.EVENT_CLEAR_PROPERTY,
+ EXIST_PROPERTY, null, true);
+ l.skipToLast(AbstractConfiguration.EVENT_CLEAR_PROPERTY);
+ l.checkEvent(AbstractConfiguration.EVENT_CLEAR_PROPERTY,
+ EXIST_PROPERTY, null, false);
+ l.done();
+ }
+
+ /**
+ * Tests events generated by setProperty().
+ */
+ @Test
+ public void testSetPropertyEvent()
+ {
+ config.setProperty(EXIST_PROPERTY, TEST_PROPVALUE);
+ l.checkEvent(AbstractConfiguration.EVENT_SET_PROPERTY, EXIST_PROPERTY,
+ TEST_PROPVALUE, true);
+ l.checkEvent(AbstractConfiguration.EVENT_SET_PROPERTY, EXIST_PROPERTY,
+ TEST_PROPVALUE, false);
+ l.done();
+ }
+
+ /**
+ * Tests events generated by setProperty() when detail events are enabled.
+ */
+ @Test
+ public void testSetPropertyEventWithDetails()
+ {
+ config.setDetailEvents(true);
+ config.setProperty(EXIST_PROPERTY, TEST_PROPVALUE);
+ l.checkEventCount(2);
+ l.checkEvent(AbstractConfiguration.EVENT_SET_PROPERTY, EXIST_PROPERTY,
+ TEST_PROPVALUE, true);
+ l.skipToLast(AbstractConfiguration.EVENT_SET_PROPERTY);
+ l.checkEvent(AbstractConfiguration.EVENT_SET_PROPERTY, EXIST_PROPERTY,
+ TEST_PROPVALUE, false);
+ l.done();
+ }
+
+ /**
+ * Tests the events generated by the clear() method.
+ */
+ @Test
+ public void testClearEvent()
+ {
+ config.clear();
+ l.checkEvent(AbstractConfiguration.EVENT_CLEAR, null, null, true);
+ l.checkEvent(AbstractConfiguration.EVENT_CLEAR, null, null, false);
+ l.done();
+ }
+
+ /**
+ * Tests the events generated by the clear method when detail events are
+ * enabled.
+ */
+ @Test
+ public void testClearEventWithDetails()
+ {
+ config.setDetailEvents(true);
+ config.clear();
+ l.checkEventCount(2);
+ l.checkEvent(AbstractConfiguration.EVENT_CLEAR, null, null, true);
+ l.skipToLast(AbstractConfiguration.EVENT_CLEAR);
+ l.checkEvent(AbstractConfiguration.EVENT_CLEAR, null, null, false);
+ l.done();
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/event/AbstractTestFileConfigurationEvents.java b/src/test/java/org/apache/commons/configuration/event/AbstractTestFileConfigurationEvents.java
new file mode 100644
index 0000000..0d1c77b
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/event/AbstractTestFileConfigurationEvents.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.event;
+
+import java.io.IOException;
+import java.net.URL;
+
+import org.apache.commons.configuration.AbstractFileConfiguration;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.FileConfiguration;
+import org.apache.commons.configuration.reloading.ReloadingStrategy;
+import org.junit.Test;
+
+/**
+ * A base test class that can be used for testing file-based configurations.
+ * This class tests reload events, too.
+ *
+ * @version $Id: AbstractTestFileConfigurationEvents.java 1225648 2011-12-29 20:55:07Z oheger $
+ */
+public abstract class AbstractTestFileConfigurationEvents extends
+ AbstractTestConfigurationEvents
+{
+ /**
+ * Initializes the file configuration for the tests.
+ *
+ * @throws ConfigurationException if an error occurs
+ */
+ protected void setUpFileConfiguration() throws ConfigurationException,
+ IOException
+ {
+ FileConfiguration fc = (FileConfiguration) config;
+ fc.setReloadingStrategy(new AlwaysReloadingStrategy());
+ fc.setURL(getSourceURL());
+
+ // deregister event listener before load because load will cause
+ // other events being generated
+ config.removeConfigurationListener(l);
+ fc.load();
+ config.addConfigurationListener(l);
+ }
+
+ /**
+ * Returns the URL of the file to be loaded. Must be implemented in concrete
+ * test classes.
+ *
+ * @return the URL of the file-based configuration
+ * @throws IOException if an error occurs
+ */
+ protected abstract URL getSourceURL() throws IOException;
+
+ /**
+ * Tests events generated by the reload() method.
+ */
+ @Test
+ public void testReloadEvent() throws ConfigurationException, IOException
+ {
+ setUpFileConfiguration();
+ config.isEmpty(); // This should cause a reload
+ l.checkEvent(AbstractFileConfiguration.EVENT_RELOAD, null,
+ getSourceURL(), true);
+ l.checkEvent(AbstractFileConfiguration.EVENT_RELOAD, null,
+ getSourceURL(), false);
+ l.done();
+ }
+
+ /**
+ * Tests events generated by the reload() method when detail events are
+ * enabled.
+ */
+ @Test
+ public void testReloadEventWithDetails() throws ConfigurationException,
+ IOException
+ {
+ setUpFileConfiguration();
+ config.setDetailEvents(true);
+ config.isEmpty(); // This should cause a reload
+ l.checkEventCount(2);
+ l.checkEvent(AbstractFileConfiguration.EVENT_RELOAD, null,
+ getSourceURL(), true);
+ l.skipToLast(AbstractFileConfiguration.EVENT_RELOAD);
+ l.checkEvent(AbstractFileConfiguration.EVENT_RELOAD, null,
+ getSourceURL(), false);
+ l.done();
+ }
+
+ /**
+ * Tests accessing a property during a reload event to ensure that no
+ * infinite loops are possible.
+ */
+ @Test
+ public void testAccessPropertiesOnReload() throws ConfigurationException,
+ IOException
+ {
+ setUpFileConfiguration();
+ config.addConfigurationListener(new ConfigurationListener()
+ {
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ config.getString("test");
+ }
+ });
+ config.isEmpty();
+ l.checkEvent(AbstractFileConfiguration.EVENT_RELOAD, null,
+ getSourceURL(), true);
+ l.checkEvent(AbstractFileConfiguration.EVENT_RELOAD, null,
+ getSourceURL(), false);
+ l.done();
+ }
+
+ /**
+ * A dummy implementation of the ReloadingStrategy interface. This
+ * implementation will always indicate that a reload should be performed. So
+ * it can be used for testing reloading events.
+ */
+ static class AlwaysReloadingStrategy implements ReloadingStrategy
+ {
+ public void setConfiguration(FileConfiguration configuration)
+ {
+ }
+
+ public void init()
+ {
+ }
+
+ public boolean reloadingRequired()
+ {
+ return true;
+ }
+
+ public void reloadingPerformed()
+ {
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/event/ConfigurationListenerTestImpl.java b/src/test/java/org/apache/commons/configuration/event/ConfigurationListenerTestImpl.java
new file mode 100644
index 0000000..9da6e84
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/event/ConfigurationListenerTestImpl.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.event;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A test event listener class that can be used for testing whether
+ * configurations generated correct events.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: ConfigurationListenerTestImpl.java 1225648 2011-12-29 20:55:07Z oheger $
+ */
+public class ConfigurationListenerTestImpl implements ConfigurationListener
+{
+ /** The expected event source. */
+ private final Object expectedSource;
+
+ /** Stores the received events. */
+ private final List<ConfigurationEvent> events;
+
+ /**
+ * Creates a new instance of {@code ConfigurationListenerTestImpl} and sets
+ * the expected event source.
+ *
+ * @param source the event source (<b>null</b> if the source need not to be
+ * checked)
+ */
+ public ConfigurationListenerTestImpl(Object source)
+ {
+ expectedSource = source;
+ events = new LinkedList<ConfigurationEvent>();
+ }
+
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ events.add(event);
+ }
+
+ /**
+ * Checks if at least {@code minEvents} events have been received.
+ *
+ * @param minEvents the minimum number of expected events
+ */
+ public void checkEventCount(int minEvents)
+ {
+ assertTrue("Too view events received", events.size() >= minEvents);
+ }
+
+ /**
+ * Checks an expected event.
+ *
+ * @param type the event type
+ * @param propName the expected property name
+ * @param propValue the expected property value
+ * @param before the expected before flag
+ */
+ public void checkEvent(int type, String propName, Object propValue,
+ boolean before)
+ {
+ ConfigurationEvent e = nextEvent(type);
+ assertEquals("Wrong property name", propName, e.getPropertyName());
+ assertEquals("Wrong property value", propValue, e.getPropertyValue());
+ assertEquals("Wrong before flag", before, e.isBeforeUpdate());
+ }
+
+ /**
+ * Returns the next received event and checks for the expected type. This
+ * method can be used instead of {@code checkEvent()} for comparing
+ * complex event values.
+ *
+ * @param expectedType the expected type of the event
+ * @return the event object
+ */
+ public ConfigurationEvent nextEvent(int expectedType)
+ {
+ assertFalse("Too few events received", events.isEmpty());
+ ConfigurationEvent e = events.remove(0);
+ if (expectedSource != null)
+ {
+ assertEquals("Wrong event source", expectedSource, e.getSource());
+ }
+ assertEquals("Wrong event type", expectedType, e.getType());
+ return e;
+ }
+
+ /**
+ * Skips to the last received event and checks that no events of the given
+ * type have been received. This method is used by checks for detail events
+ * to ignore the detail events.
+ *
+ * @param type the event type
+ */
+ public void skipToLast(int type)
+ {
+ while (events.size() > 1)
+ {
+ ConfigurationEvent e = events.remove(0);
+ assertTrue("Found end event in details", type != e.getType());
+ }
+ }
+
+ /**
+ * Checks if all events has been processed.
+ */
+ public void done()
+ {
+ assertTrue("Too many events received", events.isEmpty());
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/event/TestDatabaseConfigurationEvents.java b/src/test/java/org/apache/commons/configuration/event/TestDatabaseConfigurationEvents.java
new file mode 100644
index 0000000..5f8053d
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/event/TestDatabaseConfigurationEvents.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.event;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.DatabaseConfigurationTestHelper;
+import org.junit.After;
+import org.junit.Before;
+
+/**
+ * A test class for the events generated by DatabaseConfiguration.
+ *
+ * @version $Id: TestDatabaseConfigurationEvents.java 1225648 2011-12-29 20:55:07Z oheger $
+ */
+public class TestDatabaseConfigurationEvents extends
+ AbstractTestConfigurationEvents
+{
+ /** The test helper. */
+ private DatabaseConfigurationTestHelper helper;
+
+ @Override
+ @Before
+ public void setUp() throws Exception
+ {
+ helper = new DatabaseConfigurationTestHelper();
+ helper.setUp();
+
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception
+ {
+ helper.tearDown();
+ }
+
+ @Override
+ protected AbstractConfiguration createConfiguration()
+ {
+ return helper.setUpConfig();
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/event/TestEventSource.java b/src/test/java/org/apache/commons/configuration/event/TestEventSource.java
new file mode 100644
index 0000000..622fe20
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/event/TestEventSource.java
@@ -0,0 +1,394 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.event;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collection;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for EventSource.
+ *
+ * @version $Id: TestEventSource.java 1225652 2011-12-29 21:00:57Z oheger $
+ */
+public class TestEventSource
+{
+ /** Constant for the event type used for testing. */
+ static final int TEST_TYPE = 42;
+
+ /** Constant for the event property name. */
+ static final String TEST_PROPNAME = "test.property.name";
+
+ /** Constant for the event property value. */
+ static final Object TEST_PROPVALUE = "a test property value";
+
+ /** The object under test. */
+ CountingEventSource source;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ source = new CountingEventSource();
+ }
+
+ /**
+ * Tests a newly created source object.
+ */
+ @Test
+ public void testInit()
+ {
+ assertTrue("Listeners list is not empty", source
+ .getConfigurationListeners().isEmpty());
+ assertFalse("Removing listener", source
+ .removeConfigurationListener(new TestListener()));
+ assertFalse("Detail events are enabled", source.isDetailEvents());
+ assertTrue("Error listeners list is not empty", source
+ .getErrorListeners().isEmpty());
+ }
+
+ /**
+ * Tests registering a new listener.
+ */
+ @Test
+ public void testAddConfigurationListener()
+ {
+ TestListener l = new TestListener();
+ source.addConfigurationListener(l);
+ Collection<ConfigurationListener> listeners = source.getConfigurationListeners();
+ assertEquals("Wrong number of listeners", 1, listeners.size());
+ assertTrue("Listener not in list", listeners.contains(l));
+ }
+
+ /**
+ * Tests adding an undefined configuration listener. This should cause an
+ * exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddNullConfigurationListener()
+ {
+ source.addConfigurationListener(null);
+ }
+
+ /**
+ * Tests removing a listener.
+ */
+ @Test
+ public void testRemoveConfigurationListener()
+ {
+ TestListener l = new TestListener();
+ assertFalse("Listener can be removed?", source
+ .removeConfigurationListener(l));
+ source.addConfigurationListener(l);
+ source.addConfigurationListener(new TestListener());
+ assertFalse("Unknown listener can be removed", source
+ .removeConfigurationListener(new TestListener()));
+ assertTrue("Could not remove listener", source
+ .removeConfigurationListener(l));
+ assertFalse("Listener still in list", source
+ .getConfigurationListeners().contains(l));
+ }
+
+ /**
+ * Tests if a null listener can be removed. This should be a no-op.
+ */
+ @Test
+ public void testRemoveNullConfigurationListener()
+ {
+ source.addConfigurationListener(new TestListener());
+ assertFalse("Null listener can be removed", source
+ .removeConfigurationListener(null));
+ assertEquals("Listener list was modified", 1, source
+ .getConfigurationListeners().size());
+ }
+
+ /**
+ * Tests whether the listeners list is read only.
+ */
+ @Test(expected = UnsupportedOperationException.class)
+ public void testGetConfigurationListenersUpdate()
+ {
+ source.addConfigurationListener(new TestListener());
+ Collection<ConfigurationListener> list = source.getConfigurationListeners();
+ list.clear();
+ }
+
+ /**
+ * Tests that the collection returned by getConfigurationListeners() is
+ * really a snapshot. A later added listener must not be visible.
+ */
+ @Test
+ public void testGetConfigurationListenersAddNew()
+ {
+ Collection<ConfigurationListener> list = source.getConfigurationListeners();
+ source.addConfigurationListener(new TestListener());
+ assertTrue("Listener snapshot not empty", list.isEmpty());
+ }
+
+ /**
+ * Tests enabling and disabling the detail events flag.
+ */
+ @Test
+ public void testSetDetailEvents()
+ {
+ source.setDetailEvents(true);
+ assertTrue("Detail events are disabled", source.isDetailEvents());
+ source.setDetailEvents(true);
+ source.setDetailEvents(false);
+ assertTrue("Detail events are disabled again", source.isDetailEvents());
+ source.setDetailEvents(false);
+ assertFalse("Detail events are still enabled", source.isDetailEvents());
+ }
+
+ /**
+ * Tests delivering an event to a listener.
+ */
+ @Test
+ public void testFireEvent()
+ {
+ TestListener l = new TestListener();
+ source.addConfigurationListener(l);
+ source.fireEvent(TEST_TYPE, TEST_PROPNAME, TEST_PROPVALUE, true);
+ assertEquals("Not 1 event created", 1, source.eventCount);
+ assertEquals("Listener not called once", 1, l.numberOfCalls);
+ assertEquals("Wrong event type", TEST_TYPE, l.lastEvent.getType());
+ assertEquals("Wrong property name", TEST_PROPNAME, l.lastEvent
+ .getPropertyName());
+ assertEquals("Wrong property value", TEST_PROPVALUE, l.lastEvent
+ .getPropertyValue());
+ assertTrue("Wrong before event flag", l.lastEvent.isBeforeUpdate());
+ }
+
+ /**
+ * Tests firing an event if there are no listeners.
+ */
+ @Test
+ public void testFireEventNoListeners()
+ {
+ source.fireEvent(TEST_TYPE, TEST_PROPNAME, TEST_PROPVALUE, false);
+ assertEquals("An event object was created", 0, source.eventCount);
+ }
+
+ /**
+ * Tests generating a detail event if detail events are not allowed.
+ */
+ @Test
+ public void testFireEventNoDetails()
+ {
+ TestListener l = new TestListener();
+ source.addConfigurationListener(l);
+ source.setDetailEvents(false);
+ source.fireEvent(TEST_TYPE, TEST_PROPNAME, TEST_PROPVALUE, false);
+ assertEquals("Event object was created", 0, source.eventCount);
+ assertEquals("Listener was called", 0, l.numberOfCalls);
+ }
+
+ /**
+ * Tests whether an event listener can deregister itself in reaction of a
+ * delivered event.
+ */
+ @Test
+ public void testRemoveListenerInFireEvent()
+ {
+ ConfigurationListener lstRemove = new ConfigurationListener()
+ {
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ source.removeConfigurationListener(this);
+ }
+ };
+
+ source.addConfigurationListener(lstRemove);
+ TestListener l = new TestListener();
+ source.addConfigurationListener(l);
+ source.fireEvent(TEST_TYPE, TEST_PROPNAME, TEST_PROPVALUE, false);
+ assertEquals("Listener was not called", 1, l.numberOfCalls);
+ assertEquals("Listener was not removed", 1, source
+ .getConfigurationListeners().size());
+ }
+
+ /**
+ * Tests registering a new error listener.
+ */
+ @Test
+ public void testAddErrorListener()
+ {
+ TestListener l = new TestListener();
+ source.addErrorListener(l);
+ Collection<ConfigurationErrorListener> listeners = source.getErrorListeners();
+ assertEquals("Wrong number of listeners", 1, listeners.size());
+ assertTrue("Listener not in list", listeners.contains(l));
+ }
+
+ /**
+ * Tests adding an undefined error listener. This should cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddNullErrorListener()
+ {
+ source.addErrorListener(null);
+ }
+
+ /**
+ * Tests removing an error listener.
+ */
+ @Test
+ public void testRemoveErrorListener()
+ {
+ TestListener l = new TestListener();
+ assertFalse("Listener can be removed?", source.removeErrorListener(l));
+ source.addErrorListener(l);
+ source.addErrorListener(new TestListener());
+ assertFalse("Unknown listener can be removed", source
+ .removeErrorListener(new TestListener()));
+ assertTrue("Could not remove listener", source.removeErrorListener(l));
+ assertFalse("Listener still in list", source.getErrorListeners()
+ .contains(l));
+ }
+
+ /**
+ * Tests if a null error listener can be removed. This should be a no-op.
+ */
+ @Test
+ public void testRemoveNullErrorListener()
+ {
+ source.addErrorListener(new TestListener());
+ assertFalse("Null listener can be removed", source
+ .removeErrorListener(null));
+ assertEquals("Listener list was modified", 1, source
+ .getErrorListeners().size());
+ }
+
+ /**
+ * Tests whether the listeners list is read only.
+ */
+ @Test(expected = UnsupportedOperationException.class)
+ public void testGetErrorListenersUpdate()
+ {
+ source.addErrorListener(new TestListener());
+ Collection<ConfigurationErrorListener> list = source.getErrorListeners();
+ list.clear();
+ }
+
+ /**
+ * Tests delivering an error event to a listener.
+ */
+ @Test
+ public void testFireError()
+ {
+ TestListener l = new TestListener();
+ source.addErrorListener(l);
+ Exception testException = new Exception("A test");
+ source.fireError(TEST_TYPE, TEST_PROPNAME, TEST_PROPVALUE,
+ testException);
+ assertEquals("Not 1 event created", 1, source.errorCount);
+ assertEquals("Error listener not called once", 1, l.numberOfErrors);
+ assertEquals("Normal event was generated", 0, l.numberOfCalls);
+ assertEquals("Wrong event type", TEST_TYPE, l.lastEvent.getType());
+ assertEquals("Wrong property name", TEST_PROPNAME, l.lastEvent
+ .getPropertyName());
+ assertEquals("Wrong property value", TEST_PROPVALUE, l.lastEvent
+ .getPropertyValue());
+ assertEquals("Wrong Throwable object", testException,
+ ((ConfigurationErrorEvent) l.lastEvent).getCause());
+ }
+
+ /**
+ * Tests firing an error event if there are no error listeners.
+ */
+ @Test
+ public void testFireErrorNoListeners()
+ {
+ source.fireError(TEST_TYPE, TEST_PROPNAME, TEST_PROPVALUE,
+ new Exception());
+ assertEquals("An error event object was created", 0, source.errorCount);
+ }
+
+ /**
+ * Tests cloning an event source object. The registered listeners should not
+ * be registered at the clone.
+ */
+ @Test
+ public void testClone() throws CloneNotSupportedException
+ {
+ source.addConfigurationListener(new TestListener());
+ source.addErrorListener(new TestListener());
+ EventSource copy = (EventSource) source.clone();
+ assertTrue("Configuration listeners registered for clone", copy
+ .getConfigurationListeners().isEmpty());
+ assertTrue("Error listeners registered for clone", copy
+ .getErrorListeners().isEmpty());
+ }
+
+ /**
+ * A test event listener implementation.
+ */
+ static class TestListener implements ConfigurationListener,
+ ConfigurationErrorListener
+ {
+ ConfigurationEvent lastEvent;
+
+ int numberOfCalls;
+
+ int numberOfErrors;
+
+ public void configurationChanged(ConfigurationEvent event)
+ {
+ lastEvent = event;
+ numberOfCalls++;
+ }
+
+ public void configurationError(ConfigurationErrorEvent event)
+ {
+ lastEvent = event;
+ numberOfErrors++;
+ }
+ }
+
+ /**
+ * A specialized event source implementation that counts the number of
+ * created event objects. It is used to test whether the
+ * {@code fireEvent()} methods only creates event objects if
+ * necessary. It also allows testing the clone() operation.
+ */
+ static class CountingEventSource extends EventSource implements Cloneable
+ {
+ int eventCount;
+
+ int errorCount;
+
+ @Override
+ protected ConfigurationEvent createEvent(int type, String propName,
+ Object propValue, boolean before)
+ {
+ eventCount++;
+ return super.createEvent(type, propName, propValue, before);
+ }
+
+ @Override
+ protected ConfigurationErrorEvent createErrorEvent(int type,
+ String propName, Object value, Throwable ex)
+ {
+ errorCount++;
+ return super.createErrorEvent(type, propName, value, ex);
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/event/TestHierarchicalConfigurationEvents.java b/src/test/java/org/apache/commons/configuration/event/TestHierarchicalConfigurationEvents.java
new file mode 100644
index 0000000..ef3bbf2
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/event/TestHierarchicalConfigurationEvents.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.event;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.apache.commons.configuration.SubnodeConfiguration;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.DefaultConfigurationNode;
+import org.junit.Test;
+
+/**
+ * Test class for the events generated by hierarchical configurations.
+ *
+ * @version $Id: TestHierarchicalConfigurationEvents.java 1225648 2011-12-29 20:55:07Z oheger $
+ */
+public class TestHierarchicalConfigurationEvents extends
+ AbstractTestConfigurationEvents
+{
+ @Override
+ protected AbstractConfiguration createConfiguration()
+ {
+ return new HierarchicalConfiguration();
+ }
+
+ /**
+ * Tests events generated by the clearTree() method.
+ */
+ @Test
+ public void testClearTreeEvent()
+ {
+ HierarchicalConfiguration hc = (HierarchicalConfiguration) config;
+ String key = EXIST_PROPERTY.substring(0, EXIST_PROPERTY.indexOf('.'));
+ Collection<ConfigurationNode> nodes = hc.getExpressionEngine()
+ .query(hc.getRootNode(), key);
+ hc.clearTree(key);
+ l.checkEvent(HierarchicalConfiguration.EVENT_CLEAR_TREE, key, null,
+ true);
+ l.checkEvent(HierarchicalConfiguration.EVENT_CLEAR_TREE, key, nodes,
+ false);
+ l.done();
+ }
+
+ /**
+ * Tests events generated by the addNodes() method.
+ */
+ @Test
+ public void testAddNodesEvent()
+ {
+ HierarchicalConfiguration hc = (HierarchicalConfiguration) config;
+ Collection<ConfigurationNode> nodes = new ArrayList<ConfigurationNode>(1);
+ nodes.add(new DefaultConfigurationNode("a_key", TEST_PROPVALUE));
+ hc.addNodes(TEST_PROPNAME, nodes);
+ l.checkEvent(HierarchicalConfiguration.EVENT_ADD_NODES, TEST_PROPNAME,
+ nodes, true);
+ l.checkEvent(HierarchicalConfiguration.EVENT_ADD_NODES, TEST_PROPNAME,
+ nodes, false);
+ l.done();
+ }
+
+ /**
+ * Tests events generated by addNodes() when the list of nodes is empty. In
+ * this case no events should be generated.
+ */
+ @Test
+ public void testAddNodesEmptyEvent()
+ {
+ ((HierarchicalConfiguration) config).addNodes(TEST_PROPNAME,
+ new ArrayList<ConfigurationNode>());
+ l.done();
+ }
+
+ /**
+ * Tests whether manipulations of a subnode configuration trigger correct
+ * events.
+ */
+ @Test
+ public void testSubnodeChangedEvent()
+ {
+ SubnodeConfiguration sub = ((HierarchicalConfiguration) config)
+ .configurationAt(EXIST_PROPERTY);
+ sub.addProperty("newProp", "newValue");
+ checkSubnodeEvent(l
+ .nextEvent(HierarchicalConfiguration.EVENT_SUBNODE_CHANGED),
+ true);
+ checkSubnodeEvent(l
+ .nextEvent(HierarchicalConfiguration.EVENT_SUBNODE_CHANGED),
+ false);
+ l.done();
+ }
+
+ /**
+ * Tests whether a received event contains a correct subnode event.
+ *
+ * @param event the event object
+ * @param before the expected before flag
+ */
+ private void checkSubnodeEvent(ConfigurationEvent event, boolean before)
+ {
+ assertEquals("Wrong before flag of nesting event", before, event
+ .isBeforeUpdate());
+ assertTrue("No subnode event found in value",
+ event.getPropertyValue() instanceof ConfigurationEvent);
+ ConfigurationEvent evSub = (ConfigurationEvent) event
+ .getPropertyValue();
+ assertEquals("Wrong event type",
+ HierarchicalConfiguration.EVENT_ADD_PROPERTY, evSub.getType());
+ assertEquals("Wrong property name", "newProp", evSub.getPropertyName());
+ assertEquals("Wrong property value", "newValue", evSub
+ .getPropertyValue());
+ assertEquals("Wrong before flag", before, evSub.isBeforeUpdate());
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/event/TestMapConfigurationEvents.java b/src/test/java/org/apache/commons/configuration/event/TestMapConfigurationEvents.java
new file mode 100644
index 0000000..08ab7ee
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/event/TestMapConfigurationEvents.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.event;
+
+import java.util.HashMap;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.MapConfiguration;
+
+/**
+ * Test class for the events generated by MapConfiguration.
+ *
+ * @version $Id: TestMapConfigurationEvents.java 1225648 2011-12-29 20:55:07Z oheger $
+ */
+public class TestMapConfigurationEvents extends AbstractTestConfigurationEvents
+{
+ @Override
+ protected AbstractConfiguration createConfiguration()
+ {
+ return new MapConfiguration(new HashMap<String, Object>());
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/event/TestPropertiesConfigurationEvents.java b/src/test/java/org/apache/commons/configuration/event/TestPropertiesConfigurationEvents.java
new file mode 100644
index 0000000..74de18c
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/event/TestPropertiesConfigurationEvents.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.event;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.ConfigurationAssert;
+import org.apache.commons.configuration.PropertiesConfiguration;
+
+/**
+ * Test class for the events generated by properties configurations. This class
+ * also tests reload events.
+ *
+ * @version $Id: TestPropertiesConfigurationEvents.java 1225648 2011-12-29 20:55:07Z oheger $
+ */
+public class TestPropertiesConfigurationEvents extends
+ AbstractTestFileConfigurationEvents
+{
+ /** The file to be loaded.*/
+ static final File TEST_FILE = ConfigurationAssert.getTestFile("test.properties");
+
+ @Override
+ protected AbstractConfiguration createConfiguration()
+ {
+ return new PropertiesConfiguration();
+ }
+
+ @Override
+ protected URL getSourceURL() throws IOException
+ {
+ return TEST_FILE.toURI().toURL();
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/event/TestSubsetConfigurationEvents.java b/src/test/java/org/apache/commons/configuration/event/TestSubsetConfigurationEvents.java
new file mode 100644
index 0000000..758c223
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/event/TestSubsetConfigurationEvents.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.event;
+
+import java.util.HashMap;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.MapConfiguration;
+import org.apache.commons.configuration.SubsetConfiguration;
+
+/**
+ * Test class for the events generated by SubsetConfiguration.
+ *
+ * @version $Id: TestSubsetConfigurationEvents.java 1225648 2011-12-29 20:55:07Z oheger $
+ */
+public class TestSubsetConfigurationEvents extends AbstractTestConfigurationEvents
+{
+ @Override
+ protected AbstractConfiguration createConfiguration()
+ {
+ return (SubsetConfiguration)new MapConfiguration(new HashMap<String, Object>()).subset("test");
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/event/TestXMLConfigurationEvents.java b/src/test/java/org/apache/commons/configuration/event/TestXMLConfigurationEvents.java
new file mode 100644
index 0000000..b02f071
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/event/TestXMLConfigurationEvents.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.event;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.ConfigurationAssert;
+import org.apache.commons.configuration.XMLConfiguration;
+
+/**
+ * Test class for events generated by XMLConfiguration.
+ *
+ * @version $Id: TestXMLConfigurationEvents.java 1225648 2011-12-29 20:55:07Z oheger $
+ */
+public class TestXMLConfigurationEvents extends
+ AbstractTestFileConfigurationEvents
+{
+ static final File TEST_FILE = ConfigurationAssert.getTestFile("test.xml");
+
+ @Override
+ protected URL getSourceURL() throws IOException
+ {
+ return TEST_FILE.toURI().toURL();
+ }
+
+ @Override
+ protected AbstractConfiguration createConfiguration()
+ {
+ return new XMLConfiguration();
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/interpol/TestConfigurationInterpolator.java b/src/test/java/org/apache/commons/configuration/interpol/TestConfigurationInterpolator.java
new file mode 100644
index 0000000..cb0ab2f
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/interpol/TestConfigurationInterpolator.java
@@ -0,0 +1,351 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.interpol;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.commons.lang.text.StrLookup;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for ConfigurationInterpolator.
+ *
+ * @version $Id: TestConfigurationInterpolator.java 1225653 2011-12-29 21:06:26Z oheger $
+ */
+public class TestConfigurationInterpolator
+{
+ /** Constant for a test variable prefix. */
+ private static final String TEST_PREFIX = "prefix";
+
+ /** Constant for a test variable name. */
+ private static final String TEST_NAME = "varname";
+
+ /** Constant for the value of the test variable. */
+ private static final String TEST_VALUE = "TestVariableValue";
+
+ /** Stores the object to be tested. */
+ private ConfigurationInterpolator interpolator;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ interpolator = new ConfigurationInterpolator();
+ }
+
+ /**
+ * Cleans the test environment. Deregisters the test lookup object if
+ * necessary.
+ */
+ @After
+ public void tearDown() throws Exception
+ {
+ ConfigurationInterpolator.deregisterGlobalLookup(TEST_PREFIX);
+ }
+
+ /**
+ * Tests creating an instance. Does it contain some predefined lookups?
+ */
+ @Test
+ public void testInit()
+ {
+ assertNull("A default lookup is set", interpolator.getDefaultLookup());
+ assertFalse("No predefined lookups", interpolator.prefixSet().isEmpty());
+ }
+
+ /**
+ * Tries to register a global lookup for a null prefix. This should cause an
+ * exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testRegisterGlobalLookupNullPrefix()
+ {
+ ConfigurationInterpolator.registerGlobalLookup(null, StrLookup
+ .noneLookup());
+ }
+
+ /**
+ * Tries to register a global null lookup. This should cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testRegisterGlobalLookupNull()
+ {
+ ConfigurationInterpolator.registerGlobalLookup(TEST_PREFIX, null);
+ }
+
+ /**
+ * Tests registering a global lookup object. This lookup object should then
+ * be available for instances created later on.
+ */
+ @Test
+ public void testRegisterGlobalLookup()
+ {
+ ConfigurationInterpolator.registerGlobalLookup(TEST_PREFIX, StrLookup
+ .noneLookup());
+ ConfigurationInterpolator int2 = new ConfigurationInterpolator();
+ assertTrue("No lookup registered for test prefix", int2.prefixSet()
+ .contains(TEST_PREFIX));
+ assertFalse("Existing instance was modified", interpolator.prefixSet()
+ .contains(TEST_PREFIX));
+ }
+
+ /**
+ * Tests deregistering a global lookup object.
+ */
+ @Test
+ public void testDeregisterGlobalLookup()
+ {
+ ConfigurationInterpolator.registerGlobalLookup(TEST_PREFIX, StrLookup
+ .noneLookup());
+ assertTrue("Lookup could not be deregistered",
+ ConfigurationInterpolator.deregisterGlobalLookup(TEST_PREFIX));
+ ConfigurationInterpolator int2 = new ConfigurationInterpolator();
+ assertFalse("Deregistered lookup still available", int2.prefixSet()
+ .contains(TEST_PREFIX));
+ }
+
+ /**
+ * Tests deregistering an unknown lookup.
+ */
+ @Test
+ public void testDeregisterGlobalLookupNonExisting()
+ {
+ assertFalse("Could deregister unknown global lookup",
+ ConfigurationInterpolator.deregisterGlobalLookup(TEST_PREFIX));
+ }
+
+ /**
+ * Tests registering a lookup object at an instance.
+ */
+ @Test
+ public void testRegisterLookup()
+ {
+ int cnt = interpolator.prefixSet().size();
+ interpolator.registerLookup(TEST_PREFIX, StrLookup.noneLookup());
+ assertTrue("New lookup not registered", interpolator.prefixSet()
+ .contains(TEST_PREFIX));
+ assertEquals("Wrong number of registered lookups", cnt + 1,
+ interpolator.prefixSet().size());
+ ConfigurationInterpolator int2 = new ConfigurationInterpolator();
+ assertFalse("Local registration has global impact", int2.prefixSet()
+ .contains(TEST_PREFIX));
+ }
+
+ /**
+ * Tests registering a null lookup object. This should cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testRegisterLookupNull()
+ {
+ interpolator.registerLookup(TEST_PREFIX, null);
+ }
+
+ /**
+ * Tests registering a lookup object for an undefined prefix. This should
+ * cause an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testRegisterLookupNullPrefix()
+ {
+ interpolator.registerLookup(null, StrLookup.noneLookup());
+ }
+
+ /**
+ * Tests deregistering a local lookup object.
+ */
+ @Test
+ public void testDeregisterLookup()
+ {
+ interpolator.registerLookup(TEST_PREFIX, StrLookup.noneLookup());
+ assertTrue("Derigstration not successfull", interpolator
+ .deregisterLookup(TEST_PREFIX));
+ assertFalse("Deregistered prefix still contained", interpolator
+ .prefixSet().contains(TEST_PREFIX));
+ }
+
+ /**
+ * Tests deregistering an unknown lookup object.
+ */
+ @Test
+ public void testDeregisterLookupNonExisting()
+ {
+ assertFalse("Could deregister unknown lookup", interpolator
+ .deregisterLookup(TEST_PREFIX));
+ }
+
+ /**
+ * Tests whether a variable can be resolved using the associated lookup
+ * object. The lookup is identified by the variable's prefix.
+ */
+ @Test
+ public void testLookupWithPrefix()
+ {
+ interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
+ assertEquals("Wrong variable value", TEST_VALUE, interpolator
+ .lookup(TEST_PREFIX + ':' + TEST_NAME));
+ }
+
+ /**
+ * Tests the behavior of the lookup method for variables with an unknown
+ * prefix. These variables should not be resolved.
+ */
+ @Test
+ public void testLookupWithUnknownPrefix()
+ {
+ interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
+ assertNull("Variable could be resolved", interpolator
+ .lookup("UnknownPrefix:" + TEST_NAME));
+ assertNull("Variable with empty prefix could be resolved", interpolator
+ .lookup(":" + TEST_NAME));
+ }
+
+ /**
+ * Tests looking up a variable without a prefix. This should trigger the
+ * default lookup object.
+ */
+ @Test
+ public void testLookupDefault()
+ {
+ interpolator.setDefaultLookup(setUpTestLookup());
+ assertEquals("Wrong variable value", TEST_VALUE, interpolator
+ .lookup(TEST_NAME));
+ }
+
+ /**
+ * Tests looking up a variable without a prefix when no default lookup is
+ * specified. Result should be null in this case.
+ */
+ @Test
+ public void testLookupNoDefault()
+ {
+ assertNull("Variable could be resolved", interpolator.lookup(TEST_NAME));
+ }
+
+ /**
+ * Tests the empty variable prefix. This is a special case, but legal.
+ */
+ @Test
+ public void testLookupEmptyPrefix()
+ {
+ interpolator.registerLookup("", setUpTestLookup());
+ assertEquals("Wrong variable value", TEST_VALUE, interpolator
+ .lookup(":" + TEST_NAME));
+ }
+
+ /**
+ * Tests an empty variable name.
+ */
+ @Test
+ public void testLookupEmptyVarName()
+ {
+ Map<String, String> map = new HashMap<String, String>();
+ map.put("", TEST_VALUE);
+ interpolator.registerLookup(TEST_PREFIX, StrLookup.mapLookup(map));
+ assertEquals("Wrong variable value", TEST_VALUE, interpolator
+ .lookup(TEST_PREFIX + ":"));
+ }
+
+ /**
+ * Tests an empty variable name without a prefix.
+ */
+ @Test
+ public void testLookupDefaultEmptyVarName()
+ {
+ Map<String, String> map = new HashMap<String, String>();
+ map.put("", TEST_VALUE);
+ interpolator.setDefaultLookup(StrLookup.mapLookup(map));
+ assertEquals("Wrong variable value", TEST_VALUE, interpolator
+ .lookup(""));
+ }
+
+ /**
+ * Tests looking up a null variable. Result shoult be null, too.
+ */
+ @Test
+ public void testLookupNull()
+ {
+ assertNull("Could resolve null variable", interpolator.lookup(null));
+ }
+
+ /**
+ * Creates a lookup object that can resolve the test variable.
+ *
+ * @return the test lookup object
+ */
+ private StrLookup setUpTestLookup()
+ {
+ Map<String, String> map = new HashMap<String, String>();
+ map.put(TEST_NAME, TEST_VALUE);
+ return StrLookup.mapLookup(map);
+ }
+
+ /**
+ * Tests whether system properties can be correctly resolved.
+ */
+ @Test
+ public void testLookupSysProperties()
+ {
+ Properties sysProps = System.getProperties();
+ for (Object prop : sysProps.keySet())
+ {
+ String key = (String) prop;
+ assertEquals("Wrong value for system property " + key, sysProps
+ .getProperty(key), interpolator
+ .lookup(ConfigurationInterpolator.PREFIX_SYSPROPERTIES
+ + ":" + key));
+ }
+ }
+
+ /**
+ * Tests whether constants can be correctly resolved.
+ */
+ @Test
+ public void testLookupConstants()
+ {
+ String varName = ConfigurationInterpolator.class.getName()
+ + ".PREFIX_CONSTANTS";
+ assertEquals("Wrong constant value",
+ ConfigurationInterpolator.PREFIX_CONSTANTS, interpolator
+ .lookup(ConfigurationInterpolator.PREFIX_CONSTANTS
+ + ":" + varName));
+ }
+
+ /**
+ * Tests whether the default lookup is called for variables with a prefix
+ * when the lookup that was registered for this prefix is not able to
+ * resolve the variable.
+ */
+ @Test
+ public void testLookupDefaultAfterPrefixFails()
+ {
+ final String varName = TEST_PREFIX + ':' + TEST_NAME + "2";
+ interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.put(varName, TEST_VALUE);
+ interpolator.setDefaultLookup(StrLookup.mapLookup(map));
+ assertEquals("Variable is not resolved by default lookup", TEST_VALUE,
+ interpolator.lookup(varName));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/interpol/TestConstantLookup.java b/src/test/java/org/apache/commons/configuration/interpol/TestConstantLookup.java
new file mode 100644
index 0000000..3d46885
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/interpol/TestConstantLookup.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.interpol;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.awt.event.KeyEvent;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for ConstantLookup.
+ *
+ * @version $Id: TestConstantLookup.java 1225656 2011-12-29 21:09:11Z oheger $
+ */
+public class TestConstantLookup
+{
+ /** Constant for the name of the test class. */
+ private static final String CLS_NAME = ConfigurationInterpolator.class
+ .getName() + '.';
+
+ /** Constant for the name of the test field. */
+ private static final String FIELD = "PREFIX_CONSTANTS";
+
+ /** Constant for the test variable name. */
+ private static final String VARNAME = CLS_NAME + FIELD;
+
+ /** The lookup object to be tested. */
+ private ConstantLookup lookup;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ lookup = new ConstantLookup();
+ }
+
+ /**
+ * Clears the test environment. Here the static cache of the constant lookup
+ * class is wiped out.
+ */
+ @After
+ public void tearDown() throws Exception
+ {
+ ConstantLookup.clear();
+ }
+
+ /**
+ * Tests resolving a valid constant.
+ */
+ @Test
+ public void testLookupConstant()
+ {
+ assertEquals("Wrong value of constant",
+ ConfigurationInterpolator.PREFIX_CONSTANTS, lookup
+ .lookup(VARNAME));
+ }
+
+ /**
+ * Tests resolving a non existing constant. Result should be null.
+ */
+ @Test
+ public void testLookupNonExisting()
+ {
+ assertNull("Non null return value for non existing constant", lookup
+ .lookup(CLS_NAME + "NO_FIELD"));
+ }
+
+ /**
+ * Tests resolving a private constant. Because a private field cannot be
+ * accessed this should again yield null.
+ */
+ @Test
+ public void testLookupPrivate()
+ {
+ assertNull("Non null return value for non accessable field", lookup
+ .lookup(CLS_NAME + "PREFIX_SEPARATOR"));
+ }
+
+ /**
+ * Tests resolving a field from an unknown class.
+ */
+ @Test
+ public void testLookupUnknownClass()
+ {
+ assertNull("Non null return value for unknown class", lookup
+ .lookup("org.apache.commons.configuration.NonExistingConfig."
+ + FIELD));
+ }
+
+ /**
+ * Tries to resolve a variable with an invalid syntax: The name does not
+ * contain a dot as a field separator.
+ */
+ @Test
+ public void testLookupInvalidSyntax()
+ {
+ assertNull("Non null return value for invalid variable name", lookup
+ .lookup("InvalidVariableName"));
+ }
+
+ /**
+ * Tests looking up a null variable.
+ */
+ @Test
+ public void testLookupNull()
+ {
+ assertNull("Non null return value for null variable", lookup
+ .lookup(null));
+ }
+
+ /**
+ * Tests accessing the cache by querying a variable twice.
+ */
+ @Test
+ public void testLookupCache()
+ {
+ testLookupConstant();
+ testLookupConstant();
+ }
+
+ /**
+ * Tests resolving a non string constant. Then looks the same variable up
+ * from the cache.
+ */
+ @Test
+ public void testLookupNonStringFromCache()
+ {
+ final String var = KeyEvent.class.getName() + ".VK_ESCAPE";
+ final String expected = String.valueOf(KeyEvent.VK_ESCAPE);
+ assertEquals("Wrong result of first lookup", expected, lookup
+ .lookup(var));
+ assertEquals("Wrong result of 2nd lookup", expected, lookup.lookup(var));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/interpol/TestEnvironmentLookup.java b/src/test/java/org/apache/commons/configuration/interpol/TestEnvironmentLookup.java
new file mode 100644
index 0000000..478d312
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/interpol/TestEnvironmentLookup.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.interpol;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.Iterator;
+
+import org.apache.commons.configuration.EnvironmentConfiguration;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for EnvironmentLookup.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestEnvironmentLookup.java 1225658 2011-12-29 21:11:03Z oheger $
+ */
+public class TestEnvironmentLookup
+{
+ /** The lookup to be tested. */
+ private EnvironmentLookup lookup;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ lookup = new EnvironmentLookup();
+ }
+
+ /**
+ * Tests whether environment variables can be queried.
+ */
+ @Test
+ public void testLookup()
+ {
+ EnvironmentConfiguration envConf = new EnvironmentConfiguration();
+ for (Iterator<String> it = envConf.getKeys(); it.hasNext();)
+ {
+ String var = it.next();
+ assertEquals("Wrong value for " + var, envConf.getString(var),
+ lookup.lookup(var));
+ }
+ }
+
+ /**
+ * Tries to lookup a non existing property.
+ */
+ @Test
+ public void testLookupNonExisting()
+ {
+ assertNull("Got result for non existing environment variable", lookup
+ .lookup("a non existing variable!"));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/interpol/TestExprLookup.java b/src/test/java/org/apache/commons/configuration/interpol/TestExprLookup.java
new file mode 100644
index 0000000..2b1dc3e
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/interpol/TestExprLookup.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.interpol;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+
+import org.apache.commons.configuration.ConfigurationAssert;
+import org.apache.commons.configuration.XMLConfiguration;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.logging.impl.Log4JLogger;
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.SimpleLayout;
+import org.junit.Test;
+
+/**
+ * Test class for ExprLookup.
+ *
+ * @version $Id: TestExprLookup.java 1225659 2011-12-29 21:12:54Z oheger $
+ */
+public class TestExprLookup
+{
+ private static File TEST_FILE = ConfigurationAssert.getTestFile("test.xml");
+
+ private static String PATTERN1 =
+ "String.replace(Util.message, 'Hello', 'Goodbye') + System.getProperty('user.name')";
+ private static String PATTERN2 =
+ "'$[element] ' + String.trimToEmpty('$[space.description]')";
+
+ @Test
+ public void testLookup() throws Exception
+ {
+ ConsoleAppender app = new ConsoleAppender(new SimpleLayout());
+ Log log = LogFactory.getLog("TestLogger");
+ Logger logger = ((Log4JLogger)log).getLogger();
+ logger.addAppender(app);
+ logger.setLevel(Level.DEBUG);
+ logger.setAdditivity(false);
+ ExprLookup.Variables vars = new ExprLookup.Variables();
+ vars.add(new ExprLookup.Variable("String", org.apache.commons.lang.StringUtils.class));
+ vars.add(new ExprLookup.Variable("Util", new Utility("Hello")));
+ vars.add(new ExprLookup.Variable("System", "Class:java.lang.System"));
+ XMLConfiguration config = new XMLConfiguration(TEST_FILE);
+ config.setLogger(log);
+ ExprLookup lookup = new ExprLookup(vars);
+ lookup.setConfiguration(config);
+ String str = lookup.lookup(PATTERN1);
+ assertTrue(str.startsWith("Goodbye"));
+ str = lookup.lookup(PATTERN2);
+ assertTrue("Incorrect value: " + str, str.equals("value Some text"));
+ logger.removeAppender(app);
+
+ }
+
+ public static class Utility
+ {
+ String message;
+
+ public Utility(String msg)
+ {
+ this.message = msg;
+ }
+
+ public String getMessage()
+ {
+ return message;
+ }
+
+ public String str(String str)
+ {
+ return str;
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/plist/AbstractTestPListEvents.java b/src/test/java/org/apache/commons/configuration/plist/AbstractTestPListEvents.java
new file mode 100644
index 0000000..af4981d
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/plist/AbstractTestPListEvents.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.plist;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.event.AbstractTestConfigurationEvents;
+import org.junit.Test;
+
+/**
+ * A base test class for testing the events generated by the plist
+ * configurations. This class especially checks events related to the special
+ * handling of byte arrays.
+ *
+ * @version $Id: AbstractTestPListEvents.java 1225901 2011-12-30 19:37:46Z oheger $
+ */
+public abstract class AbstractTestPListEvents extends
+ AbstractTestConfigurationEvents
+{
+ /** Constant for the name of the byte array property. */
+ private static final String TEST_PROPBYTE = "byteData";
+
+ /** Constant for the test byte array used for testing. */
+ private static final byte[] TEST_DATA =
+ { 1, 2, 3 };
+
+ /**
+ * Tests the events generated by an added byte array property.
+ */
+ @Test
+ public void testAddByteArrayPropertyEvent()
+ {
+ config.addProperty(TEST_PROPBYTE, TEST_DATA);
+ l.checkEvent(AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_PROPBYTE,
+ TEST_DATA, true);
+ l.checkEvent(AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_PROPBYTE,
+ TEST_DATA, false);
+ l.done();
+ }
+
+ /**
+ * Tests the events generated by setting a byte array property.
+ */
+ @Test
+ public void testSetByteArrayPropertyEvent()
+ {
+ config.setProperty(TEST_PROPBYTE, TEST_DATA);
+ l.checkEvent(AbstractConfiguration.EVENT_SET_PROPERTY, TEST_PROPBYTE,
+ TEST_DATA, true);
+ l.checkEvent(AbstractConfiguration.EVENT_SET_PROPERTY, TEST_PROPBYTE,
+ TEST_DATA, false);
+ l.done();
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/plist/TestPropertyListConfiguration.java b/src/test/java/org/apache/commons/configuration/plist/TestPropertyListConfiguration.java
new file mode 100644
index 0000000..7c0b3d7
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/plist/TestPropertyListConfiguration.java
@@ -0,0 +1,414 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.plist;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.io.StringReader;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TimeZone;
+
+import junitx.framework.ArrayAssert;
+import junitx.framework.ListAssert;
+import junitx.framework.ObjectAssert;
+
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.configuration.ConfigurationAssert;
+import org.apache.commons.configuration.ConfigurationComparator;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.StrictConfigurationComparator;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Emmanuel Bourg
+ * @version $Id: TestPropertyListConfiguration.java 1225902 2011-12-30 19:46:24Z oheger $
+ */
+public class TestPropertyListConfiguration
+{
+ private PropertyListConfiguration config;
+
+ private String testProperties = ConfigurationAssert.getTestFile("test.plist").getAbsolutePath();
+
+ @Before
+ public void setUp() throws Exception
+ {
+ config = new PropertyListConfiguration();
+ config.setFileName(testProperties);
+ config.load();
+ }
+
+ @Test
+ public void testLoad()
+ {
+ assertFalse("the configuration is empty", config.isEmpty());
+ }
+
+ @Test
+ public void testLoadWithError()
+ {
+ config = new PropertyListConfiguration();
+ try {
+ config.load(new StringReader(""));
+ fail("No exception thrown on loading an empty file");
+ } catch (ConfigurationException e) {
+ // expected
+ assertNotNull(e.getMessage());
+ }
+ }
+
+ @Test
+ public void testString()
+ {
+ assertEquals("simple-string", "string1", config.getProperty("simple-string"));
+ }
+
+ @Test
+ public void testQuotedString()
+ {
+ assertEquals("quoted-string", "string2", config.getProperty("quoted-string"));
+ assertEquals("quoted-string2", "this is a string", config.getProperty("quoted-string2"));
+ assertEquals("complex-string", "this is a \"complex\" string {(=,;)}", config.getProperty("complex-string"));
+ }
+
+ @Test
+ public void testEmptyArray()
+ {
+ String key = "empty-array";
+ assertNotNull("array null", config.getProperty(key));
+
+ List<?> list = (List<?>) config.getProperty(key);
+ assertTrue("array is not empty", list.isEmpty());
+ }
+
+ @Test
+ public void testArray()
+ {
+ String key = "array";
+ assertNotNull("array null", config.getProperty(key));
+
+ List<?> list = (List<?>) config.getProperty(key);
+ assertFalse("array is empty", list.isEmpty());
+
+ assertEquals("1st value", "value1", list.get(0));
+ assertEquals("2nd value", "value2", list.get(1));
+ assertEquals("3rd value", "value3", list.get(2));
+ }
+
+ @Test
+ public void testNestedArrays()
+ {
+ String key = "nested-arrays";
+
+ Object array = config.getProperty(key);
+
+ // root array
+ assertNotNull("array not found", array);
+ ObjectAssert.assertInstanceOf("the array element is not parsed as a List", List.class, array);
+ List<?> list = config.getList(key);
+
+ assertFalse("empty array", list.isEmpty());
+ assertEquals("size", 2, list.size());
+
+ // 1st array
+ ObjectAssert.assertInstanceOf("the array element is not parsed as a List", List.class, list.get(0));
+ List<?> list1 = (List<?>) list.get(0);
+ assertFalse("nested array 1 is empty", list1.isEmpty());
+ assertEquals("size", 2, list1.size());
+ assertEquals("1st element", "a", list1.get(0));
+ assertEquals("2nd element", "b", list1.get(1));
+
+ // 2nd array
+ ObjectAssert.assertInstanceOf("the array element is not parsed as a List", List.class, list.get(1));
+ List<?> list2 = (List<?>) list.get(1);
+ assertFalse("nested array 2 is empty", list2.isEmpty());
+ assertEquals("size", 2, list2.size());
+ assertEquals("1st element", "c", list2.get(0));
+ assertEquals("2nd element", "d", list2.get(1));
+ }
+
+ @Test
+ public void testDictionary()
+ {
+ assertEquals("1st element in dictionary", "bar1", config.getProperty("dictionary.foo1"));
+ assertEquals("2nd element in dictionary", "bar2", config.getProperty("dictionary.foo2"));
+ }
+
+ @Test
+ public void testDictionaryArray()
+ {
+ String key = "dictionary-array";
+
+ Object array = config.getProperty(key);
+
+ // root array
+ assertNotNull("array not found", array);
+ ObjectAssert.assertInstanceOf("the array element is not parsed as a List", List.class, array);
+ List<?> list = config.getList(key);
+
+ assertFalse("empty array", list.isEmpty());
+ assertEquals("size", 2, list.size());
+
+ // 1st dictionary
+ ObjectAssert.assertInstanceOf("the dict element is not parsed as a Configuration", Configuration.class, list.get(0));
+ Configuration conf1 = (Configuration) list.get(0);
+ assertFalse("configuration 1 is empty", conf1.isEmpty());
+ assertEquals("configuration element", "bar", conf1.getProperty("foo"));
+
+ // 2nd dictionary
+ ObjectAssert.assertInstanceOf("the dict element is not parsed as a Configuration", Configuration.class, list.get(1));
+ Configuration conf2 = (Configuration) list.get(1);
+ assertFalse("configuration 2 is empty", conf2.isEmpty());
+ assertEquals("configuration element", "value", conf2.getProperty("key"));
+ }
+
+ @Test
+ public void testNestedDictionaries()
+ {
+ assertEquals("nested property", "value", config.getString("nested-dictionaries.foo.bar.key"));
+ }
+
+ @Test
+ public void testData()
+ {
+ ObjectAssert.assertInstanceOf("data", (new byte[0]).getClass(), config.getProperty("data"));
+ ArrayAssert.assertEquals("data", "foo bar".getBytes(), (byte[]) config.getProperty("data"));
+ }
+
+ @Test
+ public void testDate() throws Exception
+ {
+ Calendar cal = Calendar.getInstance();
+ cal.clear();
+ cal.set(2002, 2, 22, 11, 30, 0);
+ cal.setTimeZone(TimeZone.getTimeZone("GMT+0100"));
+ Date date = cal.getTime();
+
+ assertEquals("date", date, config.getProperty("date"));
+ }
+
+ @Test
+ public void testSave() throws Exception
+ {
+ File savedFile = new File("target/testsave.plist");
+
+ // remove the file previously saved if necessary
+ if (savedFile.exists())
+ {
+ assertTrue(savedFile.delete());
+ }
+
+ // save the configuration
+ String filename = savedFile.getAbsolutePath();
+ config.save(filename);
+
+ assertTrue("The saved file doesn't exist", savedFile.exists());
+
+ // read the configuration and compare the properties
+ Configuration checkConfig = new PropertyListConfiguration(new File(filename));
+
+ Iterator<String> it = config.getKeys();
+ while (it.hasNext())
+ {
+ String key = it.next();
+ assertTrue("The saved configuration doesn't contain the key '" + key + "'", checkConfig.containsKey(key));
+
+ Object value = checkConfig.getProperty(key);
+ if (value instanceof byte[])
+ {
+ byte[] array = (byte[]) value;
+ ArrayAssert.assertEquals("Value of the '" + key + "' property", (byte[]) config.getProperty(key), array);
+ }
+ else if (value instanceof List)
+ {
+ List<?> list1 = (List<?>) config.getProperty(key);
+ List<?> list2 = (List<?>) value;
+
+ assertEquals("The size of the list for the key '" + key + "' doesn't match", list1.size(), list2.size());
+
+ for (int i = 0; i < list2.size(); i++)
+ {
+ Object value1 = list1.get(i);
+ Object value2 = list2.get(i);
+
+ if (value1 instanceof Configuration)
+ {
+ ConfigurationComparator comparator = new StrictConfigurationComparator();
+ assertTrue("The dictionnary at index " + i + " for the key '" + key + "' doesn't match", comparator.compare((Configuration) value1, (Configuration) value2));
+ }
+ else
+ {
+ assertEquals("Element at index " + i + " for the key '" + key + "'", value1, value2);
+ }
+ }
+
+ ListAssert.assertEquals("Value of the '" + key + "' property", (List<?>) config.getProperty(key), list1);
+ }
+ else
+ {
+ assertEquals("Value of the '" + key + "' property", config.getProperty(key), checkConfig.getProperty(key));
+ }
+
+ }
+ }
+
+ @Test
+ public void testSaveEmptyDictionary() throws Exception
+ {
+ File savedFile = new File("target/testsave.plist");
+
+ // remove the file previously saved if necessary
+ if (savedFile.exists())
+ {
+ assertTrue(savedFile.delete());
+ }
+
+ // save the configuration
+ String filename = savedFile.getAbsolutePath();
+ config.save(filename);
+
+ assertTrue("The saved file doesn't exist", savedFile.exists());
+
+ // read the configuration and compare the properties
+ PropertyListConfiguration checkConfig = new PropertyListConfiguration(new File(filename));
+
+ assertFalse(config.getRootNode().getChildren("empty-dictionary").isEmpty());
+ assertFalse(checkConfig.getRootNode().getChildren("empty-dictionary").isEmpty());
+ }
+
+ @Test
+ public void testQuoteString()
+ {
+ assertEquals("null string", null, config.quoteString(null));
+ assertEquals("simple string", "abcd", config.quoteString("abcd"));
+ assertEquals("string with a space", "\"ab cd\"", config.quoteString("ab cd"));
+ assertEquals("string with a quote", "\"foo\\\"bar\"", config.quoteString("foo\"bar"));
+ assertEquals("string with a special char", "\"foo;bar\"", config.quoteString("foo;bar"));
+ }
+
+ /**
+ * Ensure that setProperty doesn't alter an array of byte
+ * since it's a first class type in plist file
+ */
+ @Test
+ public void testSetDataProperty() throws Exception
+ {
+ byte[] expected = new byte[]{1, 2, 3, 4};
+ PropertyListConfiguration config = new PropertyListConfiguration();
+ config.setProperty("foo", expected);
+ config.save("target/testdata.plist");
+
+ PropertyListConfiguration config2 = new PropertyListConfiguration("target/testdata.plist");
+ Object array = config2.getProperty("foo");
+
+ assertNotNull("data not found", array);
+ assertEquals("property type", byte[].class, array.getClass());
+ ArrayAssert.assertEquals(expected, (byte[]) array);
+ }
+
+ /**
+ * Ensure that addProperty doesn't alter an array of byte
+ */
+ @Test
+ public void testAddDataProperty() throws Exception
+ {
+ byte[] expected = new byte[]{1, 2, 3, 4};
+ PropertyListConfiguration config = new PropertyListConfiguration();
+ config.addProperty("foo", expected);
+ config.save("target/testdata.plist");
+
+ PropertyListConfiguration config2 = new PropertyListConfiguration("target/testdata.plist");
+ Object array = config2.getProperty("foo");
+
+ assertNotNull("data not found", array);
+ assertEquals("property type", byte[].class, array.getClass());
+ ArrayAssert.assertEquals(expected, (byte[]) array);
+ }
+
+ @Test
+ public void testInitCopy()
+ {
+ PropertyListConfiguration copy = new PropertyListConfiguration(config);
+ assertFalse("Nothing was copied", copy.isEmpty());
+ }
+
+ /**
+ * Tests parsing a date with an invalid numeric value.
+ */
+ @Test(expected = ParseException.class)
+ public void testParseDateNoNumber() throws ParseException
+ {
+ PropertyListConfiguration
+ .parseDate("<*D2002-03-22 1c:30:00 +0100>");
+ }
+
+ /**
+ * Tests parsing a date that is not long enough.
+ */
+ @Test(expected = ParseException.class)
+ public void testParseDateTooShort() throws ParseException
+ {
+ PropertyListConfiguration.parseDate("<*D2002-03-22 11:3>");
+ }
+
+ /**
+ * Tests parsing a date that contains an invalid separator character.
+ */
+ @Test(expected = ParseException.class)
+ public void testParseDateInvalidChar() throws ParseException
+ {
+ PropertyListConfiguration
+ .parseDate("<*D2002+03-22 11:30:00 +0100>");
+ }
+
+ /**
+ * Tries parsing a null date. This should cause an exception.n
+ */
+ @Test(expected = ParseException.class)
+ public void testParseDateNull() throws ParseException
+ {
+ PropertyListConfiguration.parseDate(null);
+ }
+
+ /**
+ * Tests formatting a date.
+ */
+ @Test
+ public void testFormatDate()
+ {
+ Calendar cal = Calendar.getInstance();
+ cal.clear();
+ cal.set(2007, 9, 29, 23, 4, 30);
+ cal.setTimeZone(TimeZone.getTimeZone("GMT-0230"));
+ assertEquals("Wrong date literal (1)", "<*D2007-10-29 23:04:30 -0230>",
+ PropertyListConfiguration.formatDate(cal));
+ cal.clear();
+ cal.set(2007, 9, 30, 22, 2, 15);
+ cal.setTimeZone(TimeZone.getTimeZone("GMT+1111"));
+ assertEquals("Wrong date literal (2)", "<*D2007-10-30 22:02:15 +1111>",
+ PropertyListConfiguration.formatDate(cal));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/plist/TestPropertyListConfigurationEvents.java b/src/test/java/org/apache/commons/configuration/plist/TestPropertyListConfigurationEvents.java
new file mode 100644
index 0000000..427b9be
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/plist/TestPropertyListConfigurationEvents.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.plist;
+
+import java.io.File;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.ConfigurationRuntimeException;
+
+/**
+ * Test class for the events generated by PropertyListConfiguration.
+ *
+ * @version $Id: TestPropertyListConfigurationEvents.java 1301994 2012-03-17 20:21:23Z sebb $
+ */
+public class TestPropertyListConfigurationEvents extends
+ AbstractTestPListEvents
+{
+ /** Constant for the test file that will be loaded. */
+ private static final File TEST_FILE = new File("conf/test.plist");
+
+ @Override
+ protected AbstractConfiguration createConfiguration()
+ {
+ try
+ {
+ return new PropertyListConfiguration(TEST_FILE);
+ }
+ catch (ConfigurationException cex)
+ {
+ throw new ConfigurationRuntimeException(cex);
+ }
+ }
+
+}
diff --git a/src/test/java/org/apache/commons/configuration/plist/TestPropertyListParser.java b/src/test/java/org/apache/commons/configuration/plist/TestPropertyListParser.java
new file mode 100644
index 0000000..a90f85a
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/plist/TestPropertyListParser.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.plist;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.Reader;
+import java.util.Calendar;
+import java.util.SimpleTimeZone;
+
+import junitx.framework.ArrayAssert;
+
+import org.junit.Test;
+
+/**
+ * @author Emmanuel Bourg
+ * @version $Id: TestPropertyListParser.java 1225903 2011-12-30 19:49:15Z oheger $
+ */
+public class TestPropertyListParser
+{
+ private PropertyListParser parser = new PropertyListParser((Reader) null);
+
+ @Test
+ public void testRemoveQuotes()
+ {
+ assertEquals("unquoted string", "abc", parser.removeQuotes("abc"));
+ assertEquals("quoted string", "abc", parser.removeQuotes("\"abc\""));
+ assertEquals("empty quotes", "", parser.removeQuotes("\"\""));
+ assertEquals("empty string", "", parser.removeQuotes(""));
+ assertEquals("null string", null, parser.removeQuotes(null));
+ }
+
+ @Test
+ public void testUnescapeQuotes()
+ {
+ assertEquals("non escaped quotes", "aaa\"bbb\"ccc", parser.unescapeQuotes("aaa\"bbb\"ccc"));
+ assertEquals("escaped quotes", "aaa\"bbb\"ccc", parser.unescapeQuotes("aaa\\\"bbb\\\"ccc"));
+ }
+
+ @Test
+ public void testParseDate() throws Exception
+ {
+ Calendar calendar = Calendar.getInstance();
+ calendar.set(Calendar.YEAR, 2002);
+ calendar.set(Calendar.MONTH, Calendar.MARCH);
+ calendar.set(Calendar.DAY_OF_MONTH, 22);
+ calendar.set(Calendar.HOUR_OF_DAY, 11);
+ calendar.set(Calendar.MINUTE, 30);
+ calendar.set(Calendar.SECOND, 0);
+ calendar.set(Calendar.MILLISECOND, 0);
+ calendar.setTimeZone(new SimpleTimeZone(60 * 60 * 1000, "Apache/Jakarta"));
+
+ assertEquals("parsed date", calendar.getTime(), parser.parseDate("<*D2002-03-22 11:30:00 +0100>"));
+ }
+
+ @Test
+ public void testFilterData() throws Exception
+ {
+ byte[] expected = new byte[] {0x20, 0x20};
+ ArrayAssert.assertEquals("null string", null, parser.filterData(null));
+ ArrayAssert.assertEquals("data with < >", expected, parser.filterData("<2020>"));
+ ArrayAssert.assertEquals("data without < >", expected, parser.filterData("2020"));
+ ArrayAssert.assertEquals("data with space", expected, parser.filterData("20 20"));
+ ArrayAssert.assertEquals("odd length", new byte[]{9, 0x20}, parser.filterData("920"));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/plist/TestXMLPropertyListConfiguration.java b/src/test/java/org/apache/commons/configuration/plist/TestXMLPropertyListConfiguration.java
new file mode 100644
index 0000000..cd026c6
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/plist/TestXMLPropertyListConfiguration.java
@@ -0,0 +1,409 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.plist;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.util.Calendar;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TimeZone;
+
+import junitx.framework.ArrayAssert;
+import junitx.framework.ListAssert;
+import junitx.framework.ObjectAssert;
+
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.configuration.ConfigurationAssert;
+import org.apache.commons.configuration.ConfigurationComparator;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.FileConfiguration;
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.apache.commons.configuration.StrictConfigurationComparator;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Emmanuel Bourg
+ * @version $Id: TestXMLPropertyListConfiguration.java 1367253 2012-07-30 19:59:36Z oheger $
+ */
+public class TestXMLPropertyListConfiguration
+{
+ private FileConfiguration config;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ config = new XMLPropertyListConfiguration();
+ config.setFile(ConfigurationAssert.getTestFile("test.plist.xml"));
+ config.load();
+ }
+
+ @Test
+ public void testString() throws Exception
+ {
+ assertEquals("'string' property", "value1", config.getString("string"));
+ }
+
+ @Test
+ public void testInteger() throws Exception
+ {
+ assertEquals("'integer' property", 12345678900L, config.getLong("integer"));
+ }
+
+ @Test
+ public void testReal() throws Exception
+ {
+ assertEquals("'real' property", -12.345, config.getDouble("real"), 0);
+ }
+
+ @Test
+ public void testBoolean() throws Exception
+ {
+ assertEquals("'boolean1' property", true, config.getBoolean("boolean1"));
+ assertEquals("'boolean2' property", false, config.getBoolean("boolean2"));
+ }
+
+ @Test
+ public void testDictionary()
+ {
+ assertEquals("1st element", "value1", config.getProperty("dictionary.key1"));
+ assertEquals("2nd element", "value2", config.getProperty("dictionary.key2"));
+ assertEquals("3rd element", "value3", config.getProperty("dictionary.key3"));
+ }
+
+ @Test
+ public void testDate() throws Exception
+ {
+ Calendar calendar = Calendar.getInstance();
+ calendar.clear();
+ calendar.setTimeZone(TimeZone.getTimeZone("UTC"));
+ calendar.set(2005, Calendar.JANUARY, 1, 12, 0, 0);
+
+ assertEquals("'date' property", calendar.getTime(), config.getProperty("date"));
+
+ calendar.setTimeZone(TimeZone.getTimeZone("CET"));
+ calendar.set(2002, Calendar.MARCH, 22, 11, 30, 0);
+
+ assertEquals("'date-gnustep' property", calendar.getTime(), config.getProperty("date-gnustep"));
+ }
+
+ @Test
+ public void testSubset()
+ {
+ Configuration subset = config.subset("dictionary");
+ Iterator<String> keys = subset.getKeys();
+
+ String key = keys.next();
+ assertEquals("1st key", "key1", key);
+ assertEquals("1st value", "value1", subset.getString(key));
+
+ key = keys.next();
+ assertEquals("2nd key", "key2", key);
+ assertEquals("2nd value", "value2", subset.getString(key));
+
+ key = keys.next();
+ assertEquals("3rd key", "key3", key);
+ assertEquals("3rd value", "value3", subset.getString(key));
+
+ assertFalse("more than 3 properties founds", keys.hasNext());
+ }
+
+ @Test
+ public void testArray()
+ {
+ Object array = config.getProperty("array");
+
+ assertNotNull("array not found", array);
+ ObjectAssert.assertInstanceOf("the array element is not parsed as a List", List.class, array);
+ List<?> list = config.getList("array");
+
+ assertFalse("empty array", list.isEmpty());
+ assertEquals("size", 3, list.size());
+ assertEquals("1st element", "value1", list.get(0));
+ assertEquals("2nd element", "value2", list.get(1));
+ assertEquals("3rd element", "value3", list.get(2));
+ }
+
+ @Test
+ public void testNestedArray()
+ {
+ String key = "nested-array";
+
+ Object array = config.getProperty(key);
+
+ // root array
+ assertNotNull("array not found", array);
+ ObjectAssert.assertInstanceOf("the array element is not parsed as a List", List.class, array);
+ List<?> list = config.getList(key);
+
+ assertFalse("empty array", list.isEmpty());
+ assertEquals("size", 2, list.size());
+
+ // 1st array
+ ObjectAssert.assertInstanceOf("the array element is not parsed as a List", List.class, list.get(0));
+ List<?> list1 = (List<?>) list.get(0);
+ assertFalse("nested array 1 is empty", list1.isEmpty());
+ assertEquals("size", 2, list1.size());
+ assertEquals("1st element", "a", list1.get(0));
+ assertEquals("2nd element", "b", list1.get(1));
+
+ // 2nd array
+ ObjectAssert.assertInstanceOf("the array element is not parsed as a List", List.class, list.get(1));
+ List<?> list2 = (List<?>) list.get(1);
+ assertFalse("nested array 2 is empty", list2.isEmpty());
+ assertEquals("size", 2, list2.size());
+ assertEquals("1st element", "c", list2.get(0));
+ assertEquals("2nd element", "d", list2.get(1));
+ }
+
+ @Test
+ public void testDictionaryArray()
+ {
+ String key = "dictionary-array";
+
+ Object array = config.getProperty(key);
+
+ // root array
+ assertNotNull("array not found", array);
+ ObjectAssert.assertInstanceOf("the array element is not parsed as a List", List.class, array);
+ List<?> list = config.getList(key);
+
+ assertFalse("empty array", list.isEmpty());
+ assertEquals("size", 2, list.size());
+
+ // 1st dictionary
+ ObjectAssert.assertInstanceOf("the dict element is not parsed as a Configuration", Configuration.class, list.get(0));
+ Configuration conf1 = (Configuration) list.get(0);
+ assertFalse("configuration 1 is empty", conf1.isEmpty());
+ assertEquals("configuration element", "bar", conf1.getProperty("foo"));
+
+ // 2nd dictionary
+ ObjectAssert.assertInstanceOf("the dict element is not parsed as a Configuration", Configuration.class, list.get(1));
+ Configuration conf2 = (Configuration) list.get(1);
+ assertFalse("configuration 2 is empty", conf2.isEmpty());
+ assertEquals("configuration element", "value", conf2.getProperty("key"));
+ }
+
+ @Test
+ public void testNested()
+ {
+ assertEquals("nested property", "value", config.getString("nested.node1.node2.node3"));
+ }
+
+ @Test
+ public void testSave() throws Exception
+ {
+ File savedFile = new File("target/testsave.plist.xml");
+
+ // remove the file previously saved if necessary
+ if (savedFile.exists())
+ {
+ assertTrue(savedFile.delete());
+ }
+
+ // add an array of strings to the configuration
+ /*
+ config.addProperty("string", "value1");
+ List list = new ArrayList();
+ for (int i = 1; i < 5; i++)
+ {
+ list.add("value" + i);
+ }
+ config.addProperty("newarray", list);*/
+ // todo : investigate why the array structure of 'newarray' is lost in the saved file
+
+ // add a map of strings
+ /*
+ Map map = new HashMap();
+ map.put("foo", "bar");
+ map.put("int", new Integer(123));
+ config.addProperty("newmap", map);
+ */
+ // todo : a Map added to a HierarchicalConfiguration should be decomposed as list of nodes
+
+ // save the configuration
+ String filename = savedFile.getAbsolutePath();
+ config.save(filename);
+
+ assertTrue("The saved file doesn't exist", savedFile.exists());
+
+ // read the configuration and compare the properties
+ Configuration checkConfig = new XMLPropertyListConfiguration(new File(filename));
+
+ Iterator<String> it = config.getKeys();
+ while (it.hasNext())
+ {
+ String key = it.next();
+ assertTrue("The saved configuration doesn't contain the key '" + key + "'", checkConfig.containsKey(key));
+
+ Object value = checkConfig.getProperty(key);
+ if (value instanceof byte[])
+ {
+ byte[] array = (byte[]) value;
+ ArrayAssert.assertEquals("Value of the '" + key + "' property", (byte[]) config.getProperty(key), array);
+ }
+ else if (value instanceof List)
+ {
+ List<?> list1 = (List<?>) config.getProperty(key);
+ List<?> list2 = (List<?>) value;
+
+ assertEquals("The size of the list for the key '" + key + "' doesn't match", list1.size(), list2.size());
+
+ for (int i = 0; i < list2.size(); i++)
+ {
+ Object value1 = list1.get(i);
+ Object value2 = list2.get(i);
+
+ if (value1 instanceof Configuration)
+ {
+ ConfigurationComparator comparator = new StrictConfigurationComparator();
+ assertTrue("The dictionnary at index " + i + " for the key '" + key + "' doesn't match", comparator.compare((Configuration) value1, (Configuration) value2));
+ }
+ else
+ {
+ assertEquals("Element at index " + i + " for the key '" + key + "'", value1, value2);
+ }
+ }
+
+ ListAssert.assertEquals("Value of the '" + key + "' property", (List<?>) config.getProperty(key), list1);
+ }
+ else
+ {
+ assertEquals("Value of the '" + key + "' property", config.getProperty(key), checkConfig.getProperty(key));
+ }
+
+ }
+ }
+
+ @Test
+ public void testSaveEmptyDictionary() throws Exception
+ {
+ File savedFile = new File("target/testsave.plist.xml");
+
+ // remove the file previously saved if necessary
+ if (savedFile.exists())
+ {
+ assertTrue(savedFile.delete());
+ }
+
+ // save the configuration
+ String filename = savedFile.getAbsolutePath();
+ config.save(filename);
+
+ assertTrue("The saved file doesn't exist", savedFile.exists());
+
+ // read the configuration and compare the properties
+ Configuration checkConfig = new XMLPropertyListConfiguration(new File(filename));
+
+ assertEquals(null, config.getProperty("empty-dictionary"));
+ assertEquals(null, checkConfig.getProperty("empty-dictionary"));
+ }
+
+ /**
+ * Ensure that setProperty doesn't alter an array of byte
+ * since it's a first class type in plist file
+ */
+ @Test
+ public void testSetDataProperty() throws Exception
+ {
+ byte[] expected = new byte[]{1, 2, 3, 4};
+ XMLPropertyListConfiguration config = new XMLPropertyListConfiguration();
+ config.setProperty("foo", expected);
+ config.save("target/testdata.plist.xml");
+
+ XMLPropertyListConfiguration config2 = new XMLPropertyListConfiguration("target/testdata.plist.xml");
+ Object array = config2.getProperty("foo");
+
+ assertNotNull("data not found", array);
+ assertEquals("property type", byte[].class, array.getClass());
+ ArrayAssert.assertEquals(expected, (byte[]) array);
+ }
+
+ /**
+ * Ensure that addProperty doesn't alter an array of byte
+ */
+ @Test
+ public void testAddDataProperty() throws Exception
+ {
+ byte[] expected = new byte[]{1, 2, 3, 4};
+ XMLPropertyListConfiguration config = new XMLPropertyListConfiguration();
+ config.addProperty("foo", expected);
+ config.save("target/testdata.plist.xml");
+
+ XMLPropertyListConfiguration config2 = new XMLPropertyListConfiguration("target/testdata.plist.xml");
+ Object array = config2.getProperty("foo");
+
+ assertNotNull("data not found", array);
+ assertEquals("property type", byte[].class, array.getClass());
+ ArrayAssert.assertEquals(expected, (byte[]) array);
+ }
+
+ @Test
+ public void testInitCopy()
+ {
+ XMLPropertyListConfiguration copy = new XMLPropertyListConfiguration((HierarchicalConfiguration) config);
+ StrictConfigurationComparator comp = new StrictConfigurationComparator();
+ assertTrue("Configurations are not equal", comp.compare(config, copy));
+ }
+
+ /**
+ * Tests whether a configuration can be loaded that does not start with a
+ * {@code dict} element. This test case is related to
+ * CONFIGURATION-405.
+ */
+ @Test
+ public void testLoadNoDict() throws ConfigurationException
+ {
+ XMLPropertyListConfiguration plist = new XMLPropertyListConfiguration();
+ plist.setFile(ConfigurationAssert.getTestFile("test2.plist.xml"));
+ plist.load();
+ assertFalse("Configuration is empty", plist.isEmpty());
+ }
+
+ /**
+ * Tests whether a configuration that does not start with a
+ * {@code dict} element can be loaded from a constructor. This test
+ * case is related to CONFIGURATION-405.
+ */
+ @Test
+ public void testLoadNoDictConstr() throws ConfigurationException
+ {
+ XMLPropertyListConfiguration plist = new XMLPropertyListConfiguration(
+ ConfigurationAssert.getTestFile("test2.plist.xml"));
+ assertFalse("Configuration is empty", plist.isEmpty());
+ }
+
+ /**
+ * Tests a configuration file which contains an invalid date property value.
+ * This test is related to CONFIGURATION-501.
+ */
+ @Test
+ public void testSetDatePropertyInvalid() throws ConfigurationException
+ {
+ config.clear();
+ config.setFile(ConfigurationAssert.getTestFile("test_invalid_date.plist.xml"));
+ config.load();
+ assertEquals("'string' property", "value1", config.getString("string"));
+ assertFalse("Date property was loaded", config.containsKey("date"));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/plist/TestXMLPropertyListConfigurationEvents.java b/src/test/java/org/apache/commons/configuration/plist/TestXMLPropertyListConfigurationEvents.java
new file mode 100644
index 0000000..c569a91
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/plist/TestXMLPropertyListConfigurationEvents.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.plist;
+
+import java.io.File;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.ConfigurationRuntimeException;
+
+/**
+ * Test class for the events generated by XMLPropertyListConfiguration.
+ *
+ * @version $Id: TestXMLPropertyListConfigurationEvents.java 1301994 2012-03-17 20:21:23Z sebb $
+ */
+public class TestXMLPropertyListConfigurationEvents extends
+ AbstractTestPListEvents
+{
+ /** Constant for the test file that will be loaded. */
+ private static final File TEST_FILE = new File("conf/test.plist.xml");
+
+ @Override
+ protected AbstractConfiguration createConfiguration()
+ {
+ try
+ {
+ return new XMLPropertyListConfiguration(TEST_FILE);
+ }
+ catch (ConfigurationException cex)
+ {
+ throw new ConfigurationRuntimeException(cex);
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/reloading/FileAlwaysReloadingStrategy.java b/src/test/java/org/apache/commons/configuration/reloading/FileAlwaysReloadingStrategy.java
new file mode 100644
index 0000000..703a95d
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/reloading/FileAlwaysReloadingStrategy.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.reloading;
+
+import java.io.File;
+
+/**
+ * A specialized reloading strategy for files that will always report a change
+ * of the monitored file. Thus it is well suited for testing reloading
+ * operations on file-based configurations.
+ *
+ * @version $Id: FileAlwaysReloadingStrategy.java 1301995 2012-03-17 20:24:16Z sebb $
+ */
+public class FileAlwaysReloadingStrategy extends FileChangedReloadingStrategy
+{
+ /**
+ * Checks whether a reload is necessary. This implementation returns always
+ * <b>true</b>.
+ *
+ * @return a flag whether a reload is required
+ */
+ @Override
+ public boolean reloadingRequired()
+ {
+ return true;
+ }
+
+ /**
+ * Returns the file that is watched by this strategy.
+ *
+ * @return the monitored file
+ */
+ public File getMonitoredFile()
+ {
+ return getFile();
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/reloading/FileRandomReloadingStrategy.java b/src/test/java/org/apache/commons/configuration/reloading/FileRandomReloadingStrategy.java
new file mode 100644
index 0000000..0694df4
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/reloading/FileRandomReloadingStrategy.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.reloading;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.io.File;
+import java.util.Random;
+
+/**
+ * A ReloadingStrategy that randomly returns true or false;
+ */
+public class FileRandomReloadingStrategy extends FileChangedReloadingStrategy
+{
+ Random random = new Random();
+
+ /** The Log to use for diagnostic messages */
+ private Log logger = LogFactory.getLog(FileRandomReloadingStrategy.class);
+
+ /**
+ * Checks whether a reload is necessary.
+ *
+ * @return a flag whether a reload is required
+ */
+ @Override
+ public boolean reloadingRequired()
+ {
+ boolean result = random.nextBoolean();
+ if (result)
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("File change detected: " + getName());
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns the file that is watched by this strategy.
+ *
+ * @return the monitored file
+ */
+ public File getMonitoredFile()
+ {
+ return getFile();
+ }
+
+ private String getName()
+ {
+ return getName(getFile());
+ }
+
+ private String getName(File file)
+ {
+ String name = configuration.getURL().toString();
+ if (name == null)
+ {
+ if (file != null)
+ {
+ name = file.getAbsolutePath();
+ }
+ else
+ {
+ name = "base: " + configuration.getBasePath()
+ + "file: " + configuration.getFileName();
+ }
+ }
+ return name;
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/reloading/TestFileChangedReloadingStrategy.java b/src/test/java/org/apache/commons/configuration/reloading/TestFileChangedReloadingStrategy.java
new file mode 100644
index 0000000..f765e5b
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/reloading/TestFileChangedReloadingStrategy.java
@@ -0,0 +1,228 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.reloading;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileWriter;
+import java.net.URL;
+
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.PropertiesConfiguration;
+import org.apache.commons.configuration.XMLConfiguration;
+import org.apache.log4j.Appender;
+import org.apache.log4j.Layout;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.PatternLayout;
+import org.apache.log4j.WriterAppender;
+import org.junit.Test;
+
+/**
+ * Test case for the ReloadableConfiguration class.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: TestFileChangedReloadingStrategy.java 1225906 2011-12-30 20:01:37Z oheger $
+ */
+public class TestFileChangedReloadingStrategy
+{
+ /** Constant for the name of a test properties file.*/
+ private static final String TEST_FILE = "test.properties";
+
+ @Test
+ public void testAutomaticReloading() throws Exception
+ {
+ // create a new configuration
+ File file = new File("target/testReload.properties");
+
+ if (file.exists())
+ {
+ file.delete();
+ }
+
+ // create the configuration file
+ FileWriter out = new FileWriter(file);
+ out.write("string=value1");
+ out.flush();
+ out.close();
+
+ // load the configuration
+ PropertiesConfiguration config = new PropertiesConfiguration("target/testReload.properties");
+ FileChangedReloadingStrategy strategy = new FileChangedReloadingStrategy();
+ strategy.setRefreshDelay(500);
+ config.setReloadingStrategy(strategy);
+ assertEquals("Initial value", "value1", config.getString("string"));
+
+ Thread.sleep(2000);
+
+ // change the file
+ out = new FileWriter(file);
+ out.write("string=value2");
+ out.flush();
+ out.close();
+
+ // test the automatic reloading
+ assertEquals("Modified value with enabled reloading", "value2", config.getString("string"));
+ }
+
+ @Test
+ public void testNewFileReloading() throws Exception
+ {
+ // create a new configuration
+ File file = new File("target/testReload.properties");
+
+ if (file.exists())
+ {
+ file.delete();
+ }
+
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ config.setFile(file);
+ FileChangedReloadingStrategy strategy = new FileChangedReloadingStrategy();
+ strategy.setRefreshDelay(500);
+ config.setReloadingStrategy(strategy);
+
+ assertNull("Initial value", config.getString("string"));
+
+ // change the file
+ FileWriter out = new FileWriter(file);
+ out.write("string=value1");
+ out.flush();
+ out.close();
+
+ Thread.sleep(2000);
+
+ // test the automatic reloading
+ assertEquals("Modified value with enabled reloading", "value1", config.getString("string"));
+ }
+
+ @Test
+ public void testGetRefreshDelay()
+ {
+ FileChangedReloadingStrategy strategy = new FileChangedReloadingStrategy();
+ strategy.setRefreshDelay(500);
+ assertEquals("refresh delay", 500, strategy.getRefreshDelay());
+ }
+
+ /**
+ * Tests if a file from the classpath can be monitored.
+ */
+ @Test
+ public void testFromClassPath() throws Exception
+ {
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ config.setFileName(TEST_FILE);
+ config.load();
+ assertTrue(config.getBoolean("configuration.loaded"));
+ FileChangedReloadingStrategy strategy = new FileChangedReloadingStrategy();
+ config.setReloadingStrategy(strategy);
+ assertEquals(config.getURL().toExternalForm(), strategy.getFile().toURI().toURL().toExternalForm());
+ }
+
+ /**
+ * Tests to watch a configuration file in a jar. In this case the jar file
+ * itself should be monitored.
+ */
+ @Test
+ public void testFromJar() throws Exception
+ {
+ XMLConfiguration config = new XMLConfiguration();
+ // use some jar: URL
+ config.setURL(new URL("jar:" + new File("conf/resources.jar").getAbsoluteFile().toURI().toURL() + "!/test-jar.xml"));
+ FileChangedReloadingStrategy strategy = new FileChangedReloadingStrategy();
+ config.setReloadingStrategy(strategy);
+ File file = strategy.getFile();
+ assertNotNull("Strategy's file is null", file);
+ assertEquals("Strategy does not monitor the jar file", "resources.jar", file.getName());
+ }
+
+ /**
+ * Tests calling reloadingRequired() multiple times before a reload actually
+ * happens. This test is related to CONFIGURATION-302.
+ */
+ @Test
+ public void testReloadingRequiredMultipleTimes()
+ throws ConfigurationException
+ {
+ FileChangedReloadingStrategy strategy = new FileChangedReloadingStrategy()
+ {
+ @Override
+ protected boolean hasChanged()
+ {
+ // signal always a change
+ return true;
+ }
+ };
+ strategy.setRefreshDelay(100000);
+ PropertiesConfiguration config = new PropertiesConfiguration(TEST_FILE);
+ config.setReloadingStrategy(strategy);
+ assertTrue("Reloading not required", strategy.reloadingRequired());
+ assertTrue("Reloading no more required", strategy.reloadingRequired());
+ strategy.reloadingPerformed();
+ assertFalse("Reloading still required", strategy.reloadingRequired());
+ }
+
+ @Test
+ public void testFileDeletion() throws Exception
+ {
+ Logger logger = Logger.getLogger(FileChangedReloadingStrategy.class.getName());
+ Layout layout = new PatternLayout("%p - %m%n");
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ Appender appender = new WriterAppender(layout, os);
+ logger.addAppender(appender);
+ logger.setLevel(Level.WARN);
+ logger.setAdditivity(false);
+ // create a new configuration
+ File file = new File("target/testReload.properties");
+
+ if (file.exists())
+ {
+ file.delete();
+ }
+
+ // create the configuration file
+ FileWriter out = new FileWriter(file);
+ out.write("string=value1");
+ out.flush();
+ out.close();
+
+ // load the configuration
+ PropertiesConfiguration config = new PropertiesConfiguration("target/testReload.properties");
+ FileChangedReloadingStrategy strategy = new FileChangedReloadingStrategy();
+ strategy.setRefreshDelay(500);
+ config.setReloadingStrategy(strategy);
+ assertEquals("Initial value", "value1", config.getString("string"));
+
+ Thread.sleep(2000);
+
+ // Delete the file.
+ file.delete();
+ //Old value should still be returned.
+ assertEquals("Initial value", "value1", config.getString("string"));
+ logger.removeAppender(appender);
+ String str = os.toString();
+ //System.out.println(str);
+ assertTrue("No error was logged", str != null && str.length() > 0);
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/reloading/TestManagedReloadingStrategy.java b/src/test/java/org/apache/commons/configuration/reloading/TestManagedReloadingStrategy.java
new file mode 100644
index 0000000..3411c3b
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/reloading/TestManagedReloadingStrategy.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.reloading;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+import java.io.FileWriter;
+
+import org.apache.commons.configuration.PropertiesConfiguration;
+import org.junit.Test;
+
+/**
+ * Test case for the ManagedReloadingStrategy class.
+ *
+ * @author Nicolas De loof
+ * @version $Id: TestManagedReloadingStrategy.java 1225908 2011-12-30 20:04:48Z oheger $
+ */
+public class TestManagedReloadingStrategy
+{
+ @Test
+ public void testManagedRefresh() throws Exception
+ {
+ File file = new File("target/testReload.properties");
+ if (file.exists())
+ {
+ file.delete();
+ }
+ // create the configuration file
+ FileWriter out = new FileWriter(file);
+ out.write("string=value1");
+ out.flush();
+ out.close();
+
+ // load the configuration
+ PropertiesConfiguration config = new PropertiesConfiguration("target/testReload.properties");
+ ManagedReloadingStrategy strategy = new ManagedReloadingStrategy();
+ config.setReloadingStrategy(strategy);
+ assertEquals("Initial value", "value1", config.getString("string"));
+
+ // change the file
+ out = new FileWriter(file);
+ out.write("string=value2");
+ out.flush();
+ out.close();
+
+ // test the automatic reloading
+ assertEquals("No automatic reloading", "value1", config.getString("string"));
+ strategy.refresh();
+ assertEquals("Modified value with enabled reloading", "value2", config.getString("string"));
+ }
+
+}
diff --git a/src/test/java/org/apache/commons/configuration/reloading/TestVFSFileChangedReloadingStrategy.java b/src/test/java/org/apache/commons/configuration/reloading/TestVFSFileChangedReloadingStrategy.java
new file mode 100644
index 0000000..df227b7
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/reloading/TestVFSFileChangedReloadingStrategy.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.reloading;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.FileWriter;
+
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.FileSystem;
+import org.apache.commons.configuration.PropertiesConfiguration;
+import org.apache.commons.configuration.VFSFileSystem;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test case for the VFSFileMonitorReloadingStrategy class.
+ *
+ * @author Ralph Goers
+ * @version $Id: TestVFSFileChangedReloadingStrategy.java 1225909 2011-12-30 20:09:00Z oheger $
+ */
+public class TestVFSFileChangedReloadingStrategy
+{
+ /** Constant for the name of a test properties file.*/
+ private static final String TEST_FILE = "test.properties";
+
+ @Before
+ public void setUp() throws Exception
+ {
+ FileSystem.setDefaultFileSystem(new VFSFileSystem());
+ }
+
+ @After
+ public void tearDown() throws Exception
+ {
+ FileSystem.resetDefaultFileSystem();
+ }
+
+ @Test
+ public void testAutomaticReloading() throws Exception
+ {
+ // create a new configuration
+ File file = new File("target/testReload.properties");
+
+ if (file.exists())
+ {
+ file.delete();
+ }
+
+ // create the configuration file
+ FileWriter out = new FileWriter(file);
+ out.write("string=value1");
+ out.flush();
+ out.close();
+
+ // load the configuration
+ PropertiesConfiguration config = new PropertiesConfiguration("target/testReload.properties");
+ VFSFileChangedReloadingStrategy strategy = new VFSFileChangedReloadingStrategy();
+ strategy.setRefreshDelay(500);
+ config.setReloadingStrategy(strategy);
+ assertEquals("Initial value", "value1", config.getString("string"));
+
+ Thread.sleep(2000);
+
+ // change the file
+ out = new FileWriter(file);
+ out.write("string=value2");
+ out.flush();
+ out.close();
+
+ // test the automatic reloading
+ assertEquals("Modified value with enabled reloading", "value2", config.getString("string"));
+ }
+
+ @Test
+ public void testNewFileReloading() throws Exception
+ {
+ // create a new configuration
+ File file = new File("target/testReload.properties");
+
+ if (file.exists())
+ {
+ file.delete();
+ }
+
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ config.setFile(file);
+ VFSFileChangedReloadingStrategy strategy = new VFSFileChangedReloadingStrategy();
+ strategy.setRefreshDelay(500);
+ config.setReloadingStrategy(strategy);
+
+ assertNull("Initial value", config.getString("string"));
+
+ // change the file
+ FileWriter out = new FileWriter(file);
+ out.write("string=value1");
+ out.flush();
+ out.close();
+
+ Thread.sleep(2000);
+
+ // test the automatic reloading
+ assertEquals("Modified value with enabled reloading", "value1", config.getString("string"));
+ }
+
+ @Test
+ public void testGetRefreshDelay() throws Exception
+ {
+ VFSFileChangedReloadingStrategy strategy = new VFSFileChangedReloadingStrategy();
+ strategy.setRefreshDelay(500);
+ assertEquals("refresh delay", 500, strategy.getRefreshDelay());
+ }
+
+ /**
+ * Tests calling reloadingRequired() multiple times before a reload actually
+ * happens. This test is related to CONFIGURATION-302.
+ */
+ @Test
+ public void testReloadingRequiredMultipleTimes()
+ throws ConfigurationException
+ {
+ VFSFileChangedReloadingStrategy strategy = new VFSFileChangedReloadingStrategy()
+ {
+ @Override
+ protected boolean hasChanged()
+ {
+ // signal always a change
+ return true;
+ }
+ };
+ strategy.setRefreshDelay(100000);
+ PropertiesConfiguration config = new PropertiesConfiguration(TEST_FILE);
+ config.setReloadingStrategy(strategy);
+ assertTrue("Reloading not required", strategy.reloadingRequired());
+ assertTrue("Reloading no more required", strategy.reloadingRequired());
+ strategy.reloadingPerformed();
+ assertFalse("Reloading still required", strategy.reloadingRequired());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/commons/configuration/test/HsqlDB.java b/src/test/java/org/apache/commons/configuration/test/HsqlDB.java
new file mode 100644
index 0000000..9e37ab3
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/test/HsqlDB.java
@@ -0,0 +1,123 @@
+package org.apache.commons.configuration.test;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.FileReader;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Stolen from Turbine
+ *
+ * @author <a href="mailto:hps at intermeta.de">Henning P. Schmiedehausen</a>
+ * @version $Id: HsqlDB.java 439648 2006-09-02 20:42:10Z oheger $
+ */
+
+public class HsqlDB
+{
+ private Connection connection = null;
+ private static Log log = LogFactory.getLog(HsqlDB.class);
+
+ public HsqlDB(String uri, String databaseDriver, String loadFile)
+ throws Exception
+ {
+ Class.forName(databaseDriver);
+
+ this.connection = DriverManager.getConnection(uri, "sa", "");
+
+ if (StringUtils.isNotEmpty(loadFile))
+ {
+ loadSqlFile(loadFile);
+ }
+ this.connection.commit();
+ }
+
+ public Connection getConnection()
+ {
+ return connection;
+ }
+
+ public void close()
+ {
+ try
+ {
+ connection.close();
+ }
+ catch (Exception e)
+ {
+ }
+ }
+
+ private void loadSqlFile(String fileName)
+ throws Exception
+ {
+ Statement statement = null;
+ try
+ {
+ statement = connection.createStatement();
+ String commands = getFileContents(fileName);
+
+ for (int targetPos = commands.indexOf(';'); targetPos > -1; targetPos = commands.indexOf(';'))
+ {
+ String cmd = commands.substring(0, targetPos + 1);
+ try
+ {
+ statement.execute(cmd);
+ }
+ catch (SQLException sqle)
+ {
+ log.warn("Statement: " + cmd + ": " + sqle.getMessage());
+ }
+
+ commands = commands.substring(targetPos + 2);
+ }
+ }
+ finally
+ {
+ if (statement != null)
+ {
+ statement.close();
+ }
+ }
+ }
+
+ private String getFileContents(String fileName)
+ throws Exception
+ {
+ FileReader fr = new FileReader(fileName);
+
+ char fileBuf[] = new char[1024];
+ StringBuffer sb = new StringBuffer(1000);
+ int res = -1;
+
+ while ((res = fr.read(fileBuf, 0, 1024)) > -1)
+ {
+ sb.append(fileBuf, 0, res);
+ }
+ fr.close();
+ return sb.toString();
+ }
+}
+
diff --git a/src/test/java/org/apache/commons/configuration/tree/AbstractCombinerTest.java b/src/test/java/org/apache/commons/configuration/tree/AbstractCombinerTest.java
new file mode 100644
index 0000000..c5291e4
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/AbstractCombinerTest.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+
+import org.apache.commons.configuration.ConfigurationAssert;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.apache.commons.configuration.XMLConfiguration;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * A base class for testing combiner implementations. This base class provides
+ * some functionality for loading the test configurations, which are to be
+ * combined. Concrete sub classes only need to create the correct combiner
+ * object.
+ *
+ * @version $Id: AbstractCombinerTest.java 1225911 2011-12-30 20:19:10Z oheger $
+ */
+public abstract class AbstractCombinerTest
+{
+ /** Constant for the first test configuration. */
+ static File CONF1 = ConfigurationAssert.getTestFile("testcombine1.xml");
+
+ /** Constant for the second test configuration. */
+ static File CONF2 = ConfigurationAssert.getTestFile("testcombine2.xml");
+
+ /** The combiner to be tested. */
+ protected NodeCombiner combiner;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ combiner = createCombiner();
+ }
+
+ /**
+ * Creates the combiner to be tested. This method is called by
+ * <code>setUp()</code>. It must be implemented in concrete sub classes.
+ *
+ * @return the combiner to be tested
+ */
+ protected abstract NodeCombiner createCombiner();
+
+ /**
+ * Constructs a union configuration based on the source configurations.
+ *
+ * @return the union configuration
+ * @throws ConfigurationException if an error occurs
+ */
+ protected HierarchicalConfiguration createCombinedConfiguration()
+ throws ConfigurationException
+ {
+ XMLConfiguration conf1 = new XMLConfiguration(CONF1);
+ XMLConfiguration conf2 = new XMLConfiguration(CONF2);
+ ConfigurationNode cn = combiner.combine(conf1.getRootNode(), conf2
+ .getRootNode());
+
+ HierarchicalConfiguration result = new HierarchicalConfiguration();
+ result.setRootNode(cn);
+
+ return result;
+ }
+
+ /**
+ * Tests a newly created combiner.
+ */
+ @Test
+ public void testInit()
+ {
+ assertTrue("Combiner has list nodes", combiner.getListNodes().isEmpty());
+ assertFalse("Node is list node", combiner
+ .isListNode(new DefaultConfigurationNode("test")));
+ }
+
+}
diff --git a/src/test/java/org/apache/commons/configuration/tree/TestDefaultConfigurationKey.java b/src/test/java/org/apache/commons/configuration/tree/TestDefaultConfigurationKey.java
new file mode 100644
index 0000000..85dcdaf
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/TestDefaultConfigurationKey.java
@@ -0,0 +1,516 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.NoSuchElementException;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for DefaultConfigurationKey.
+ *
+ * @author Oliver Heger
+ * @version $Id: TestDefaultConfigurationKey.java 1225914 2011-12-30 20:26:36Z oheger $
+ */
+public class TestDefaultConfigurationKey
+{
+ /** Constant for a test key. */
+ private static final String TESTPROPS = "tables.table(0).fields.field(1)";
+
+ /** Constant for a test attribute key. */
+ private static final String TESTATTR = "[@dataType]";
+
+ /** Constant for a complex attribute key. */
+ private static final String TESTKEY = TESTPROPS + TESTATTR;
+
+ /** Stores the expression engine of the key to test. */
+ DefaultExpressionEngine expressionEngine;
+
+ /** Stores the object to be tested. */
+ DefaultConfigurationKey key;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ expressionEngine = new DefaultExpressionEngine();
+ key = new DefaultConfigurationKey(expressionEngine);
+ }
+
+ /**
+ * Tests setting the expression engine to null. This should not be allowed.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetNullExpressionEngine()
+ {
+ key.setExpressionEngine(null);
+ }
+
+ /**
+ * Tests the isAttributeKey() method with several keys.
+ */
+ @Test
+ public void testIsAttributeKey()
+ {
+ assertTrue("Attribute key not detected", key.isAttributeKey(TESTATTR));
+ assertFalse("Property key considered as attribute", key
+ .isAttributeKey(TESTPROPS));
+ assertFalse("Null key considered as attribute", key
+ .isAttributeKey(null));
+ }
+
+ /**
+ * Tests if attribute keys are correctly detected if no end markers are set.
+ * (In this test case we use the same delimiter for attributes as for simple
+ * properties.)
+ */
+ @Test
+ public void testIsAttributeKeyWithoutEndMarkers()
+ {
+ expressionEngine.setAttributeEnd(null);
+ expressionEngine
+ .setAttributeStart(DefaultExpressionEngine.DEFAULT_PROPERTY_DELIMITER);
+ assertTrue(
+ "Attribute key not detected",
+ key
+ .isAttributeKey(DefaultExpressionEngine.DEFAULT_PROPERTY_DELIMITER
+ + "test"));
+ assertFalse("Property key considered as attribute key", key
+ .isAttributeKey(TESTATTR));
+ }
+
+ /**
+ * Tests removing leading delimiters.
+ */
+ @Test
+ public void testTrimLeft()
+ {
+ assertEquals("Key was not left trimmed", "test.", key
+ .trimLeft(".test."));
+ assertEquals("Too much left trimming", "..test.", key
+ .trimLeft("..test."));
+ }
+
+ /**
+ * Tests removing trailing delimiters.
+ */
+ @Test
+ public void testTrimRight()
+ {
+ assertEquals("Key was not right trimmed", ".test", key
+ .trimRight(".test."));
+ assertEquals("Too much right trimming", ".test..", key
+ .trimRight(".test.."));
+ }
+
+ /**
+ * Tests removing delimiters.
+ */
+ @Test
+ public void testTrim()
+ {
+ assertEquals("Key was not trimmed", "test", key.trim(".test."));
+ assertEquals("Null key could not be processed", "", key.trim(null));
+ assertEquals("Delimiter could not be processed", "", key
+ .trim(DefaultExpressionEngine.DEFAULT_PROPERTY_DELIMITER));
+ }
+
+ /**
+ * Tests appending keys.
+ */
+ @Test
+ public void testAppend()
+ {
+ key.append("tables").append("table(0).");
+ key.append("fields.").append("field(1)");
+ key.append(null).append(TESTATTR);
+ assertEquals("Wrong key", TESTKEY, key.toString());
+ }
+
+ /**
+ * Tests appending keys that contain delimiters.
+ */
+ @Test
+ public void testAppendDelimiters()
+ {
+ key.append("key..").append("test").append(".");
+ key.append(".more").append("..tests");
+ assertEquals("Wrong key", "key...test.more...tests", key.toString());
+ }
+
+ /**
+ * Tests appending keys that contain delimiters when no escpaped delimiter
+ * is defined.
+ */
+ @Test
+ public void testAppendDelimitersWithoutEscaping()
+ {
+ expressionEngine.setEscapedDelimiter(null);
+ key.append("key.......").append("test").append(".");
+ key.append(".more").append("..tests");
+ assertEquals("Wrong constructed key", "key.test.more.tests", key
+ .toString());
+ }
+
+ /**
+ * Tests calling append with the escape flag.
+ */
+ @Test
+ public void testAppendWithEscapeFlag()
+ {
+ key.append(".key.test.", true);
+ key.append(".more").append(".tests", true);
+ assertEquals("Wrong constructed key", "..key..test...more...tests", key
+ .toString());
+ }
+
+ /**
+ * Tests constructing keys for attributes.
+ */
+ @Test
+ public void testConstructAttributeKey()
+ {
+ assertEquals("Wrong attribute key", TESTATTR, key
+ .constructAttributeKey("dataType"));
+ assertEquals("Attribute key was incorrectly converted", TESTATTR, key
+ .constructAttributeKey(TESTATTR));
+ assertEquals("Null key could not be processed", "", key
+ .constructAttributeKey(null));
+ }
+
+ /**
+ * Tests constructing attribute keys when no end markers are defined. In
+ * this test case we use the property delimiter as attribute prefix.
+ */
+ @Test
+ public void testConstructAttributeKeyWithoutEndMarkers()
+ {
+ expressionEngine.setAttributeEnd(null);
+ expressionEngine.setAttributeStart(expressionEngine
+ .getPropertyDelimiter());
+ assertEquals("Wrong attribute key", ".test", key
+ .constructAttributeKey("test"));
+ assertEquals("Attribute key was incorrectly converted", ".test", key
+ .constructAttributeKey(".test"));
+ }
+
+ /**
+ * Tests appending attribute keys.
+ */
+ @Test
+ public void testAppendAttribute()
+ {
+ key.appendAttribute("dataType");
+ assertEquals("Attribute key not correctly appended", TESTATTR, key
+ .toString());
+ }
+
+ /**
+ * Tests appending an attribute key that is already decorated-
+ */
+ @Test
+ public void testAppendDecoratedAttributeKey()
+ {
+ key.appendAttribute(TESTATTR);
+ assertEquals("Decorated attribute key not correctly appended",
+ TESTATTR, key.toString());
+ }
+
+ /**
+ * Tests appending a null attribute key.
+ */
+ @Test
+ public void testAppendNullAttributeKey()
+ {
+ key.appendAttribute(null);
+ assertEquals("Null attribute key not correctly appended", "", key
+ .toString());
+ }
+
+ /**
+ * Tests appending an index to a key.
+ */
+ @Test
+ public void testAppendIndex()
+ {
+ key.append("test").appendIndex(42);
+ assertEquals("Index was not correctly appended", "test(42)", key
+ .toString());
+ }
+
+ /**
+ * Tests constructing a complex key by chaining multiple append operations.
+ */
+ @Test
+ public void testAppendComplexKey()
+ {
+ key.append("tables").append("table.").appendIndex(0);
+ key.append("fields.").append("field").appendIndex(1);
+ key.appendAttribute("dataType");
+ assertEquals("Wrong complex key", TESTKEY, key.toString());
+ }
+
+ /**
+ * Tests getting and setting the key's length.
+ */
+ @Test
+ public void testLength()
+ {
+ key.append(TESTPROPS);
+ assertEquals("Wrong length", TESTPROPS.length(), key.length());
+ key.appendAttribute("dataType");
+ assertEquals("Wrong length", TESTKEY.length(), key.length());
+ key.setLength(TESTPROPS.length());
+ assertEquals("Wrong length after shortening", TESTPROPS.length(), key
+ .length());
+ assertEquals("Wrong resulting key", TESTPROPS, key.toString());
+ }
+
+ /**
+ * Tests comparing configuration keys.
+ */
+ @Test
+ public void testEquals()
+ {
+ DefaultConfigurationKey k1 = new DefaultConfigurationKey(
+ expressionEngine, TESTKEY);
+ DefaultConfigurationKey k2 = new DefaultConfigurationKey(
+ expressionEngine, TESTKEY);
+ assertTrue("Keys are not equal", k1.equals(k2));
+ assertTrue("Not reflexiv", k2.equals(k1));
+ assertEquals("Hash codes not equal", k1.hashCode(), k2.hashCode());
+ k2.append("anotherPart");
+ assertFalse("Keys considered equal", k1.equals(k2));
+ assertFalse("Keys considered equal", k2.equals(k1));
+ assertFalse("Key equals null key", k1.equals(null));
+ assertTrue("Faild comparison with string", k1.equals(TESTKEY));
+ }
+
+ /**
+ * Tests determining an attribute key's name.
+ */
+ @Test
+ public void testAttributeName()
+ {
+ assertEquals("Plain key not detected", "test", key
+ .attributeName("test"));
+ assertEquals("Attribute markers not stripped", "dataType", key
+ .attributeName(TESTATTR));
+ assertNull("Null key not processed", key.attributeName(null));
+ }
+
+ /**
+ * Tests to iterate over a simple key.
+ */
+ @Test
+ public void testIterate()
+ {
+ key.append(TESTKEY);
+ DefaultConfigurationKey.KeyIterator it = key.iterator();
+ assertTrue("No key parts", it.hasNext());
+ assertEquals("Wrong key part", "tables", it.nextKey());
+ assertEquals("Wrong key part", "table", it.nextKey());
+ assertTrue("No index found", it.hasIndex());
+ assertEquals("Wrong index", 0, it.getIndex());
+ assertEquals("Wrong key part", "fields", it.nextKey());
+ assertFalse("Found an index", it.hasIndex());
+ assertEquals("Wrong key part", "field", it.nextKey(true));
+ assertEquals("Wrong index", 1, it.getIndex());
+ assertFalse("Found an attribute", it.isAttribute());
+ assertEquals("Wrong current key", "field", it.currentKey(true));
+ assertEquals("Wrong key part", "dataType", it.nextKey());
+ assertEquals("Wrong decorated key part", "[@dataType]", it
+ .currentKey(true));
+ assertTrue("Attribute not found", it.isAttribute());
+ assertFalse("Too many key parts", it.hasNext());
+ try
+ {
+ it.next();
+ fail("Could iterate over the iteration's end!");
+ }
+ catch (NoSuchElementException nex)
+ {
+ // ok
+ }
+ }
+
+ /**
+ * Tests an iteration where the remove() method is called. This is not
+ * supported.
+ */
+ @Test(expected = UnsupportedOperationException.class)
+ public void testIterateWithRemove()
+ {
+ assertFalse(key.iterator().hasNext());
+ key.append("simple");
+ DefaultConfigurationKey.KeyIterator it = key.iterator();
+ assertTrue(it.hasNext());
+ assertEquals("simple", it.next());
+ it.remove();
+ }
+
+ /**
+ * Tests iterating over some funny keys.
+ */
+ @Test
+ public void testIterateStrangeKeys()
+ {
+ key = new DefaultConfigurationKey(expressionEngine, "key.");
+ DefaultConfigurationKey.KeyIterator it = key.iterator();
+ assertTrue("Too few key parts", it.hasNext());
+ assertEquals("Wrong key part", "key", it.next());
+ assertFalse("Too many key parts", it.hasNext());
+
+ key = new DefaultConfigurationKey(expressionEngine, ".");
+ it = key.iterator();
+ assertFalse("Simple delimiter key has more parts", it.hasNext());
+
+ key = new DefaultConfigurationKey(expressionEngine,
+ "key().index()undefined(0).test");
+ it = key.iterator();
+ assertEquals("Wrong first part", "key()", it.next());
+ assertFalse("Index detected in first part", it.hasIndex());
+ assertEquals("Wrong second part", "index()undefined", it.nextKey(false));
+ assertTrue("No index detected in second part", it.hasIndex());
+ assertEquals("Wrong index value", 0, it.getIndex());
+ }
+
+ /**
+ * Tests iterating over keys with escaped delimiters.
+ */
+ @Test
+ public void testIterateEscapedDelimiters()
+ {
+ key.append("my..elem");
+ key.append("trailing..dot..");
+ key.append(".strange");
+ assertEquals("my..elem.trailing..dot...strange", key.toString());
+ DefaultConfigurationKey.KeyIterator kit = key.iterator();
+ assertEquals("Wrong first part", "my.elem", kit.nextKey());
+ assertEquals("Wrong second part", "trailing.dot.", kit.nextKey());
+ assertEquals("Wrong third part", "strange", kit.nextKey());
+ assertFalse("Too many parts", kit.hasNext());
+ }
+
+ /**
+ * Tests iterating over keys when a different escaped delimiter is used.
+ */
+ @Test
+ public void testIterateAlternativeEscapeDelimiter()
+ {
+ expressionEngine.setEscapedDelimiter("\\.");
+ key.append("\\.my\\.elem");
+ key.append("trailing\\.dot\\.");
+ key.append(".strange");
+ assertEquals("\\.my\\.elem.trailing\\.dot\\..strange", key.toString());
+ DefaultConfigurationKey.KeyIterator kit = key.iterator();
+ assertEquals("Wrong first part", ".my.elem", kit.nextKey());
+ assertEquals("Wrong second part", "trailing.dot.", kit.nextKey());
+ assertEquals("Wrong third part", "strange", kit.nextKey());
+ assertFalse("Too many parts", kit.hasNext());
+ }
+
+ /**
+ * Tests iterating when no escape delimiter is defined.
+ */
+ @Test
+ public void testIterateWithoutEscapeDelimiter()
+ {
+ expressionEngine.setEscapedDelimiter(null);
+ key.append("..my..elem.trailing..dot...strange");
+ assertEquals("Wrong key", "my..elem.trailing..dot...strange", key
+ .toString());
+ DefaultConfigurationKey.KeyIterator kit = key.iterator();
+ final String[] parts =
+ { "my", "elem", "trailing", "dot", "strange"};
+ for (int i = 0; i < parts.length; i++)
+ {
+ assertEquals("Wrong key part " + i, parts[i], kit.next());
+ }
+ assertFalse("Too many parts", kit.hasNext());
+ }
+
+ /**
+ * Tests whether a key with brackets in it can be iterated over.
+ */
+ @Test
+ public void testIterateWithBrackets()
+ {
+ key.append("directory.platform(x86).path");
+ DefaultConfigurationKey.KeyIterator kit = key.iterator();
+ String part = kit.nextKey();
+ assertEquals("Wrong part 1", "directory", part);
+ assertFalse("Has index 1", kit.hasIndex());
+ part = kit.nextKey();
+ assertEquals("Wrong part 2", "platform(x86)", part);
+ assertFalse("Has index 2", kit.hasIndex());
+ part = kit.nextKey();
+ assertEquals("Wrong part 3", "path", part);
+ assertFalse("Has index 3", kit.hasIndex());
+ assertFalse("Too many elements", kit.hasNext());
+ }
+
+ /**
+ * Tests iterating over an attribute key that has an index.
+ */
+ @Test
+ public void testAttributeKeyWithIndex()
+ {
+ key.append(TESTATTR);
+ key.appendIndex(0);
+ assertEquals("Wrong attribute key with index", TESTATTR + "(0)", key
+ .toString());
+
+ DefaultConfigurationKey.KeyIterator it = key.iterator();
+ assertTrue("No first element", it.hasNext());
+ it.next();
+ assertTrue("Index not found", it.hasIndex());
+ assertEquals("Incorrect index", 0, it.getIndex());
+ assertTrue("Attribute not found", it.isAttribute());
+ assertEquals("Wrong plain key", "dataType", it.currentKey(false));
+ assertEquals("Wrong decorated key", TESTATTR, it.currentKey(true));
+ }
+
+ /**
+ * Tests iteration when the attribute markers equals the property delimiter.
+ */
+ @Test
+ public void testIterateAttributeEqualsPropertyDelimiter()
+ {
+ expressionEngine.setAttributeEnd(null);
+ expressionEngine.setAttributeStart(expressionEngine
+ .getPropertyDelimiter());
+ key.append("this.isa.key");
+ DefaultConfigurationKey.KeyIterator kit = key.iterator();
+ assertEquals("Wrong first key part", "this", kit.next());
+ assertFalse("First part is an attribute", kit.isAttribute());
+ assertTrue("First part is not a property key", kit.isPropertyKey());
+ assertEquals("Wrong second key part", "isa", kit.next());
+ assertFalse("Second part is an attribute", kit.isAttribute());
+ assertTrue("Second part is not a property key", kit.isPropertyKey());
+ assertEquals("Wrong third key part", "key", kit.next());
+ assertTrue("Third part is not an attribute", kit.isAttribute());
+ assertTrue("Third part is not a property key", kit.isPropertyKey());
+ assertEquals("Wrong decorated key part", "key", kit.currentKey(true));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/tree/TestDefaultConfigurationNode.java b/src/test/java/org/apache/commons/configuration/tree/TestDefaultConfigurationNode.java
new file mode 100644
index 0000000..b801566
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/TestDefaultConfigurationNode.java
@@ -0,0 +1,507 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for DefaultConfigurationNode.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestDefaultConfigurationNode.java 1225917 2011-12-30 20:42:09Z oheger $
+ */
+public class TestDefaultConfigurationNode
+{
+ /** Constant array for the field names. */
+ private static final String[] FIELD_NAMES =
+ { "UID", "NAME", "FIRSTNAME", "LASTLOGIN"};
+
+ /** Constant array for the field data types. */
+ private static final String[] FIELD_TYPES =
+ { "long", "string", "string", "date"};
+
+ /** Constant array for additional field attributes. */
+ private static final String[] FIELD_ATTRS =
+ { "primarykey,unique", "notnull", "notnull", null};
+
+ /** The node to be tested. */
+ DefaultConfigurationNode node;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ node = new DefaultConfigurationNode();
+ node.setName("table");
+ node.setReference("TestReference");
+ node.addAttribute(new DefaultConfigurationNode("type", "system"));
+ node.addChild(new DefaultConfigurationNode("name", "users"));
+
+ // Add nodes for the table's fields
+ for (int i = 0; i < FIELD_NAMES.length; i++)
+ {
+ DefaultConfigurationNode field = new DefaultConfigurationNode(
+ "field");
+ field
+ .addChild(new DefaultConfigurationNode("name",
+ FIELD_NAMES[i]));
+ field.addAttribute(new DefaultConfigurationNode("type",
+ FIELD_TYPES[i]));
+ if (FIELD_ATTRS[i] != null)
+ {
+ StringTokenizer tok = new StringTokenizer(FIELD_ATTRS[i], ", ");
+ while (tok.hasMoreTokens())
+ {
+ field.addAttribute(new DefaultConfigurationNode(
+ "attribute", tok.nextToken()));
+ }
+ }
+ node.addChild(field);
+ }
+ }
+
+ /**
+ * Tests a newly created, uninitialized node.
+ */
+ @Test
+ public void testNewNode()
+ {
+ node = new DefaultConfigurationNode();
+ assertNull("name is not null", node.getName());
+ assertNull("value is not null", node.getValue());
+ assertNull("reference is not null", node.getReference());
+ assertTrue("Children are not empty", node.getChildren().isEmpty());
+ assertTrue("Named children are not empty", node.getChildren("test")
+ .isEmpty());
+ assertEquals("Children cound is not 0", 0, node.getChildrenCount());
+ assertEquals("Named children count is not 0", 0, node
+ .getChildrenCount("test"));
+ assertTrue("Attributes are not empty", node.getAttributes().isEmpty());
+ assertTrue("Named attributes are not empty", node.getAttributes("test")
+ .isEmpty());
+ assertNull("Node has a parent", node.getParentNode());
+ assertFalse("Node is defined", node.isDefined());
+ }
+
+ /**
+ * Tries to access an attribute using an invalid index.
+ */
+ @Test(expected = IndexOutOfBoundsException.class)
+ public void testGetAttributeNonExisting()
+ {
+ node = new DefaultConfigurationNode();
+ node.getAttribute(0);
+ }
+
+ /**
+ * Tests accessing a node's reference.
+ */
+ @Test
+ public void testGetReference()
+ {
+ assertEquals("Reference was not stored", "TestReference", node
+ .getReference());
+ }
+
+ /**
+ * Tests accessing the node's children.
+ */
+ @Test
+ public void testGetChildren()
+ {
+ assertEquals("Number of children incorrect", FIELD_NAMES.length + 1,
+ node.getChildrenCount());
+ List<ConfigurationNode> children = node.getChildren();
+ Iterator<ConfigurationNode> it = children.iterator();
+ DefaultConfigurationNode child = (DefaultConfigurationNode) it.next();
+ assertEquals("Wrong node", "name", child.getName());
+ checkFieldNodes(it);
+ }
+
+ /**
+ * Tests accessing the node's children by name.
+ */
+ @Test
+ public void testGetChildrenByName()
+ {
+ List<ConfigurationNode> children = node.getChildren("field");
+ assertEquals("Incorrect number of child nodes", FIELD_NAMES.length,
+ children.size());
+ assertEquals("Incorrect result of getChildrenCount()",
+ FIELD_NAMES.length, node.getChildrenCount("field"));
+ checkFieldNodes(children.iterator());
+ assertTrue("Found non existing nodes", node.getChildren("test")
+ .isEmpty());
+ assertEquals("Wrong children list for null", node.getChildren(), node
+ .getChildren(null));
+ }
+
+ /**
+ * Tests adding a new child node.
+ */
+ @Test
+ public void testAddChild()
+ {
+ int cnt = node.getChildrenCount();
+ DefaultConfigurationNode ndNew = new DefaultConfigurationNode("test",
+ "xyz");
+ node.addChild(ndNew);
+ assertEquals("New node was not added", cnt + 1, node.getChildrenCount());
+ List<ConfigurationNode> children = node.getChildren();
+ assertEquals("Incorrect number of children", node.getChildrenCount(),
+ children.size());
+ assertSame("Node was not added to end", ndNew, children.get(cnt));
+ assertEquals("Incorrect number of named children", 1, node
+ .getChildrenCount(ndNew.getName()));
+ assertFalse("Child is an attribute", ndNew.isAttribute());
+ assertSame("Parent was not set", node, ndNew.getParentNode());
+ }
+
+ /**
+ * Tries to add a null child node.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddChildNull()
+ {
+ node.addChild(null);
+ }
+
+ /**
+ * Tries to add a node without a name.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddUndefinedChild()
+ {
+ node.addChild(new DefaultConfigurationNode());
+ }
+
+ /**
+ * Tests removing a child node.
+ */
+ @Test
+ public void testRemoveChild()
+ {
+ DefaultConfigurationNode child = (DefaultConfigurationNode) node
+ .getChildren().get(3);
+ int cnt = node.getChildrenCount();
+ node.removeChild(child);
+ assertEquals("Child was not removed", cnt - 1, node.getChildrenCount());
+ for (ConfigurationNode nd : node.getChildren())
+ {
+ assertNotSame("Found removed node", child, nd);
+ }
+ assertNull("Parent reference was not removed", child.getParentNode());
+ }
+
+ /**
+ * Tests removing a child node that does not belong to this node.
+ */
+ @Test
+ public void testRemoveNonExistingChild()
+ {
+ int cnt = node.getChildrenCount();
+ node.removeChild(new DefaultConfigurationNode("test"));
+ node.removeChild(new DefaultConfigurationNode());
+ node.removeChild((ConfigurationNode) null);
+ node.removeChild("non existing child node");
+ node.removeChild((String) null);
+ assertEquals("Children were changed", cnt, node.getChildrenCount());
+ }
+
+ /**
+ * Tests removing children by their name.
+ */
+ @Test
+ public void testRemoveChildByName()
+ {
+ int cnt = node.getChildrenCount();
+ node.removeChild("name");
+ assertEquals("Child was not removed", cnt - 1, node.getChildrenCount());
+ assertEquals("Still found name child", 0, node.getChildrenCount("name"));
+ node.removeChild("field");
+ assertEquals("Still remaining nodes", 0, node.getChildrenCount());
+ }
+
+ /**
+ * Tests removing all children at once.
+ */
+ @Test
+ public void testRemoveChildren()
+ {
+ node.removeChildren();
+ assertEquals("Children count is not 0", 0, node.getChildrenCount());
+ assertTrue("Children are not empty", node.getChildren().isEmpty());
+ }
+
+ /**
+ * Tests accessing a child by its index.
+ */
+ @Test
+ public void testGetChild()
+ {
+ ConfigurationNode child = node.getChild(2);
+ assertEquals("Wrong child returned", child, node.getChildren().get(2));
+ }
+
+ /**
+ * Tests accessing child nodes with invalid indices.
+ */
+ @Test(expected = IndexOutOfBoundsException.class)
+ public void testGetChildInvalidIndex()
+ {
+ node.getChild(4724);
+ }
+
+ /**
+ * Tests accessing the node's attributes.
+ */
+ @Test
+ public void testGetAttributes()
+ {
+ assertEquals("Number of attributes incorrect", 1, node
+ .getAttributeCount());
+ List<ConfigurationNode> attributes = node.getAttributes();
+ Iterator<ConfigurationNode> it = attributes.iterator();
+ DefaultConfigurationNode attr = (DefaultConfigurationNode) it.next();
+ assertEquals("Wrong node", "type", attr.getName());
+ assertFalse("More attributes", it.hasNext());
+ }
+
+ /**
+ * Tests accessing the node's attributes by name.
+ */
+ @Test
+ public void testGetAttributesByName()
+ {
+ assertEquals("Incorrect number of attributes", 1, node
+ .getAttributeCount("type"));
+ DefaultConfigurationNode field = (DefaultConfigurationNode) node
+ .getChildren().get(1);
+ assertEquals("Incorrect number of attributes", 2, field
+ .getAttributeCount("attribute"));
+ List<ConfigurationNode> attrs = field.getAttributes("attribute");
+ assertEquals("Wrong value", "primarykey",
+ ((DefaultConfigurationNode) attrs.get(0)).getValue());
+ assertEquals("Wrong value", "unique", ((DefaultConfigurationNode) attrs
+ .get(1)).getValue());
+ }
+
+ /**
+ * Tests adding a new attribute node.
+ */
+ @Test
+ public void testAddAttribute()
+ {
+ int cnt = node.getAttributeCount();
+ DefaultConfigurationNode ndNew = new DefaultConfigurationNode("test",
+ "xyz");
+ node.addAttribute(ndNew);
+ assertEquals("New node was not added", cnt + 1, node
+ .getAttributeCount());
+ List<ConfigurationNode> attrs = node.getAttributes();
+ assertEquals("Incorrect number of attributes",
+ node.getAttributeCount(), attrs.size());
+ assertSame("Node was not added to end", ndNew, attrs.get(cnt));
+ assertEquals("Incorrect number of named attributes", 1, node
+ .getAttributeCount(ndNew.getName()));
+ assertTrue("Child is no attribute", ndNew.isAttribute());
+ assertSame("Parent was not set", node, ndNew.getParentNode());
+ }
+
+ /**
+ * Tests removing an attribute node.
+ */
+ @Test
+ public void testRemoveAttribute()
+ {
+ DefaultConfigurationNode attr = (DefaultConfigurationNode) node
+ .getAttributes().get(0);
+ int cnt = node.getAttributeCount();
+ node.removeAttribute(attr);
+ assertEquals("Attribute was not removed", cnt - 1, node
+ .getAttributeCount());
+ for (ConfigurationNode nd : node.getAttributes())
+ {
+ assertNotSame("Found removed node", attr, nd);
+ }
+ assertNull("Parent reference was not removed", attr.getParentNode());
+ }
+
+ /**
+ * Tests removing attributes by their names.
+ */
+ @Test
+ public void testRemoveAttributeByName()
+ {
+ ConfigurationNode field = node.getChild(1);
+ assertEquals("Incorrect number of attributes", 3, field
+ .getAttributeCount());
+ field.removeAttribute("attribute");
+ assertEquals("Not all nodes removed", 1, field.getAttributeCount());
+ assertTrue("Remaining attributes", field.getAttributes("attribute")
+ .isEmpty());
+ field.removeAttribute("type");
+ assertEquals("Remaining attributes", 0, field.getAttributeCount());
+ }
+
+ /**
+ * Tests removing all attributes.
+ */
+ @Test
+ public void testRemoveAttributes()
+ {
+ node.removeAttributes();
+ assertEquals("Not all attributes removed", 0, node.getAttributeCount());
+ assertTrue("Attributes not empty", node.getAttributes().isEmpty());
+ }
+
+ /**
+ * Tests changing a node's attribute state.
+ */
+ @Test(expected = IllegalStateException.class)
+ public void testChangeAttributeState()
+ {
+ ConfigurationNode attr = node.getAttribute(0);
+ attr.setAttribute(false);
+ }
+
+ /**
+ * Tests the visit() method using a simple visitor.
+ */
+ @Test
+ public void testVisit()
+ {
+ CountNodeVisitor visitor = new CountNodeVisitor();
+ node.visit(visitor);
+ assertEquals("Not all nodes visited", 19, visitor.beforeCalls);
+ assertEquals("Different number of before and after calls",
+ visitor.beforeCalls, visitor.afterCalls);
+ }
+
+ /**
+ * Tests the visit() method with a visitor that terminates the visit
+ * process.
+ */
+ @Test
+ public void testVisitWithTerminate()
+ {
+ CountNodeVisitor visitor = new CountNodeVisitor(10);
+ node.visit(visitor);
+ assertEquals("Incorrect number of nodes visited", visitor.maxCalls,
+ visitor.beforeCalls);
+ assertEquals("Different number of before and after calls",
+ visitor.beforeCalls, visitor.afterCalls);
+ }
+
+ /**
+ * Tests the visit() method when null is passed in. This should throw an
+ * exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testVisitWithNullVisitor()
+ {
+ node.visit(null);
+ }
+
+ /**
+ * Tests cloning a node.
+ */
+ @Test
+ public void testClone()
+ {
+ node.setValue("TestValue");
+ DefaultConfigurationNode clone = (DefaultConfigurationNode) node.clone();
+ assertEquals("Value not cloned", "TestValue", clone.getValue());
+ assertEquals("Name not cloned", "table", clone.getName());
+ assertEquals("Reference not cloned", "TestReference", clone.getReference());
+ assertEquals("Children were cloned", 0, clone.getChildrenCount());
+ assertEquals("Attributes were cloned", 0, clone.getAttributeCount());
+ }
+
+ /**
+ * Helper method for checking the child nodes of type "field".
+ *
+ * @param itFields the iterator with the child nodes
+ */
+ private void checkFieldNodes(Iterator<ConfigurationNode> itFields)
+ {
+ for (int i = 0; i < FIELD_NAMES.length; i++)
+ {
+ DefaultConfigurationNode child = (DefaultConfigurationNode) itFields
+ .next();
+ assertEquals("Wrong node", "field", child.getName());
+ List<ConfigurationNode> nameNodes = child.getChildren("name");
+ assertEquals("Wrong number of name nodes", 1, nameNodes.size());
+ DefaultConfigurationNode nameNode = (DefaultConfigurationNode) nameNodes
+ .get(0);
+ assertEquals("Wrong field name", FIELD_NAMES[i], nameNode
+ .getValue());
+ }
+ }
+
+ /**
+ * A test visitor implementation that is able to count the number of visits.
+ * It also supports a maximum number of visits to be set; if this number is
+ * reached, the <code>terminate()</code> method returns <b>true</b>.
+ */
+ public static class CountNodeVisitor implements ConfigurationNodeVisitor
+ {
+ public int beforeCalls;
+
+ public int afterCalls;
+
+ public int maxCalls;
+
+ public CountNodeVisitor()
+ {
+ this(Integer.MAX_VALUE);
+ }
+
+ public CountNodeVisitor(int maxNumberOfVisits)
+ {
+ maxCalls = maxNumberOfVisits;
+ }
+
+ public void visitBeforeChildren(ConfigurationNode node)
+ {
+ beforeCalls++;
+ }
+
+ public void visitAfterChildren(ConfigurationNode node)
+ {
+ afterCalls++;
+ }
+
+ public boolean terminate()
+ {
+ return beforeCalls >= maxCalls;
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/tree/TestDefaultExpressionEngine.java b/src/test/java/org/apache/commons/configuration/tree/TestDefaultExpressionEngine.java
new file mode 100644
index 0000000..33efd92
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/TestDefaultExpressionEngine.java
@@ -0,0 +1,539 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for DefaultExpressionEngine.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestDefaultExpressionEngine.java 1225918 2011-12-30 20:54:47Z oheger $
+ */
+public class TestDefaultExpressionEngine
+{
+ /** Stores the names of the test nodes representing tables. */
+ private static String[] tables =
+ { "users", "documents"};
+
+ /** Stores the types of the test table nodes. */
+ private static String[] tabTypes =
+ { "system", "application"};
+
+ /** Test data fields for the node hierarchy. */
+ private static String[][] fields =
+ {
+ { "uid", "uname", "firstName", "lastName", "email"},
+ { "docid", "name", "creationDate", "authorID", "version"}};
+
+ /** The object to be tested. */
+ DefaultExpressionEngine engine;
+
+ /** The root of a hierarchy with configuration nodes. */
+ ConfigurationNode root;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ root = setUpNodes();
+ engine = new DefaultExpressionEngine();
+ }
+
+ /**
+ * Tests some simple queries.
+ */
+ @Test
+ public void testQueryKeys()
+ {
+ checkKey("tables.table.name", "name", 2);
+ checkKey("tables.table.fields.field.name", "name", 10);
+ checkKey("tables.table[@type]", "type", 2);
+ checkKey("tables.table(0).fields.field.name", "name", 5);
+ checkKey("tables.table(1).fields.field.name", "name", 5);
+ checkKey("tables.table.fields.field(1).name", "name", 2);
+ }
+
+ /**
+ * Performs some queries and evaluates the values of the result nodes.
+ */
+ @Test
+ public void testQueryNodes()
+ {
+ for (int i = 0; i < tables.length; i++)
+ {
+ checkKeyValue("tables.table(" + i + ").name", "name", tables[i]);
+ checkKeyValue("tables.table(" + i + ")[@type]", "type", tabTypes[i]);
+
+ for (int j = 0; j < fields[i].length; j++)
+ {
+ checkKeyValue("tables.table(" + i + ").fields.field(" + j
+ + ").name", "name", fields[i][j]);
+ }
+ }
+ }
+
+ /**
+ * Tests querying keys that do not exist.
+ */
+ @Test
+ public void testQueryNonExistingKeys()
+ {
+ checkKey("tables.tablespace.name", null, 0);
+ checkKey("tables.table(2).name", null, 0);
+ checkKey("a complete unknown key", null, 0);
+ checkKey("tables.table(0).fields.field(-1).name", null, 0);
+ checkKey("tables.table(0).fields.field(28).name", null, 0);
+ checkKey("tables.table(0).fields.field().name", null, 0);
+ checkKey("connection.settings.usr.name", null, 0);
+ }
+
+ /**
+ * Tests querying nodes whose names contain a delimiter.
+ */
+ @Test
+ public void testQueryEscapedKeys()
+ {
+ checkKeyValue("connection..settings.usr..name", "usr.name", "scott");
+ checkKeyValue("connection..settings.usr..pwd", "usr.pwd", "tiger");
+ }
+
+ /**
+ * Tests some queries when the same delimiter is used for properties and
+ * attributes.
+ */
+ @Test
+ public void testQueryAttributeEmulation()
+ {
+ engine.setAttributeEnd(null);
+ engine.setAttributeStart(engine.getPropertyDelimiter());
+ checkKeyValue("tables.table(0).name", "name", tables[0]);
+ checkKeyValue("tables.table(0).type", "type", tabTypes[0]);
+ checkKey("tables.table.type", "type", 2);
+ }
+
+ /**
+ * Tests accessing the root node.
+ */
+ @Test
+ public void testQueryRootNode()
+ {
+ List<ConfigurationNode> nodes = checkKey(null, null, 1);
+ assertSame("Root node not found", root, nodes.get(0));
+ nodes = checkKey("", null, 1);
+ assertSame("Root node not found", root, nodes.get(0));
+ checkKeyValue("[@test]", "test", "true");
+ }
+
+ /**
+ * Tests a different query syntax. Sets other strings for the typical tokens
+ * used by the expression engine.
+ */
+ @Test
+ public void testQueryAlternativeSyntax()
+ {
+ setUpAlternativeSyntax();
+ checkKeyValue("tables/table[1]/name", "name", tables[1]);
+ checkKeyValue("tables/table[0]@type", "type", tabTypes[0]);
+ checkKeyValue("@test", "test", "true");
+ checkKeyValue("connection.settings/usr.name", "usr.name", "scott");
+ }
+
+ /**
+ * Tests obtaining keys for nodes.
+ */
+ @Test
+ public void testNodeKey()
+ {
+ ConfigurationNode node = root.getChild(0);
+ assertEquals("Invalid name for descendant of root", "tables", engine
+ .nodeKey(node, ""));
+ assertEquals("Parent key not respected", "test.tables", engine.nodeKey(
+ node, "test"));
+ assertEquals("Full parent key not taken into account",
+ "a.full.parent.key.tables", engine.nodeKey(node,
+ "a.full.parent.key"));
+ }
+
+ /**
+ * Tests obtaining keys when the root node is involved.
+ */
+ @Test
+ public void testNodeKeyWithRoot()
+ {
+ assertEquals("Wrong name for root noot", "", engine.nodeKey(root, null));
+ assertEquals("Null name not detected", "test", engine.nodeKey(root,
+ "test"));
+ }
+
+ /**
+ * Tests obtaining keys for attribute nodes.
+ */
+ @Test
+ public void testNodeKeyWithAttribute()
+ {
+ ConfigurationNode node = root.getChild(0).getChild(0).getAttribute(0);
+ assertEquals("Wrong attribute node", "type", node.getName());
+ assertEquals("Wrong attribute key", "tables.table[@type]", engine
+ .nodeKey(node, "tables.table"));
+ assertEquals("Wrong key for root attribute", "[@test]", engine.nodeKey(
+ root.getAttribute(0), ""));
+ }
+
+ /**
+ * Tests obtaining keys for nodes that contain the delimiter character.
+ */
+ @Test
+ public void testNodeKeyWithEscapedDelimiters()
+ {
+ ConfigurationNode node = root.getChild(1);
+ assertEquals("Wrong escaped key", "connection..settings", engine
+ .nodeKey(node, ""));
+ assertEquals("Wrong complex escaped key",
+ "connection..settings.usr..name", engine.nodeKey(node
+ .getChild(0), engine.nodeKey(node, "")));
+ }
+
+ /**
+ * Tests obtaining node keys when a different syntax is set.
+ */
+ @Test
+ public void testNodeKeyWithAlternativeSyntax()
+ {
+ setUpAlternativeSyntax();
+ assertEquals("Wrong child key", "tables/table", engine.nodeKey(root
+ .getChild(0).getChild(0), "tables"));
+ assertEquals("Wrong attribute key", "@test", engine.nodeKey(root
+ .getAttribute(0), ""));
+
+ engine.setAttributeStart(engine.getPropertyDelimiter());
+ assertEquals("Wrong attribute key", "/test", engine.nodeKey(root
+ .getAttribute(0), ""));
+ }
+
+ /**
+ * Tests adding direct child nodes to the existing hierarchy.
+ */
+ @Test
+ public void testPrepareAddDirectly()
+ {
+ NodeAddData data = engine.prepareAdd(root, "newNode");
+ assertSame("Wrong parent node", root, data.getParent());
+ assertTrue("Path nodes available", data.getPathNodes().isEmpty());
+ assertEquals("Wrong name of new node", "newNode", data.getNewNodeName());
+ assertFalse("New node is an attribute", data.isAttribute());
+
+ data = engine.prepareAdd(root, "tables.table.fields.field.name");
+ assertEquals("Wrong name of new node", "name", data.getNewNodeName());
+ assertTrue("Path nodes available", data.getPathNodes().isEmpty());
+ assertEquals("Wrong parent node", "field", data.getParent().getName());
+ ConfigurationNode nd = data.getParent().getChild(0);
+ assertEquals("Field has no name node", "name", nd.getName());
+ assertEquals("Incorrect name", "version", nd.getValue());
+ }
+
+ /**
+ * Tests adding when indices are involved.
+ */
+ @Test
+ public void testPrepareAddWithIndex()
+ {
+ NodeAddData data = engine
+ .prepareAdd(root, "tables.table(0).tableSpace");
+ assertEquals("Wrong name of new node", "tableSpace", data
+ .getNewNodeName());
+ assertTrue("Path nodes available", data.getPathNodes().isEmpty());
+ assertEquals("Wrong type of parent node", "table", data.getParent()
+ .getName());
+ ConfigurationNode node = data.getParent().getChild(0);
+ assertEquals("Wrong table", tables[0], node.getValue());
+
+ data = engine.prepareAdd(root, "tables.table(1).fields.field(2).alias");
+ assertEquals("Wrong name of new node", "alias", data.getNewNodeName());
+ assertEquals("Wrong type of parent node", "field", data.getParent()
+ .getName());
+ assertEquals("Wrong field node", "creationDate", data.getParent()
+ .getChild(0).getValue());
+ }
+
+ /**
+ * Tests adding new attributes.
+ */
+ @Test
+ public void testPrepareAddAttribute()
+ {
+ NodeAddData data = engine.prepareAdd(root,
+ "tables.table(0)[@tableSpace]");
+ assertEquals("Wrong table node", tables[0], data.getParent()
+ .getChild(0).getValue());
+ assertEquals("Wrong name of new node", "tableSpace", data
+ .getNewNodeName());
+ assertTrue("Attribute not detected", data.isAttribute());
+ assertTrue("Path nodes available", data.getPathNodes().isEmpty());
+
+ data = engine.prepareAdd(root, "[@newAttr]");
+ assertSame("Root node is not parent", root, data.getParent());
+ assertEquals("Wrong name of new node", "newAttr", data.getNewNodeName());
+ assertTrue("Attribute not detected", data.isAttribute());
+ }
+
+ /**
+ * Tests add operations where complete paths are added.
+ */
+ @Test
+ public void testPrepareAddWithPath()
+ {
+ NodeAddData data = engine.prepareAdd(root,
+ "tables.table(1).fields.field(-1).name");
+ assertEquals("Wrong name of new node", "name", data.getNewNodeName());
+ checkNodePath(data, new String[]
+ { "field"});
+ assertEquals("Wrong type of parent node", "fields", data.getParent()
+ .getName());
+
+ data = engine.prepareAdd(root, "tables.table(-1).name");
+ assertEquals("Wrong name of new node", "name", data.getNewNodeName());
+ checkNodePath(data, new String[]
+ { "table"});
+ assertEquals("Wrong type of parent node", "tables", data.getParent()
+ .getName());
+
+ data = engine.prepareAdd(root, "a.complete.new.path");
+ assertEquals("Wrong name of new node", "path", data.getNewNodeName());
+ checkNodePath(data, new String[]
+ { "a", "complete", "new"});
+ assertSame("Root is not parent", root, data.getParent());
+ }
+
+ /**
+ * Tests add operations when property and attribute delimiters are equal.
+ * Then it is not possible to add new attribute nodes.
+ */
+ @Test
+ public void testPrepareAddWithSameAttributeDelimiter()
+ {
+ engine.setAttributeEnd(null);
+ engine.setAttributeStart(engine.getPropertyDelimiter());
+
+ NodeAddData data = engine.prepareAdd(root, "tables.table(0).test");
+ assertEquals("Wrong name of new node", "test", data.getNewNodeName());
+ assertFalse("New node is an attribute", data.isAttribute());
+ assertEquals("Wrong type of parent node", "table", data.getParent()
+ .getName());
+
+ data = engine.prepareAdd(root, "a.complete.new.path");
+ assertFalse("New node is an attribute", data.isAttribute());
+ checkNodePath(data, new String[]
+ { "a", "complete", "new"});
+ }
+
+ /**
+ * Tests add operations when an alternative syntax is set.
+ */
+ @Test
+ public void testPrepareAddWithAlternativeSyntax()
+ {
+ setUpAlternativeSyntax();
+ NodeAddData data = engine.prepareAdd(root, "tables/table[0]/test");
+ assertEquals("Wrong name of new node", "test", data.getNewNodeName());
+ assertFalse("New node is attribute", data.isAttribute());
+ assertEquals("Wrong parent node", tables[0], data.getParent().getChild(
+ 0).getValue());
+
+ data = engine.prepareAdd(root, "a/complete/new/path at attr");
+ assertEquals("Wrong name of new attribute", "attr", data
+ .getNewNodeName());
+ checkNodePath(data, new String[]
+ { "a", "complete", "new", "path"});
+ assertSame("Root is not parent", root, data.getParent());
+ }
+
+ /**
+ * Tests using invalid keys, e.g. if something should be added to
+ * attributes.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrepareAddInvalidKey()
+ {
+ engine.prepareAdd(root, "tables.table(0)[@type].new");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrepareAddInvalidKeyAttribute()
+ {
+ engine
+ .prepareAdd(root,
+ "a.complete.new.path.with.an[@attribute].at.a.non.allowed[@position]");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrepareAddNullKey()
+ {
+ engine.prepareAdd(root, null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrepareAddEmptyKey()
+ {
+ engine.prepareAdd(root, "");
+ }
+
+ /**
+ * Creates a node hierarchy for testing that consists of tables, their
+ * fields, and some additional data:
+ *
+ * <pre>
+ * tables
+ * table
+ * name
+ * fields
+ * field
+ * name
+ * field
+ * name
+ * </pre>
+ *
+ * @return the root of the test node hierarchy
+ */
+ protected ConfigurationNode setUpNodes()
+ {
+ DefaultConfigurationNode rootNode = new DefaultConfigurationNode();
+
+ DefaultConfigurationNode nodeTables = new DefaultConfigurationNode(
+ "tables");
+ rootNode.addChild(nodeTables);
+ for (int i = 0; i < tables.length; i++)
+ {
+ DefaultConfigurationNode nodeTable = new DefaultConfigurationNode(
+ "table");
+ nodeTables.addChild(nodeTable);
+ nodeTable.addChild(new DefaultConfigurationNode("name", tables[i]));
+ nodeTable.addAttribute(new DefaultConfigurationNode("type",
+ tabTypes[i]));
+ DefaultConfigurationNode nodeFields = new DefaultConfigurationNode(
+ "fields");
+ nodeTable.addChild(nodeFields);
+
+ for (int j = 0; j < fields[i].length; j++)
+ {
+ nodeFields.addChild(createFieldNode(fields[i][j]));
+ }
+ }
+
+ DefaultConfigurationNode nodeConn = new DefaultConfigurationNode(
+ "connection.settings");
+ rootNode.addChild(nodeConn);
+ nodeConn.addChild(new DefaultConfigurationNode("usr.name", "scott"));
+ nodeConn.addChild(new DefaultConfigurationNode("usr.pwd", "tiger"));
+ rootNode.addAttribute(new DefaultConfigurationNode("test", "true"));
+
+ return rootNode;
+ }
+
+ /**
+ * Configures the expression engine to use a different syntax.
+ */
+ private void setUpAlternativeSyntax()
+ {
+ engine.setAttributeEnd(null);
+ engine.setAttributeStart("@");
+ engine.setPropertyDelimiter("/");
+ engine.setEscapedDelimiter(null);
+ engine.setIndexStart("[");
+ engine.setIndexEnd("]");
+ }
+
+ /**
+ * Helper method for checking the evaluation of a key. Queries the
+ * expression engine and tests if the expected results are returned.
+ *
+ * @param key the key
+ * @param name the name of the nodes to be returned
+ * @param count the number of expected result nodes
+ * @return the list with the results of the query
+ */
+ private List<ConfigurationNode> checkKey(String key, String name, int count)
+ {
+ List<ConfigurationNode> nodes = engine.query(root, key);
+ assertEquals("Wrong number of result nodes for key " + key, count,
+ nodes.size());
+ for (Iterator<ConfigurationNode> it = nodes.iterator(); it.hasNext();)
+ {
+ assertEquals("Wrong result node for key " + key, name,
+ it.next().getName());
+ }
+ return nodes;
+ }
+
+ /**
+ * Helper method for checking the value of a node specified by the given
+ * key. This method evaluates the key and checks whether the resulting node
+ * has the expected value.
+ *
+ * @param key the key
+ * @param name the expected name of the result node
+ * @param value the expected value of the result node
+ */
+ private void checkKeyValue(String key, String name, String value)
+ {
+ List<ConfigurationNode> nodes = checkKey(key, name, 1);
+ assertEquals("Wrong value for key " + key, value,
+ nodes.get(0).getValue());
+ }
+
+ /**
+ * Helper method for checking the path of an add operation.
+ *
+ * @param data the add data object
+ * @param expected the expected path nodes
+ */
+ private void checkNodePath(NodeAddData data, String[] expected)
+ {
+ assertEquals("Wrong number of path nodes", expected.length, data
+ .getPathNodes().size());
+ Iterator<String> it = data.getPathNodes().iterator();
+ for (int i = 0; i < expected.length; i++)
+ {
+ assertEquals("Wrong path node " + i, expected[i], it.next());
+ }
+ }
+
+ /**
+ * Helper method for creating a field node with its children for the test
+ * node hierarchy.
+ *
+ * @param name the name of the field
+ * @return the field node
+ */
+ private static ConfigurationNode createFieldNode(String name)
+ {
+ DefaultConfigurationNode nodeField = new DefaultConfigurationNode(
+ "field");
+ nodeField.addChild(new DefaultConfigurationNode("name", name));
+ return nodeField;
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/tree/TestMergeCombiner.java b/src/test/java/org/apache/commons/configuration/tree/TestMergeCombiner.java
new file mode 100644
index 0000000..9e2d9de
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/TestMergeCombiner.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+
+import java.util.List;
+
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
+import org.junit.Test;
+
+/**
+ * Test class for MergeCombiner.
+ *
+ * @version $Id: TestMergeCombiner.java 1225911 2011-12-30 20:19:10Z oheger $
+ */
+public class TestMergeCombiner extends AbstractCombinerTest
+{
+ /**
+ * Creates the combiner.
+ *
+ * @return the combiner
+ */
+ @Override
+ protected NodeCombiner createCombiner()
+ {
+ return new MergeCombiner();
+ }
+
+ /**
+ * Tests combination of simple elements.
+ */
+ @Test
+ public void testSimpleValues() throws ConfigurationException
+ {
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ assertEquals("Wrong number of bgcolors", 0, config
+ .getMaxIndex("gui.bgcolor"));
+ assertEquals("Wrong bgcolor", "green", config.getString("gui.bgcolor"));
+ assertEquals("Wrong selcolor", "yellow", config
+ .getString("gui.selcolor"));
+ assertEquals("Wrong fgcolor", "blue", config.getString("gui.fgcolor"));
+ assertEquals("Wrong level", 1, config.getInt("gui.level"));
+ }
+
+ /**
+ * Tests combination of attributes.
+ */
+ @Test
+ public void testAttributes() throws ConfigurationException
+ {
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ assertEquals("Wrong value of min attribute", 1, config
+ .getInt("gui.level[@min]"));
+ assertEquals("Wrong value of default attribute", 2, config
+ .getInt("gui.level[@default]"));
+ assertEquals("Wrong number of id attributes", 0, config
+ .getMaxIndex("database.tables.table(0)[@id]"));
+ assertEquals("Wrong value of table id", 1, config
+ .getInt("database.tables.table(0)[@id]"));
+ }
+
+ /**
+ * Tests whether property values are correctly overridden.
+ */
+ @Test
+ public void testOverrideValues() throws ConfigurationException
+ {
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ assertEquals("Wrong user", "Admin", config
+ .getString("base.services.security.login.user"));
+ assertEquals("Wrong user type", "default", config
+ .getString("base.services.security.login.user[@type]"));
+ assertNull("Wrong password", config.getString("base.services.security.login.passwd"));
+ assertEquals("Wrong password type", "secret", config
+ .getString("base.services.security.login.passwd[@type]"));
+ }
+
+ /**
+ * Tests if a list from the first node structure overrides a list in the
+ * second structure.
+ */
+ @Test
+ public void testListFromFirstStructure() throws ConfigurationException
+ {
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ assertEquals("Wrong number of services", 0, config
+ .getMaxIndex("net.service.url"));
+ assertEquals("Wrong service", "http://service1.org", config
+ .getString("net.service.url"));
+ assertFalse("Type attribute available", config
+ .containsKey("net.service.url[@type]"));
+ }
+
+ /**
+ * Tests if a list from the second structure is added if it is not defined
+ * in the first structure.
+ */
+ @Test
+ public void testListFromSecondStructure() throws ConfigurationException
+ {
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ assertEquals("Wrong number of servers", 3, config
+ .getMaxIndex("net.server.url"));
+ assertEquals("Wrong server", "http://testsvr.com", config
+ .getString("net.server.url(2)"));
+ }
+
+ /**
+ * Tests the combination of the table structure. With the merge combiner
+ * both table 1 and table 2 should be present.
+ */
+ @Test
+ public void testCombinedTable() throws ConfigurationException
+ {
+ checkTable(createCombinedConfiguration());
+ }
+
+ @Test
+ public void testMerge() throws ConfigurationException
+ {
+ //combiner.setDebugStream(System.out);
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ config.setExpressionEngine(new XPathExpressionEngine());
+ assertEquals("Wrong number of Channels", 3, config.getMaxIndex("Channels/Channel"));
+ assertEquals("Bad Channel 1 Name", "My Channel",
+ config.getString("Channels/Channel[@id='1']/Name"));
+ assertEquals("Bad Channel Type", "half",
+ config.getString("Channels/Channel[@id='1']/@type"));
+ assertEquals("Bad Channel 2 Name", "Channel 2",
+ config.getString("Channels/Channel[@id='2']/Name"));
+ assertEquals("Bad Channel Type", "full",
+ config.getString("Channels/Channel[@id='2']/@type"));
+ assertEquals("Bad Channel Data", "test 1 data",
+ config.getString("Channels/Channel[@id='1']/ChannelData"));
+ assertEquals("Bad Channel Data", "test 2 data",
+ config.getString("Channels/Channel[@id='2']/ChannelData"));
+ assertEquals("Bad Channel Data", "more test 2 data",
+ config.getString("Channels/Channel[@id='2']/MoreChannelData"));
+
+ }
+
+ /**
+ * Helper method for checking the combined table structure.
+ *
+ * @param config the config
+ * @return the node for the table element
+ */
+ private ConfigurationNode checkTable(HierarchicalConfiguration config)
+ {
+ assertEquals("Wrong number of tables", 1, config
+ .getMaxIndex("database.tables.table"));
+ HierarchicalConfiguration c = config
+ .configurationAt("database.tables.table(0)");
+ assertEquals("Wrong table name", "documents", c.getString("name"));
+ assertEquals("Wrong number of fields", 2, c
+ .getMaxIndex("fields.field.name"));
+ assertEquals("Wrong field", "docname", c
+ .getString("fields.field(1).name"));
+
+ List<ConfigurationNode> nds = config.getExpressionEngine().query(config.getRoot(),
+ "database.tables.table");
+ assertFalse("No node found", nds.isEmpty());
+ return nds.get(0);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/commons/configuration/tree/TestNodeAddData.java b/src/test/java/org/apache/commons/configuration/tree/TestNodeAddData.java
new file mode 100644
index 0000000..5bec4a0
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/TestNodeAddData.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for NodeAddData.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestNodeAddData.java 1226097 2011-12-31 15:16:02Z oheger $
+ */
+public class TestNodeAddData
+{
+ /** Constant for the default parent node used for testing. */
+ private static final ConfigurationNode TEST_PARENT = new DefaultConfigurationNode(
+ "parent");
+
+ /** Constant for the name of the new node. */
+ private static final String TEST_NODENAME = "testNewNode";
+
+ /** Constant for the name of a path node. */
+ private static final String PATH_NODE_NAME = "PATHNODE";
+
+ /** Constant for the number of path nodes to be added. */
+ private static final int PATH_NODE_COUNT = 10;
+
+ /** The object to be tested. */
+ NodeAddData addData;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ addData = new NodeAddData(TEST_PARENT, TEST_NODENAME);
+ }
+
+ /**
+ * Tests the default values of an uninitialized instance.
+ */
+ @Test
+ public void testUninitialized()
+ {
+ addData = new NodeAddData();
+ assertNull("A parent is set", addData.getParent());
+ assertNull("Node has a name", addData.getNewNodeName());
+ assertFalse("Attribute flag is set", addData.isAttribute());
+ assertTrue("Path nodes are not empty", addData.getPathNodes().isEmpty());
+ }
+
+ /**
+ * Tests the constructor that initializes the most important fields.
+ */
+ @Test
+ public void testInitialized()
+ {
+ assertSame("Wrong parent", TEST_PARENT, addData.getParent());
+ assertEquals("Wrong node name", TEST_NODENAME, addData.getNewNodeName());
+ assertFalse("Attribute flag is set", addData.isAttribute());
+ assertTrue("Path nodes are not empty", addData.getPathNodes().isEmpty());
+ }
+
+ /**
+ * Tests adding path nodes.
+ */
+ @Test
+ public void testAddPathNode()
+ {
+ for (int i = 0; i < PATH_NODE_COUNT; i++)
+ {
+ addData.addPathNode(PATH_NODE_NAME + i);
+ }
+
+ List<String> nodes = addData.getPathNodes();
+ assertEquals("Incorrect number of path nodes", PATH_NODE_COUNT, nodes
+ .size());
+ for (int i = 0; i < PATH_NODE_COUNT; i++)
+ {
+ assertEquals("Wrong path node at position" + i, PATH_NODE_NAME + i,
+ nodes.get(i));
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/tree/TestOverrideCombiner.java b/src/test/java/org/apache/commons/configuration/tree/TestOverrideCombiner.java
new file mode 100644
index 0000000..6d41ed5
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/TestOverrideCombiner.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.junit.Test;
+
+/**
+ * Test class for OverrideCombiner.
+ *
+ * @version $Id: TestOverrideCombiner.java 1225911 2011-12-30 20:19:10Z oheger $
+ */
+public class TestOverrideCombiner extends AbstractCombinerTest
+{
+ /**
+ * Creates the combiner.
+ *
+ * @return the combiner
+ */
+ @Override
+ protected NodeCombiner createCombiner()
+ {
+ return new OverrideCombiner();
+ }
+
+ /**
+ * Tests combination of simple elements.
+ */
+ @Test
+ public void testSimpleValues() throws ConfigurationException
+ {
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ assertEquals("Wrong number of bgcolors", 0, config
+ .getMaxIndex("gui.bgcolor"));
+ assertEquals("Wrong bgcolor", "green", config.getString("gui.bgcolor"));
+ assertEquals("Wrong selcolor", "yellow", config
+ .getString("gui.selcolor"));
+ assertEquals("Wrong fgcolor", "blue", config.getString("gui.fgcolor"));
+ assertEquals("Wrong level", 1, config.getInt("gui.level"));
+ }
+
+ /**
+ * Tests combination of attributes.
+ */
+ @Test
+ public void testAttributes() throws ConfigurationException
+ {
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ assertEquals("Wrong value of min attribute", 1, config
+ .getInt("gui.level[@min]"));
+ assertEquals("Wrong value of default attribute", 2, config
+ .getInt("gui.level[@default]"));
+ assertEquals("Wrong number of id attributes", 0, config
+ .getMaxIndex("database.tables.table(0)[@id]"));
+ assertEquals("Wrong value of table id", 1, config
+ .getInt("database.tables.table(0)[@id]"));
+ }
+
+ /**
+ * Tests whether property values are correctly overridden.
+ */
+ @Test
+ public void testOverrideValues() throws ConfigurationException
+ {
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ assertEquals("Wrong user", "Admin", config
+ .getString("base.services.security.login.user"));
+ assertEquals("Wrong user type", "default", config
+ .getString("base.services.security.login.user[@type]"));
+ assertEquals("Wrong password", "BeamMeUp", config
+ .getString("base.services.security.login.passwd"));
+ assertEquals("Wrong password type", "secret", config
+ .getString("base.services.security.login.passwd[@type]"));
+ }
+
+ /**
+ * Tests if a list from the first node structure overrides a list in the
+ * second structure.
+ */
+ @Test
+ public void testListFromFirstStructure() throws ConfigurationException
+ {
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ assertEquals("Wrong number of services", 0, config
+ .getMaxIndex("net.service.url"));
+ assertEquals("Wrong service", "http://service1.org", config
+ .getString("net.service.url"));
+ assertFalse("Type attribute available", config
+ .containsKey("net.service.url[@type]"));
+ }
+
+ /**
+ * Tests if a list from the second structure is added if it is not defined
+ * in the first structure.
+ */
+ @Test
+ public void testListFromSecondStructure() throws ConfigurationException
+ {
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ assertEquals("Wrong number of servers", 3, config
+ .getMaxIndex("net.server.url"));
+ assertEquals("Wrong server", "http://testsvr.com", config
+ .getString("net.server.url(2)"));
+ }
+
+ /**
+ * Tests the combination of the table structure. Because the table node is
+ * not declared as a list node the structures will be combined. But this
+ * won't make any difference because the values in the first table override
+ * the values in the second table. Only the node for the table element will
+ * be a ViewNode.
+ */
+ @Test
+ public void testCombinedTableNoList() throws ConfigurationException
+ {
+ ConfigurationNode tabNode = checkTable(createCombinedConfiguration());
+ assertTrue("Node is not a view node", tabNode instanceof ViewNode);
+ }
+
+ /**
+ * Tests the combination of the table structure when the table node is
+ * declared as a list node. In this case the first table structure
+ * completely overrides the second and will be directly added to the
+ * resulting structure.
+ */
+ @Test
+ public void testCombinedTableList() throws ConfigurationException
+ {
+ combiner.addListNode("table");
+ ConfigurationNode tabNode = checkTable(createCombinedConfiguration());
+ assertFalse("Node is a view node", tabNode instanceof ViewNode);
+ }
+
+ /**
+ * Helper method for checking the combined table structure.
+ *
+ * @param config the config
+ * @return the node for the table element
+ */
+ private ConfigurationNode checkTable(HierarchicalConfiguration config)
+ {
+ assertEquals("Wrong number of tables", 0, config
+ .getMaxIndex("database.tables.table"));
+ HierarchicalConfiguration c = config
+ .configurationAt("database.tables.table");
+ assertEquals("Wrong table name", "documents", c.getString("name"));
+ assertEquals("Wrong number of fields", 2, c
+ .getMaxIndex("fields.field.name"));
+ assertEquals("Wrong field", "docname", c
+ .getString("fields.field(1).name"));
+
+ List<ConfigurationNode> nds = config.getExpressionEngine().query(config.getRoot(),
+ "database.tables.table");
+ assertFalse("No node found", nds.isEmpty());
+ return nds.get(0);
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/tree/TestUnionCombiner.java b/src/test/java/org/apache/commons/configuration/tree/TestUnionCombiner.java
new file mode 100644
index 0000000..8bf503b
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/TestUnionCombiner.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.junit.Test;
+
+/**
+ * Test class for UnionCombiner.
+ *
+ * @version $Id: TestUnionCombiner.java 1225911 2011-12-30 20:19:10Z oheger $
+ */
+public class TestUnionCombiner extends AbstractCombinerTest
+{
+ /**
+ * Creates the combiner.
+ *
+ * @return the combiner
+ */
+ @Override
+ protected NodeCombiner createCombiner()
+ {
+ return new UnionCombiner();
+ }
+
+ /**
+ * Tests combination of simple values (no lists).
+ */
+ @Test
+ public void testSimpleValues() throws ConfigurationException
+ {
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ assertEquals("Too few bgcolors", 1, config.getMaxIndex("gui.bgcolor"));
+ assertEquals("Wrong first color", "green", config
+ .getString("gui.bgcolor(0)"));
+ assertEquals("Wrong second color", "black", config
+ .getString("gui.bgcolor(1)"));
+ assertEquals("Wrong number of selcolors", 0, config
+ .getMaxIndex("gui.selcolor"));
+ assertEquals("Wrong selcolor", "yellow", config
+ .getString("gui.selcolor"));
+ }
+
+ /**
+ * Tests combinations of elements with attributes.
+ */
+ @Test
+ public void testSimpleValuesWithAttributes() throws ConfigurationException
+ {
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ assertEquals("Too few level elements", 1, config
+ .getMaxIndex("gui.level"));
+ assertEquals("Wrong value of first element", 1, config
+ .getInt("gui.level(0)"));
+ assertEquals("Wrong value of second element", 4, config
+ .getInt("gui.level(1)"));
+ assertEquals("Wrong value of first attribute", 2, config
+ .getInt("gui.level(0)[@default]"));
+ assertFalse("Found wrong attribute", config
+ .containsKey("gui.level(0)[@min]"));
+ assertEquals("Wrong value of second attribute", 1, config
+ .getInt("gui.level(1)[@min]"));
+ }
+
+ /**
+ * Tests combination of attributes.
+ */
+ @Test
+ public void testAttributes() throws ConfigurationException
+ {
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ assertEquals("Too few attributes", 1, config
+ .getMaxIndex("database.tables.table(0)[@id]"));
+ assertEquals("Wrong value of first attribute", 1, config
+ .getInt("database.tables.table(0)[@id](0)"));
+ assertEquals("Wrong value of second attribute", 2, config
+ .getInt("database.tables.table(0)[@id](1)"));
+ }
+
+ /**
+ * Tests combination of lists.
+ */
+ @Test
+ public void testLists() throws ConfigurationException
+ {
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ assertEquals("Too few list elements", 2, config
+ .getMaxIndex("net.service.url"));
+ assertEquals("Wrong first service", "http://service1.org", config
+ .getString("net.service.url(0)"));
+ assertEquals("Wrong second service", "http://service2.org", config
+ .getString("net.service.url(1)"));
+ assertEquals("Wrong service attribute", 2, config
+ .getInt("net.service.url(2)[@type]"));
+ assertEquals("Wrong number of server elements", 3, config
+ .getMaxIndex("net.server.url"));
+ }
+
+ /**
+ * Tests combining a list of tables. Per default the table elements will be
+ * combined. But if they are defined as list elements, the resulting tree
+ * should contain two table nodes.
+ */
+ @Test
+ public void testTableList() throws ConfigurationException
+ {
+ combiner.addListNode("table");
+ HierarchicalConfiguration config = createCombinedConfiguration();
+ assertEquals("Wrong name of first table", "documents", config
+ .getString("database.tables.table(0).name"));
+ assertEquals("Wrong id of first table", 1, config
+ .getInt("database.tables.table(0)[@id]"));
+ assertEquals("Wrong name of second table", "tasks", config
+ .getString("database.tables.table(1).name"));
+ assertEquals("Wrong id of second table", 2, config
+ .getInt("database.tables.table(1)[@id]"));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/tree/TestViewNode.java b/src/test/java/org/apache/commons/configuration/tree/TestViewNode.java
new file mode 100644
index 0000000..4d10e91
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/TestViewNode.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for ViewNode.
+ *
+ * @version $Id: TestViewNode.java 1226098 2011-12-31 15:18:45Z oheger $
+ */
+public class TestViewNode
+{
+ /** Stores the view node to be tested. */
+ ViewNode viewNode;
+
+ /** Stores a regular node. */
+ ConfigurationNode node;
+
+ /** A child node of the regular node. */
+ ConfigurationNode child;
+
+ /** An attribute node of the regular node. */
+ ConfigurationNode attr;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ node = new DefaultConfigurationNode();
+ child = new DefaultConfigurationNode("child");
+ attr = new DefaultConfigurationNode("attr");
+ node.addChild(child);
+ node.addAttribute(attr);
+ viewNode = new ViewNode();
+ }
+
+ /**
+ * Tests adding a child to the view node.
+ */
+ @Test
+ public void testAddChild()
+ {
+ viewNode.addChild(child);
+ assertEquals("Parent was changed", node, child.getParentNode());
+ assertEquals("Child was not added", 1, viewNode.getChildrenCount());
+ }
+
+ /**
+ * Tests adding a null child to the view node. This should throw an
+ * exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddNullChild()
+ {
+ viewNode.addChild(null);
+ }
+
+ /**
+ * Tests adding an attribute to the view node.
+ */
+ @Test
+ public void testAddAttribute()
+ {
+ viewNode.addAttribute(attr);
+ assertEquals("Parent was changed", node, attr.getParentNode());
+ assertEquals("Attribute was not added", 1, viewNode.getAttributeCount());
+ }
+
+ /**
+ * Tests adding a null attribute to the view node. This should cause an
+ * exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddNullAttribute()
+ {
+ viewNode.addAttribute(null);
+ }
+
+ /**
+ * Tests appending all children to a view node.
+ */
+ @Test
+ public void testAppendChildren()
+ {
+ viewNode.addChild(new DefaultConfigurationNode("testNode"));
+ viewNode.appendChildren(node);
+ assertEquals("Wrong number of children", 2, viewNode.getChildrenCount());
+ assertEquals("Cannot find child", child, viewNode.getChild(1));
+ assertEquals("Parent was changed", node, viewNode
+ .getChild(1).getParentNode());
+ }
+
+ /**
+ * Tests appending children from a null source. This should be a noop.
+ */
+ @Test
+ public void testAppendNullChildren()
+ {
+ viewNode.appendChildren(null);
+ assertEquals("Wrong number of children", 0, viewNode.getChildrenCount());
+ }
+
+ /**
+ * tests appending all attributes to a view node.
+ */
+ @Test
+ public void testAppendAttributes()
+ {
+ viewNode.appendAttributes(node);
+ assertEquals("Wrong number of attributes", 1, viewNode
+ .getAttributeCount());
+ assertEquals("Cannot find attribute", attr, viewNode.getAttribute(0));
+ assertEquals("Parent was changed", node, viewNode
+ .getAttribute(0).getParentNode());
+ }
+
+ /**
+ * Tests appending attributes from a null source. This should be a noop.
+ */
+ @Test
+ public void testAppendNullAttributes()
+ {
+ viewNode.appendAttributes(null);
+ assertEquals("Wrong number of attributes", 0, viewNode
+ .getAttributeCount());
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/tree/xpath/AbstractXPathTest.java b/src/test/java/org/apache/commons/configuration/tree/xpath/AbstractXPathTest.java
new file mode 100644
index 0000000..7516df3
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/xpath/AbstractXPathTest.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree.xpath;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.DefaultConfigurationNode;
+import org.apache.commons.jxpath.ri.model.NodeIterator;
+import org.junit.After;
+import org.junit.Before;
+
+/**
+ * A base class for testing classes of the XPath package. This base class
+ * creates a hierarchy of nodes in its setUp() method that can be used for test
+ * cases.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: AbstractXPathTest.java 1226104 2011-12-31 15:37:16Z oheger $
+ */
+public abstract class AbstractXPathTest
+{
+ /** Constant for the name of the counter attribute. */
+ protected static final String ATTR_NAME = "counter";
+
+ /** Constant for the name of the first child. */
+ protected static final String CHILD_NAME1 = "subNode";
+
+ /** Constant for the name of the second child. */
+ protected static final String CHILD_NAME2 = "childNode";
+
+ /** Constant for the number of sub nodes. */
+ protected static final int CHILD_COUNT = 5;
+
+ /** Constant for the number of levels in the hierarchy. */
+ protected static final int LEVEL_COUNT = 3;
+
+ /** Stores the root node of the hierarchy. */
+ protected ConfigurationNode root;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ root = constructHierarchy(LEVEL_COUNT);
+ }
+
+ /**
+ * Clears the test environment.
+ */
+ @After
+ public void tearDown() throws Exception
+ {
+ root = null;
+ }
+
+ /**
+ * Builds up a hierarchy of nodes. Each node has {@code CHILD_COUNT}
+ * child nodes having the names {@code CHILD_NAME1} or
+ * {@code CHILD_NAME2}. Their values are named like their parent
+ * node with an additional index. Each node has an attribute with a counter
+ * value.
+ *
+ * @param levels the number of levels in the hierarchy
+ * @return the root node of the hierarchy
+ */
+ protected ConfigurationNode constructHierarchy(int levels)
+ {
+ ConfigurationNode result = new DefaultConfigurationNode();
+ createLevel(result, levels);
+ return result;
+ }
+
+ /**
+ * Determines the number of elements contained in the given iterator.
+ *
+ * @param iterator the iterator
+ * @return the number of elements in this iteration
+ */
+ protected int iteratorSize(NodeIterator iterator)
+ {
+ int cnt = 0;
+ boolean ok;
+
+ do
+ {
+ ok = iterator.setPosition(cnt + 1);
+ if (ok)
+ {
+ cnt++;
+ }
+ } while (ok);
+
+ return cnt;
+ }
+
+ /**
+ * Returns a list with all configuration nodes contained in the specified
+ * iteration. It is assumed that the iteration contains only elements of
+ * this type.
+ *
+ * @param iterator the iterator
+ * @return a list with configuration nodes obtained from the iterator
+ */
+ protected List<ConfigurationNode> iterationElements(NodeIterator iterator)
+ {
+ List<ConfigurationNode> result = new ArrayList<ConfigurationNode>();
+ for (int pos = 1; iterator.setPosition(pos); pos++)
+ {
+ result.add((ConfigurationNode) iterator.getNodePointer().getNode());
+ }
+ return result;
+ }
+
+ /**
+ * Recursive helper method for creating a level of the node hierarchy.
+ *
+ * @param parent the parent node
+ * @param level the level counter
+ */
+ private void createLevel(ConfigurationNode parent, int level)
+ {
+ if (level >= 0)
+ {
+ String prefix = (parent.getValue() == null) ? "" : parent
+ .getValue()
+ + ".";
+ for (int i = 1; i <= CHILD_COUNT; i++)
+ {
+ ConfigurationNode child = new DefaultConfigurationNode(
+ (i % 2 == 0) ? CHILD_NAME1 : CHILD_NAME2, prefix + i);
+ parent.addChild(child);
+ child.addAttribute(new DefaultConfigurationNode(ATTR_NAME,
+ String.valueOf(i)));
+
+ createLevel(child, level - 1);
+ }
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/tree/xpath/TestConfigurationIteratorAttributes.java b/src/test/java/org/apache/commons/configuration/tree/xpath/TestConfigurationIteratorAttributes.java
new file mode 100644
index 0000000..e20787d
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/xpath/TestConfigurationIteratorAttributes.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree.xpath;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.List;
+import java.util.Locale;
+
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.DefaultConfigurationNode;
+import org.apache.commons.jxpath.ri.QName;
+import org.apache.commons.jxpath.ri.model.NodePointer;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for ConfigurationIteratorAttributes.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestConfigurationIteratorAttributes.java 1226104 2011-12-31 15:37:16Z oheger $
+ */
+public class TestConfigurationIteratorAttributes extends AbstractXPathTest
+{
+ /** Constant for the name of another test attribute.*/
+ private static final String TEST_ATTR = "test";
+
+ /** Stores the node pointer of the test node.*/
+ NodePointer pointer;
+
+ @Override
+ @Before
+ public void setUp() throws Exception
+ {
+ super.setUp();
+
+ // Adds further attributes to the test node
+ ConfigurationNode testNode = root.getChild(1);
+ testNode.addAttribute(new DefaultConfigurationNode(TEST_ATTR, "yes"));
+ pointer = new ConfigurationNodePointer(testNode, Locale.getDefault());
+ }
+
+ /**
+ * Tests to iterate over all attributes.
+ */
+ @Test
+ public void testIterateAllAttributes()
+ {
+ ConfigurationNodeIteratorAttribute it = new ConfigurationNodeIteratorAttribute(pointer, new QName(null, "*"));
+ assertEquals("Wrong number of attributes", 2, iteratorSize(it));
+ List<ConfigurationNode> attrs = iterationElements(it);
+ assertEquals("Wrong first attribute", ATTR_NAME, attrs.get(0).getName());
+ assertEquals("Wrong first attribute", TEST_ATTR, attrs.get(1).getName());
+ }
+
+ /**
+ * Tests to iterate over attributes with a specific name.
+ */
+ @Test
+ public void testIterateSpecificAttribute()
+ {
+ ConfigurationNodeIteratorAttribute it = new ConfigurationNodeIteratorAttribute(pointer, new QName(null, TEST_ATTR));
+ assertEquals("Wrong number of attributes", 1, iteratorSize(it));
+ assertEquals("Wrong attribute", TEST_ATTR, iterationElements(it).get(0).getName());
+ }
+
+ /**
+ * Tests to iterate over non existing attributes.
+ */
+ @Test
+ public void testIterateUnknownAttribute()
+ {
+ ConfigurationNodeIteratorAttribute it = new ConfigurationNodeIteratorAttribute(pointer, new QName(null, "unknown"));
+ assertEquals("Found attributes", 0, iteratorSize(it));
+ }
+
+ /**
+ * Tests iteration when a namespace is specified. This is not supported, so
+ * the iteration should be empty.
+ */
+ @Test
+ public void testIterateNamespace()
+ {
+ ConfigurationNodeIteratorAttribute it = new ConfigurationNodeIteratorAttribute(pointer, new QName("test", "*"));
+ assertEquals("Found attributes", 0, iteratorSize(it));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/tree/xpath/TestConfigurationNodeIteratorChildren.java b/src/test/java/org/apache/commons/configuration/tree/xpath/TestConfigurationNodeIteratorChildren.java
new file mode 100644
index 0000000..01431a6
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/xpath/TestConfigurationNodeIteratorChildren.java
@@ -0,0 +1,247 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree.xpath;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.DefaultConfigurationNode;
+import org.apache.commons.jxpath.ri.Compiler;
+import org.apache.commons.jxpath.ri.QName;
+import org.apache.commons.jxpath.ri.compiler.NodeNameTest;
+import org.apache.commons.jxpath.ri.compiler.NodeTest;
+import org.apache.commons.jxpath.ri.compiler.NodeTypeTest;
+import org.apache.commons.jxpath.ri.compiler.ProcessingInstructionTest;
+import org.apache.commons.jxpath.ri.model.NodeIterator;
+import org.apache.commons.jxpath.ri.model.NodePointer;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for ConfigurationNodeIteratorChildren.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestConfigurationNodeIteratorChildren.java 1226104 2011-12-31 15:37:16Z oheger $
+ */
+public class TestConfigurationNodeIteratorChildren extends AbstractXPathTest
+{
+ /** Stores the node pointer to the root node. */
+ NodePointer rootPointer;
+
+ @Override
+ @Before
+ public void setUp() throws Exception
+ {
+ super.setUp();
+ rootPointer = new ConfigurationNodePointer(root, Locale.getDefault());
+ }
+
+ /**
+ * Tests to iterate over all children of the root node.
+ */
+ @Test
+ public void testIterateAllChildren()
+ {
+ ConfigurationNodeIteratorChildren it = new ConfigurationNodeIteratorChildren(
+ rootPointer, null, false, null);
+ assertEquals("Wrong number of elements", CHILD_COUNT, iteratorSize(it));
+ checkValues(it, new int[]
+ { 1, 2, 3, 4, 5 });
+ }
+
+ /**
+ * Tests a reverse iteration.
+ */
+ @Test
+ public void testIterateReverse()
+ {
+ ConfigurationNodeIteratorChildren it = new ConfigurationNodeIteratorChildren(
+ rootPointer, null, true, null);
+ assertEquals("Wrong number of elements", CHILD_COUNT, iteratorSize(it));
+ checkValues(it, new int[]
+ { 5, 4, 3, 2, 1 });
+ }
+
+ /**
+ * Tests using a node test with a wildcard name.
+ */
+ @Test
+ public void testIterateWithWildcardTest()
+ {
+ NodeNameTest test = new NodeNameTest(new QName(null, "*"));
+ ConfigurationNodeIteratorChildren it = new ConfigurationNodeIteratorChildren(
+ rootPointer, test, false, null);
+ assertEquals("Wrong number of elements", CHILD_COUNT, iteratorSize(it));
+ }
+
+ /**
+ * Tests using a node test that defines a namespace prefix. Because
+ * namespaces are not supported, no elements should be in the iteration.
+ */
+ @Test
+ public void testIterateWithPrefixTest()
+ {
+ NodeNameTest test = new NodeNameTest(new QName("prefix", "*"));
+ ConfigurationNodeIteratorChildren it = new ConfigurationNodeIteratorChildren(
+ rootPointer, test, false, null);
+ assertNull("Undefined node pointer not returned", it.getNodePointer());
+ assertEquals("Prefix was not evaluated", 0, iteratorSize(it));
+ }
+
+ /**
+ * Tests using a node test that selects a certain sub node name.
+ */
+ @Test
+ public void testIterateWithNameTest()
+ {
+ NodeNameTest test = new NodeNameTest(new QName(null, CHILD_NAME2));
+ ConfigurationNodeIteratorChildren it = new ConfigurationNodeIteratorChildren(
+ rootPointer, test, false, null);
+ assertTrue("No children found", iteratorSize(it) > 0);
+ for (ConfigurationNode nd : iterationElements(it))
+ {
+ assertEquals("Wrong child element", CHILD_NAME2, nd.getName());
+ }
+ }
+
+ /**
+ * Tests using a not supported test class. This should yield an empty
+ * iteration.
+ */
+ @Test
+ public void testIterateWithUnknownTest()
+ {
+ NodeTest test = new ProcessingInstructionTest("test");
+ ConfigurationNodeIteratorChildren it = new ConfigurationNodeIteratorChildren(
+ rootPointer, test, false, null);
+ assertEquals("Unknown test was not evaluated", 0, iteratorSize(it));
+ }
+
+ /**
+ * Tests using a type test for nodes. This should return all nodes.
+ */
+ @Test
+ public void testIterateWithNodeType()
+ {
+ NodeTypeTest test = new NodeTypeTest(Compiler.NODE_TYPE_NODE);
+ ConfigurationNodeIteratorChildren it = new ConfigurationNodeIteratorChildren(
+ rootPointer, test, false, null);
+ assertEquals("Node type not evaluated", CHILD_COUNT, iteratorSize(it));
+ }
+
+ /**
+ * Tests using a type test for a non supported type. This should return an
+ * empty iteration.
+ */
+ @Test
+ public void testIterateWithUnknownType()
+ {
+ NodeTypeTest test = new NodeTypeTest(Compiler.NODE_TYPE_COMMENT);
+ ConfigurationNodeIteratorChildren it = new ConfigurationNodeIteratorChildren(
+ rootPointer, test, false, null);
+ assertEquals("Unknown node type not evaluated", 0, iteratorSize(it));
+ }
+
+ /**
+ * Tests defining a start node for the iteration.
+ */
+ @Test
+ public void testIterateStartsWith()
+ {
+ NodePointer childPointer = new ConfigurationNodePointer(rootPointer,
+ root.getChild(2));
+ ConfigurationNodeIteratorChildren it = new ConfigurationNodeIteratorChildren(
+ rootPointer, null, false, childPointer);
+ assertEquals("Wrong start position", 0, it.getPosition());
+ List<ConfigurationNode> nodes = iterationElements(it);
+ assertEquals("Wrong size of iteration", CHILD_COUNT - 3, nodes.size());
+ int index = 4;
+ for (Iterator<ConfigurationNode> it2 = nodes.iterator(); it2.hasNext(); index++)
+ {
+ ConfigurationNode node = it2.next();
+ assertEquals("Wrong node value", String.valueOf(index), node
+ .getValue());
+ }
+ }
+
+ /**
+ * Tests defining a start node for a reverse iteration.
+ */
+ @Test
+ public void testIterateStartsWithReverse()
+ {
+ NodePointer childPointer = new ConfigurationNodePointer(rootPointer,
+ root.getChild(3));
+ ConfigurationNodeIteratorChildren it = new ConfigurationNodeIteratorChildren(
+ rootPointer, null, true, childPointer);
+ int value = 3;
+ for (int index = 1; it.setPosition(index); index++, value--)
+ {
+ ConfigurationNode node = (ConfigurationNode) it.getNodePointer()
+ .getNode();
+ assertEquals("Incorrect value at index " + index, String
+ .valueOf(value), node.getValue());
+ }
+ assertEquals("Iteration ended not at end node", 0, value);
+ }
+
+ /**
+ * Tests iteration with an invalid start node. This should cause the
+ * iteration to start at the first position.
+ */
+ @Test
+ public void testIterateStartsWithInvalid()
+ {
+ NodePointer childPointer = new ConfigurationNodePointer(rootPointer,
+ new DefaultConfigurationNode("newNode"));
+ ConfigurationNodeIteratorChildren it = new ConfigurationNodeIteratorChildren(
+ rootPointer, null, false, childPointer);
+ assertEquals("Wrong size of iteration", CHILD_COUNT, iteratorSize(it));
+ it.setPosition(1);
+ ConfigurationNode node = (ConfigurationNode) it.getNodePointer()
+ .getNode();
+ assertEquals("Wrong start node", "1", node.getValue());
+ }
+
+ /**
+ * Helper method for checking the values of the nodes returned by an
+ * iterator. Because the values indicate the order of the child nodes with
+ * this test it can be checked whether the nodes were returned in the
+ * correct order.
+ *
+ * @param iterator the iterator
+ * @param expectedIndices an array with the expected indices
+ */
+ private void checkValues(NodeIterator iterator, int[] expectedIndices)
+ {
+ List<ConfigurationNode> nodes = iterationElements(iterator);
+ for (int i = 0; i < expectedIndices.length; i++)
+ {
+ ConfigurationNode child = nodes.get(i);
+ assertTrue("Wrong index value for child " + i, child.getValue()
+ .toString().endsWith(String.valueOf(expectedIndices[i])));
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/tree/xpath/TestConfigurationNodePointer.java b/src/test/java/org/apache/commons/configuration/tree/xpath/TestConfigurationNodePointer.java
new file mode 100644
index 0000000..8b300f4
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/xpath/TestConfigurationNodePointer.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree.xpath;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Locale;
+
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.DefaultConfigurationNode;
+import org.apache.commons.jxpath.ri.QName;
+import org.apache.commons.jxpath.ri.model.NodeIterator;
+import org.apache.commons.jxpath.ri.model.NodePointer;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for ConfigurationNodePointer.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestConfigurationNodePointer.java 1226104 2011-12-31 15:37:16Z oheger $
+ */
+public class TestConfigurationNodePointer extends AbstractXPathTest
+{
+ /** Stores the node pointer to be tested. */
+ NodePointer pointer;
+
+ @Override
+ @Before
+ public void setUp() throws Exception
+ {
+ super.setUp();
+ pointer = new ConfigurationNodePointer(root, Locale.getDefault());
+ }
+
+ /**
+ * Tests comparing child node pointers for child nodes.
+ */
+ @Test
+ public void testCompareChildNodePointersChildren()
+ {
+ NodePointer p1 = new ConfigurationNodePointer(pointer, root.getChild(1));
+ NodePointer p2 = new ConfigurationNodePointer(pointer, root.getChild(3));
+ assertEquals("Incorrect order", -1, pointer.compareChildNodePointers(
+ p1, p2));
+ assertEquals("Incorrect symmetric order", 1, pointer
+ .compareChildNodePointers(p2, p1));
+ }
+
+ /**
+ * Tests comparing child node pointers for attribute nodes.
+ */
+ @Test
+ public void testCompareChildNodePointersAttributes()
+ {
+ root.addAttribute(new DefaultConfigurationNode("attr1", "test1"));
+ root.addAttribute(new DefaultConfigurationNode("attr2", "test2"));
+ NodePointer p1 = new ConfigurationNodePointer(pointer, root
+ .getAttribute(0));
+ NodePointer p2 = new ConfigurationNodePointer(pointer, root
+ .getAttribute(1));
+ assertEquals("Incorrect order", -1, pointer.compareChildNodePointers(
+ p1, p2));
+ assertEquals("Incorrect symmetric order", 1, pointer
+ .compareChildNodePointers(p2, p1));
+ }
+
+ /**
+ * tests comparing child node pointers for both child and attribute nodes.
+ */
+ @Test
+ public void testCompareChildNodePointersChildAndAttribute()
+ {
+ root.addAttribute(new DefaultConfigurationNode("attr1", "test1"));
+ NodePointer p1 = new ConfigurationNodePointer(pointer, root.getChild(2));
+ NodePointer p2 = new ConfigurationNodePointer(pointer, root
+ .getAttribute(0));
+ assertEquals("Incorrect order for attributes", 1, pointer
+ .compareChildNodePointers(p1, p2));
+ assertEquals("Incorrect symmetric order for attributes", -1, pointer
+ .compareChildNodePointers(p2, p1));
+ }
+
+ /**
+ * Tests comparing child node pointers for child nodes that do not belong to
+ * the parent node.
+ */
+ @Test
+ public void testCompareChildNodePointersInvalidChildren()
+ {
+ ConfigurationNode node = root.getChild(1);
+ NodePointer p1 = new ConfigurationNodePointer(pointer, node.getChild(1));
+ NodePointer p2 = new ConfigurationNodePointer(pointer, node.getChild(3));
+ assertEquals("Non child nodes could be sorted", 0, pointer
+ .compareChildNodePointers(p1, p2));
+ assertEquals("Non child nodes could be sorted symmetrically", 0,
+ pointer.compareChildNodePointers(p2, p1));
+ }
+
+ /**
+ * Tests the attribute flag.
+ */
+ @Test
+ public void testIsAttribute()
+ {
+ ConfigurationNode node = new DefaultConfigurationNode("test", "testval");
+ NodePointer p = new ConfigurationNodePointer(pointer, node);
+ assertFalse("Node is an attribute", p.isAttribute());
+ node.setAttribute(true);
+ assertTrue("Node is no attribute", p.isAttribute());
+ }
+
+ /**
+ * Tests if leaves in the tree are correctly detected.
+ */
+ @Test
+ public void testIsLeave()
+ {
+ assertFalse("Root node is leaf", pointer.isLeaf());
+
+ NodePointer p = pointer;
+ while (!p.isLeaf())
+ {
+ ConfigurationNode node = (ConfigurationNode) p.getNode();
+ assertTrue("Node has no children", node.getChildrenCount() > 0);
+ p = new ConfigurationNodePointer(p, node.getChild(0));
+ }
+ assertTrue("Node has children", ((ConfigurationNode) p.getNode())
+ .getChildrenCount() == 0);
+ }
+
+ /**
+ * Tests the iterators returned by the node pointer.
+ */
+ @Test
+ public void testIterators()
+ {
+ checkIterators(pointer);
+ }
+
+ /**
+ * Recursive helper method for testing the returned iterators.
+ *
+ * @param p the node pointer to test
+ */
+ private void checkIterators(NodePointer p)
+ {
+ ConfigurationNode node = (ConfigurationNode) p.getNode();
+ NodeIterator it = p.childIterator(null, false, null);
+ assertEquals("Iterator count differs from children count", node
+ .getChildrenCount(), iteratorSize(it));
+
+ for (int index = 1; it.setPosition(index); index++)
+ {
+ NodePointer pchild = it.getNodePointer();
+ assertEquals("Wrong child", node.getChild(index - 1), pchild
+ .getNode());
+ checkIterators(pchild);
+ }
+
+ it = p.attributeIterator(new QName(null, "*"));
+ assertEquals("Iterator count differs from attribute count", node
+ .getAttributeCount(), iteratorSize(it));
+ for (int index = 1; it.setPosition(index); index++)
+ {
+ NodePointer pattr = it.getNodePointer();
+ assertTrue("Node pointer is no attribute", pattr.isAttribute());
+ assertEquals("Wrong attribute", node.getAttribute(index - 1), pattr
+ .getNode());
+ checkIterators(pattr);
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/tree/xpath/TestConfigurationNodePointerFactory.java b/src/test/java/org/apache/commons/configuration/tree/xpath/TestConfigurationNodePointerFactory.java
new file mode 100644
index 0000000..34074b4
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/xpath/TestConfigurationNodePointerFactory.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree.xpath;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.DefaultConfigurationNode;
+import org.apache.commons.jxpath.JXPathContext;
+import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for ConfigurationNodePointerFactory. This class does not directly
+ * call the factory's methods, but rather checks if it can be installed in a
+ * {@code JXPathContext} and if XPath expressions can be evaluated.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestConfigurationNodePointerFactory.java 1226104 2011-12-31 15:37:16Z oheger $
+ */
+public class TestConfigurationNodePointerFactory extends AbstractXPathTest
+{
+ /** Stores the JXPathContext used for testing. */
+ JXPathContext context;
+
+ @Override
+ @Before
+ public void setUp() throws Exception
+ {
+ super.setUp();
+ JXPathContextReferenceImpl
+ .addNodePointerFactory(new ConfigurationNodePointerFactory());
+ context = JXPathContext.newContext(root);
+ context.setLenient(true);
+ }
+
+ /**
+ * Tests simple XPath expressions.
+ */
+ @Test
+ public void testSimpleXPath()
+ {
+ List<?> nodes = context.selectNodes(CHILD_NAME1);
+ assertEquals("Incorrect number of results", 2, nodes.size());
+ for (Iterator<?> it = nodes.iterator(); it.hasNext();)
+ {
+ ConfigurationNode node = (ConfigurationNode) it.next();
+ assertEquals("Incorrect node name", CHILD_NAME1, node.getName());
+ assertEquals("Incorrect parent node", root, node.getParentNode());
+ }
+
+ nodes = context.selectNodes("/" + CHILD_NAME1);
+ assertEquals("Incorrect number of results", 2, nodes.size());
+
+ nodes = context.selectNodes(CHILD_NAME2 + "/" + CHILD_NAME1 + "/"
+ + CHILD_NAME2);
+ assertEquals("Incorrect number of results", 18, nodes.size());
+ }
+
+ /**
+ * Tests using indices to specify elements.
+ */
+ @Test
+ public void testIndices()
+ {
+ assertEquals("Incorrect value", "1.2.3", context.getValue("/"
+ + CHILD_NAME2 + "[1]/" + CHILD_NAME1 + "[1]/" + CHILD_NAME2
+ + "[2]"));
+ assertEquals("Incorrect value of last node", String
+ .valueOf(CHILD_COUNT), context.getValue(CHILD_NAME2
+ + "[last()]"));
+
+ List<?> nodes = context.selectNodes("/" + CHILD_NAME1 + "[1]/*");
+ assertEquals("Wrong number of children", CHILD_COUNT, nodes.size());
+ int index = 1;
+ for (Iterator<?> it = nodes.iterator(); it.hasNext(); index++)
+ {
+ ConfigurationNode node = (ConfigurationNode) it.next();
+ assertEquals("Wrong node value for child " + index, "2." + index,
+ node.getValue());
+ }
+ }
+
+ /**
+ * Tests accessing attributes.
+ */
+ @Test
+ public void testAttributes()
+ {
+ root.addAttribute(new DefaultConfigurationNode("testAttr", "true"));
+ assertEquals("Did not find attribute of root node", "true", context
+ .getValue("@testAttr"));
+ assertEquals("Incorrect attribute value", "1", context.getValue("/"
+ + CHILD_NAME2 + "[1]/@" + ATTR_NAME));
+
+ assertTrue("Found elements with name attribute", context.selectNodes(
+ "//" + CHILD_NAME2 + "[@name]").isEmpty());
+ ConfigurationNode node = root.getChild(2).getChild(
+ 1).getChildren(CHILD_NAME2).get(1);
+ node.addAttribute(new DefaultConfigurationNode("name", "testValue"));
+ List<?> nodes = context.selectNodes("//" + CHILD_NAME2 + "[@name]");
+ assertEquals("Name attribute not found", 1, nodes.size());
+ assertEquals("Wrong node returned", node, nodes.get(0));
+ }
+
+ /**
+ * Tests accessing a node's text.
+ */
+ @Test
+ public void testText()
+ {
+ List<?> nodes = context.selectNodes("//" + CHILD_NAME2
+ + "[text()='1.1.1']");
+ assertEquals("Incorrect number of result nodes", 1, nodes.size());
+ }
+
+ /**
+ * Tests accessing the parent axis.
+ */
+ @Test
+ public void testParentAxis()
+ {
+ List<?> nodes = context.selectNodes("/" + CHILD_NAME2 + "/parent::*");
+ assertEquals("Wrong number of parent nodes", 1, nodes.size());
+ }
+
+ /**
+ * Tests accessing the following sibling axis.
+ */
+ @Test
+ public void testFollowingSiblingAxis()
+ {
+ List<?> nodes = context.selectNodes("/" + CHILD_NAME1
+ + "[2]/following-sibling::*");
+ assertEquals("Wrong number of following siblings", 1, nodes.size());
+ ConfigurationNode node = (ConfigurationNode) nodes.get(0);
+ assertEquals("Wrong node type", CHILD_NAME2, node.getName());
+ assertEquals("Wrong index", String.valueOf(CHILD_COUNT), node
+ .getValue());
+ }
+
+ /**
+ * Tests accessing the preceding sibling axis.
+ */
+ @Test
+ public void testPrecedingSiblingAxis()
+ {
+ List<?> nodes = context.selectNodes("/" + CHILD_NAME1
+ + "[2]/preceding-sibling::*");
+ assertEquals("Wrong number of preceding siblings", 3, nodes.size());
+ for (int index = 0, value = 3; index < nodes.size(); index++, value--)
+ {
+ assertEquals("Wrong node index", String.valueOf(value),
+ ((ConfigurationNode) nodes.get(index)).getValue());
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/tree/xpath/TestXPathExpressionEngine.java b/src/test/java/org/apache/commons/configuration/tree/xpath/TestXPathExpressionEngine.java
new file mode 100644
index 0000000..a1f9f1a
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/xpath/TestXPathExpressionEngine.java
@@ -0,0 +1,449 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree.xpath;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.configuration.tree.DefaultConfigurationNode;
+import org.apache.commons.configuration.tree.NodeAddData;
+import org.apache.commons.jxpath.JXPathContext;
+import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
+import org.apache.commons.jxpath.ri.model.NodePointerFactory;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for XPathExpressionEngine.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestXPathExpressionEngine.java 1226111 2011-12-31 15:44:50Z oheger $
+ */
+public class TestXPathExpressionEngine
+{
+ /** Constant for the test root node. */
+ static final ConfigurationNode ROOT = new DefaultConfigurationNode(
+ "testRoot");
+
+ /** Constant for the valid test key. */
+ static final String TEST_KEY = "TESTKEY";
+
+ /** The expression engine to be tested. */
+ XPathExpressionEngine engine;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ engine = new MockJXPathContextExpressionEngine();
+ }
+
+ /**
+ * Tests the query() method with a normal expression.
+ */
+ @Test
+ public void testQueryExpression()
+ {
+ List<ConfigurationNode> nodes = engine.query(ROOT, TEST_KEY);
+ assertEquals("Incorrect number of results", 1, nodes.size());
+ assertSame("Wrong result node", ROOT, nodes.get(0));
+ checkSelectCalls(1);
+ }
+
+ /**
+ * Tests a query that has no results. This should return an empty list.
+ */
+ @Test
+ public void testQueryWithoutResult()
+ {
+ List<ConfigurationNode> nodes = engine.query(ROOT, "a non existing key");
+ assertTrue("Result list is not empty", nodes.isEmpty());
+ checkSelectCalls(1);
+ }
+
+ /**
+ * Tests a query with an empty key. This should directly return the root
+ * node without invoking the JXPathContext.
+ */
+ @Test
+ public void testQueryWithEmptyKey()
+ {
+ checkEmptyKey("");
+ }
+
+ /**
+ * Tests a query with a null key. Same as an empty key.
+ */
+ @Test
+ public void testQueryWithNullKey()
+ {
+ checkEmptyKey(null);
+ }
+
+ /**
+ * Helper method for testing undefined keys.
+ *
+ * @param key the key
+ */
+ private void checkEmptyKey(String key)
+ {
+ List<ConfigurationNode> nodes = engine.query(ROOT, key);
+ assertEquals("Incorrect number of results", 1, nodes.size());
+ assertSame("Wrong result node", ROOT, nodes.get(0));
+ checkSelectCalls(0);
+ }
+
+ /**
+ * Tests if the used JXPathContext is correctly initialized.
+ */
+ @Test
+ public void testCreateContext()
+ {
+ JXPathContext ctx = new XPathExpressionEngine().createContext(ROOT,
+ TEST_KEY);
+ assertNotNull("Context is null", ctx);
+ assertTrue("Lenient mode is not set", ctx.isLenient());
+ assertSame("Incorrect context bean set", ROOT, ctx.getContextBean());
+
+ NodePointerFactory[] factories = JXPathContextReferenceImpl
+ .getNodePointerFactories();
+ boolean found = false;
+ for (int i = 0; i < factories.length; i++)
+ {
+ if (factories[i] instanceof ConfigurationNodePointerFactory)
+ {
+ found = true;
+ }
+ }
+ assertTrue("No configuration pointer factory found", found);
+ }
+
+ /**
+ * Tests a normal call of nodeKey().
+ */
+ @Test
+ public void testNodeKeyNormal()
+ {
+ assertEquals("Wrong node key", "parent/child", engine.nodeKey(
+ new DefaultConfigurationNode("child"), "parent"));
+ }
+
+ /**
+ * Tests nodeKey() for an attribute node.
+ */
+ @Test
+ public void testNodeKeyAttribute()
+ {
+ ConfigurationNode node = new DefaultConfigurationNode("attr");
+ node.setAttribute(true);
+ assertEquals("Wrong attribute key", "node/@attr", engine.nodeKey(node,
+ "node"));
+ }
+
+ /**
+ * Tests nodeKey() for the root node.
+ */
+ @Test
+ public void testNodeKeyForRootNode()
+ {
+ assertEquals("Wrong key for root node", "", engine.nodeKey(ROOT, null));
+ assertEquals("Null name not detected", "test", engine.nodeKey(
+ new DefaultConfigurationNode(), "test"));
+ }
+
+ /**
+ * Tests node key() for direct children of the root node.
+ */
+ @Test
+ public void testNodeKeyForRootChild()
+ {
+ ConfigurationNode node = new DefaultConfigurationNode("child");
+ assertEquals("Wrong key for root child node", "child", engine.nodeKey(
+ node, ""));
+ node.setAttribute(true);
+ assertEquals("Wrong key for root attribute", "@child", engine.nodeKey(
+ node, ""));
+ }
+
+ /**
+ * Tests adding a single child node.
+ */
+ @Test
+ public void testPrepareAddNode()
+ {
+ NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY + " newNode");
+ checkAddPath(data, new String[]
+ { "newNode" }, false);
+ checkSelectCalls(1);
+ }
+
+ /**
+ * Tests adding a new attribute node.
+ */
+ @Test
+ public void testPrepareAddAttribute()
+ {
+ NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY + "\t at newAttr");
+ checkAddPath(data, new String[]
+ { "newAttr" }, true);
+ checkSelectCalls(1);
+ }
+
+ /**
+ * Tests adding a complete path.
+ */
+ @Test
+ public void testPrepareAddPath()
+ {
+ NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY
+ + " \t a/full/path/node");
+ checkAddPath(data, new String[]
+ { "a", "full", "path", "node" }, false);
+ checkSelectCalls(1);
+ }
+
+ /**
+ * Tests adding a complete path whose final node is an attribute.
+ */
+ @Test
+ public void testPrepareAddAttributePath()
+ {
+ NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY
+ + " a/full/path at attr");
+ checkAddPath(data, new String[]
+ { "a", "full", "path", "attr" }, true);
+ checkSelectCalls(1);
+ }
+
+ /**
+ * Tests adding a new node to the root.
+ */
+ @Test
+ public void testPrepareAddRootChild()
+ {
+ NodeAddData data = engine.prepareAdd(ROOT, " newNode");
+ checkAddPath(data, new String[]
+ { "newNode" }, false);
+ checkSelectCalls(0);
+ }
+
+ /**
+ * Tests adding a new attribute to the root.
+ */
+ @Test
+ public void testPrepareAddRootAttribute()
+ {
+ NodeAddData data = engine.prepareAdd(ROOT, " @attr");
+ checkAddPath(data, new String[]
+ { "attr" }, true);
+ checkSelectCalls(0);
+ }
+
+ /**
+ * Tests an add operation with a query that does not return a single node.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrepareAddInvalidParent()
+ {
+ engine.prepareAdd(ROOT, "invalidKey newNode");
+ }
+
+ /**
+ * Tests an add operation with an empty path for the new node.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrepareAddEmptyPath()
+ {
+ engine.prepareAdd(ROOT, TEST_KEY + " ");
+ }
+
+ /**
+ * Tests an add operation where the key is null.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrepareAddNullKey()
+ {
+ engine.prepareAdd(ROOT, null);
+ }
+
+ /**
+ * Tests an add operation where the key is null.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrepareAddEmptyKey()
+ {
+ engine.prepareAdd(ROOT, "");
+ }
+
+ /**
+ * Tests an add operation with an invalid path.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrepareAddInvalidPath()
+ {
+ engine.prepareAdd(ROOT, TEST_KEY + " an/invalid//path");
+ }
+
+ /**
+ * Tests an add operation with an invalid path: the path contains an
+ * attribute in the middle part.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrepareAddInvalidAttributePath()
+ {
+ engine.prepareAdd(ROOT, TEST_KEY + " a/path/with at an/attribute");
+ }
+
+ /**
+ * Tests an add operation with an invalid path: the path contains an
+ * attribute after a slash.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrepareAddInvalidAttributePath2()
+ {
+ engine.prepareAdd(ROOT, TEST_KEY + " a/path/with/@attribute");
+ }
+
+ /**
+ * Tests an add operation with an invalid path that starts with a slash.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrepareAddInvalidPathWithSlash()
+ {
+ engine.prepareAdd(ROOT, TEST_KEY + " /a/path/node");
+ }
+
+ /**
+ * Tests an add operation with an invalid path that contains multiple
+ * attribute components.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrepareAddInvalidPathMultipleAttributes()
+ {
+ engine.prepareAdd(ROOT, TEST_KEY + " an at attribute@path");
+ }
+
+ /**
+ * Helper method for testing the path nodes in the given add data object.
+ *
+ * @param data the data object to check
+ * @param expected an array with the expected path elements
+ * @param attr a flag if the new node is an attribute
+ */
+ private void checkAddPath(NodeAddData data, String[] expected, boolean attr)
+ {
+ assertSame("Wrong parent node", ROOT, data.getParent());
+ List<String> path = data.getPathNodes();
+ assertEquals("Incorrect number of path nodes", expected.length - 1,
+ path.size());
+ Iterator<String> it = path.iterator();
+ for (int idx = 0; idx < expected.length - 1; idx++)
+ {
+ assertEquals("Wrong node at position " + idx, expected[idx], it
+ .next());
+ }
+ assertEquals("Wrong name of new node", expected[expected.length - 1],
+ data.getNewNodeName());
+ assertEquals("Incorrect attribute flag", attr, data.isAttribute());
+ }
+
+ /**
+ * Checks if the JXPath context's selectNodes() method was called as often
+ * as expected.
+ *
+ * @param expected the number of expected calls
+ */
+ protected void checkSelectCalls(int expected)
+ {
+ MockJXPathContext ctx = ((MockJXPathContextExpressionEngine) engine).getContext();
+ int calls = (ctx == null) ? 0 : ctx.selectInvocations;
+ assertEquals("Incorrect number of select calls", expected, calls);
+ }
+
+ /**
+ * A mock implementation of the JXPathContext class. This implementation
+ * will overwrite the <code>selectNodes()</code> method that is used by
+ * <code>XPathExpressionEngine</code> to count the invocations of this
+ * method.
+ */
+ static class MockJXPathContext extends JXPathContextReferenceImpl
+ {
+ int selectInvocations;
+
+ public MockJXPathContext(Object bean)
+ {
+ super(null, bean);
+ }
+
+ /**
+ * Dummy implementation of this method. If the passed in string is the
+ * test key, the root node will be returned in the list. Otherwise the
+ * return value is <b>null</b>.
+ */
+ @Override
+ public List<?> selectNodes(String xpath)
+ {
+ selectInvocations++;
+ if (TEST_KEY.equals(xpath))
+ {
+ List<ConfigurationNode> result = new ArrayList<ConfigurationNode>(1);
+ result.add(ROOT);
+ return result;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * A special implementation of XPathExpressionEngine that overrides
+ * createContext() to return a mock context object.
+ */
+ static class MockJXPathContextExpressionEngine extends
+ XPathExpressionEngine
+ {
+ /** Stores the context instance. */
+ private MockJXPathContext context;
+
+ @Override
+ protected JXPathContext createContext(ConfigurationNode root, String key)
+ {
+ context = new MockJXPathContext(root);
+ return context;
+ }
+
+ /**
+ * Returns the context created by the last newContext() call.
+ *
+ * @return the current context
+ */
+ public MockJXPathContext getContext()
+ {
+ return context;
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/tree/xpath/TestXPathExpressionEngineInConfig.java b/src/test/java/org/apache/commons/configuration/tree/xpath/TestXPathExpressionEngineInConfig.java
new file mode 100644
index 0000000..4a3c0fb
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/tree/xpath/TestXPathExpressionEngineInConfig.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.configuration.tree.xpath;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.commons.configuration.XMLConfiguration;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * A test class for XPathExpressionEngine that tests the engine integrated into
+ * a hierarchical configuration.
+ *
+ * @author <a
+ * href="http://commons.apache.org/configuration/team-list.html">Commons
+ * Configuration team</a>
+ * @version $Id: TestXPathExpressionEngineInConfig.java 1226113 2011-12-31 15:46:53Z oheger $
+ */
+public class TestXPathExpressionEngineInConfig
+{
+ /** Constant for a test key. */
+ private static final String KEY = "test/expression/xpath";
+
+ /** Constant for a value for test properties. */
+ private static final String VALUE = "success";
+
+ /** The test configuration. */
+ private XMLConfiguration config;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ config = new XMLConfiguration();
+ config.setExpressionEngine(new XPathExpressionEngine());
+ }
+
+ /**
+ * Tests whether an already existing property can be changed using
+ * setProperty().
+ */
+ @Test
+ public void testSetPropertyExisting()
+ {
+ config.addProperty(" " + KEY, "failure");
+ config.setProperty(KEY, VALUE);
+ assertEquals("Value not changed", VALUE, config.getString(KEY));
+ }
+
+ /**
+ * Tests setProperty() if the specified path partly exists.
+ */
+ @Test
+ public void testSetPropertyPartlyExisting()
+ {
+ final String testKey = KEY + "/sub";
+ config.addProperty(" " + KEY, "test");
+ config.setProperty(testKey, VALUE);
+ assertEquals("Value not set", VALUE, config.getString(testKey));
+ }
+
+ /**
+ * Tests whether setProperty() can be used to add a new attribute.
+ */
+ @Test
+ public void testSetPropertyNewAttribute()
+ {
+ final String keyAttr = KEY + "/@attr";
+ config.addProperty(" " + KEY, "test");
+ config.setProperty(keyAttr, VALUE);
+ assertEquals("Value not set", VALUE, config.getString(keyAttr));
+ }
+
+ /**
+ * Tests whether setProperty() can be used to create a completely new key.
+ */
+ @Test
+ public void testSetPropertyNewKey()
+ {
+ config.setProperty(KEY, VALUE);
+ assertEquals("Value not set", VALUE, config.getString(KEY));
+ }
+
+ /**
+ * Tests whether addProperty() can be used to create more complex
+ * hierarchical structures.
+ */
+ @Test
+ public void testAddPropertyComplexStructures()
+ {
+ config.addProperty("tables/table/name", "tasks");
+ config.addProperty("tables/table[last()]/@type", "system");
+ config.addProperty("tables/table[last()]/fields/field/name", "taskid");
+ config.addProperty("tables/table[last()]/fields/field[last()]/@type",
+ "int");
+ config.addProperty("tables table/name", "documents");
+ assertEquals("Wrong table 1", "tasks",
+ config.getString("tables/table[1]/name"));
+ assertEquals("Wrong table 2", "documents",
+ config.getString("tables/table[2]/name"));
+ assertEquals("Wrong field type", "int",
+ config.getString("tables/table[1]/fields/field[1]/@type"));
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/web/TestAppletConfiguration.java b/src/test/java/org/apache/commons/configuration/web/TestAppletConfiguration.java
new file mode 100644
index 0000000..0598139
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/web/TestAppletConfiguration.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.web;
+
+import static org.junit.Assert.fail;
+
+import java.applet.Applet;
+import java.util.Properties;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.BaseConfiguration;
+import org.apache.commons.configuration.MapConfiguration;
+import org.apache.commons.configuration.TestAbstractConfiguration;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test case for the {@link AppletConfiguration} class.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: TestAppletConfiguration.java 1222465 2011-12-22 21:32:56Z oheger $
+ */
+public class TestAppletConfiguration extends TestAbstractConfiguration
+{
+ /** A flag whether tests with an applet can be run. */
+ boolean supportsApplet;
+
+ /**
+ * Initializes the tests. This implementation checks whether an applet can
+ * be used. Some environments, which do not support a GUI, don't allow
+ * creating an <code>Applet</code> instance. If we are in such an
+ * environment, some tests need to behave differently or be completely
+ * dropped.
+ */
+ @Before
+ public void setUp() throws Exception
+ {
+ try
+ {
+ new Applet();
+ supportsApplet = true;
+ }
+ catch (Exception ex)
+ {
+ // cannot use applets
+ supportsApplet = false;
+ }
+ }
+
+ @Override
+ protected AbstractConfiguration getConfiguration()
+ {
+ final Properties parameters = new Properties();
+ parameters.setProperty("key1", "value1");
+ parameters.setProperty("key2", "value2");
+ parameters.setProperty("list", "value1, value2");
+ parameters.setProperty("listesc", "value1\\,value2");
+
+ if (supportsApplet)
+ {
+ Applet applet = new Applet()
+ {
+ /**
+ * Serial version UID.
+ */
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public String getParameter(String key)
+ {
+ return parameters.getProperty(key);
+ }
+
+ @Override
+ public String[][] getParameterInfo()
+ {
+ return new String[][]
+ {
+ { "key1", "String", "" },
+ { "key2", "String", "" },
+ { "list", "String[]", "" },
+ { "listesc", "String", "" } };
+ }
+ };
+
+ return new AppletConfiguration(applet);
+ }
+ else
+ {
+ return new MapConfiguration(parameters);
+ }
+ }
+
+ @Override
+ protected AbstractConfiguration getEmptyConfiguration()
+ {
+ if (supportsApplet)
+ {
+ return new AppletConfiguration(new Applet());
+ }
+ else
+ {
+ return new BaseConfiguration();
+ }
+ }
+
+ @Override
+ @Test
+ public void testAddPropertyDirect()
+ {
+ if (supportsApplet)
+ {
+ try
+ {
+ super.testAddPropertyDirect();
+ fail("addPropertyDirect should throw an UnsupportedException");
+ }
+ catch (UnsupportedOperationException e)
+ {
+ // ok
+ }
+ }
+ }
+
+ @Override
+ @Test
+ public void testClearProperty()
+ {
+ if (supportsApplet)
+ {
+ try
+ {
+ super.testClearProperty();
+ fail("testClearProperty should throw an UnsupportedException");
+ }
+ catch (UnsupportedOperationException e)
+ {
+ // ok
+ }
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/web/TestServletConfiguration.java b/src/test/java/org/apache/commons/configuration/web/TestServletConfiguration.java
new file mode 100644
index 0000000..6a05e7b
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/web/TestServletConfiguration.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.web;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletConfig;
+import javax.servlet.http.HttpServlet;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.TestAbstractConfiguration;
+import org.junit.Test;
+
+import com.mockobjects.servlet.MockServletConfig;
+
+/**
+ * Test case for the {@link ServletConfiguration} class.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: TestServletConfiguration.java 1222465 2011-12-22 21:32:56Z oheger $
+ */
+public class TestServletConfiguration extends TestAbstractConfiguration
+{
+ @Override
+ protected AbstractConfiguration getConfiguration()
+ {
+ final MockServletConfig config = new MockServletConfig();
+ config.setInitParameter("key1", "value1");
+ config.setInitParameter("key2", "value2");
+ config.setInitParameter("list", "value1, value2");
+ config.setInitParameter("listesc", "value1\\,value2");
+
+ Servlet servlet = new HttpServlet() {
+ /**
+ * Serial version UID.
+ */
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public ServletConfig getServletConfig()
+ {
+ return config;
+ }
+ };
+
+ return new ServletConfiguration(servlet);
+ }
+
+ @Override
+ protected AbstractConfiguration getEmptyConfiguration()
+ {
+ return new ServletConfiguration(new MockServletConfig());
+ }
+
+ @Override
+ @Test(expected = UnsupportedOperationException.class)
+ public void testAddPropertyDirect()
+ {
+ super.testAddPropertyDirect();
+ }
+
+ @Override
+ @Test(expected = UnsupportedOperationException.class)
+ public void testClearProperty()
+ {
+ super.testClearProperty();
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/web/TestServletContextConfiguration.java b/src/test/java/org/apache/commons/configuration/web/TestServletContextConfiguration.java
new file mode 100644
index 0000000..8c16bae
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/web/TestServletContextConfiguration.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.web;
+
+import java.util.Enumeration;
+import java.util.Properties;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServlet;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.TestAbstractConfiguration;
+import org.junit.Test;
+
+import com.mockobjects.servlet.MockServletConfig;
+import com.mockobjects.servlet.MockServletContext;
+
+/**
+ * Test case for the {@link ServletContextConfiguration} class.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: TestServletContextConfiguration.java 1222465 2011-12-22 21:32:56Z oheger $
+ */
+public class TestServletContextConfiguration extends TestAbstractConfiguration
+{
+ @Override
+ protected AbstractConfiguration getConfiguration()
+ {
+ final Properties parameters = new Properties();
+ parameters.setProperty("key1", "value1");
+ parameters.setProperty("key2", "value2");
+ parameters.setProperty("list", "value1, value2");
+ parameters.setProperty("listesc", "value1\\,value2");
+
+ // create a servlet context
+ ServletContext context = new MockServletContext()
+ {
+ @Override
+ public String getInitParameter(String key)
+ {
+ return parameters.getProperty(key);
+ }
+
+ @Override
+ public Enumeration<?> getInitParameterNames()
+ {
+ return parameters.keys();
+ }
+ };
+
+ // create a servlet config
+ final MockServletConfig config = new MockServletConfig();
+ config.setServletContext(context);
+
+ // create a servlet
+ Servlet servlet = new HttpServlet()
+ {
+ /**
+ * Serial version UID.
+ */
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public ServletConfig getServletConfig()
+ {
+ return config;
+ }
+ };
+
+ return new ServletContextConfiguration(servlet);
+ }
+
+ @Override
+ protected AbstractConfiguration getEmptyConfiguration()
+ {
+ // create a servlet context
+ ServletContext context = new MockServletContext()
+ {
+ @Override
+ public Enumeration<?> getInitParameterNames()
+ {
+ return new Properties().keys();
+ }
+ };
+
+ return new ServletContextConfiguration(context);
+ }
+
+ @Override
+ @Test(expected = UnsupportedOperationException.class)
+ public void testAddPropertyDirect()
+ {
+ super.testAddPropertyDirect();
+ }
+
+ @Override
+ @Test(expected = UnsupportedOperationException.class)
+ public void testClearProperty()
+ {
+ super.testClearProperty();
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/web/TestServletFilterConfiguration.java b/src/test/java/org/apache/commons/configuration/web/TestServletFilterConfiguration.java
new file mode 100644
index 0000000..1b01fc7
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/web/TestServletFilterConfiguration.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.web;
+
+import java.util.Enumeration;
+import java.util.Properties;
+
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.TestAbstractConfiguration;
+import org.junit.Test;
+
+/**
+ * Test case for the {@link ServletFilterConfiguration} class.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: TestServletFilterConfiguration.java 1222465 2011-12-22 21:32:56Z oheger $
+ */
+public class TestServletFilterConfiguration extends TestAbstractConfiguration
+{
+ @Override
+ protected AbstractConfiguration getConfiguration()
+ {
+ MockFilterConfig config = new MockFilterConfig();
+ config.setInitParameter("key1", "value1");
+ config.setInitParameter("key2", "value2");
+ config.setInitParameter("list", "value1, value2");
+ config.setInitParameter("listesc", "value1\\,value2");
+
+ return new ServletFilterConfiguration(config);
+ }
+
+ @Override
+ protected AbstractConfiguration getEmptyConfiguration()
+ {
+ return new ServletFilterConfiguration(new MockFilterConfig());
+ }
+
+ private class MockFilterConfig implements FilterConfig
+ {
+ private Properties parameters = new Properties();
+
+ public String getFilterName()
+ {
+ return null;
+ }
+
+ public ServletContext getServletContext()
+ {
+ return null;
+ }
+
+ public String getInitParameter(String key)
+ {
+ return parameters.getProperty(key);
+ }
+
+ public Enumeration<?> getInitParameterNames()
+ {
+ return parameters.keys();
+ }
+
+ public void setInitParameter(String key, String value)
+ {
+ parameters.setProperty(key, value);
+ }
+ }
+
+ @Override
+ @Test(expected = UnsupportedOperationException.class)
+ public void testAddPropertyDirect()
+ {
+ super.testAddPropertyDirect();
+ }
+
+ @Override
+ @Test(expected = UnsupportedOperationException.class)
+ public void testClearProperty()
+ {
+ super.testClearProperty();
+ }
+}
diff --git a/src/test/java/org/apache/commons/configuration/web/TestServletRequestConfiguration.java b/src/test/java/org/apache/commons/configuration/web/TestServletRequestConfiguration.java
new file mode 100644
index 0000000..2aa4d5f
--- /dev/null
+++ b/src/test/java/org/apache/commons/configuration/web/TestServletRequestConfiguration.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.configuration.web;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.ServletRequest;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.BaseConfiguration;
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.configuration.ConfigurationMap;
+import org.apache.commons.configuration.TestAbstractConfiguration;
+import org.junit.Test;
+
+import com.mockobjects.servlet.MockHttpServletRequest;
+
+/**
+ * Test case for the {@link ServletRequestConfiguration} class.
+ *
+ * @author Emmanuel Bourg
+ * @version $Id: TestServletRequestConfiguration.java 1222465 2011-12-22 21:32:56Z oheger $
+ */
+public class TestServletRequestConfiguration extends TestAbstractConfiguration
+{
+ @Override
+ protected AbstractConfiguration getConfiguration()
+ {
+ final Configuration configuration = new BaseConfiguration();
+ ((BaseConfiguration) configuration).setListDelimiter('\0');
+ configuration.setProperty("key1", "value1");
+ configuration.setProperty("key2", "value2");
+ configuration.addProperty("list", "value1");
+ configuration.addProperty("list", "value2");
+ configuration.addProperty("listesc", "value1\\,value2");
+
+ return createConfiguration(configuration);
+ }
+
+ @Override
+ protected AbstractConfiguration getEmptyConfiguration()
+ {
+ ServletRequest request = new MockHttpServletRequest()
+ {
+ @Override
+ public String getParameter(String key)
+ {
+ return null;
+ }
+
+ @Override
+ public Map<?, ?> getParameterMap()
+ {
+ return new HashMap<Object, Object>();
+ }
+ };
+
+ return new ServletRequestConfiguration(request);
+ }
+
+ /**
+ * Returns a new servlet request configuration that is backed by the passed
+ * in configuration.
+ *
+ * @param base the configuration with the underlying values
+ * @return the servlet request configuration
+ */
+ private ServletRequestConfiguration createConfiguration(final Configuration base)
+ {
+ ServletRequest request = new MockHttpServletRequest()
+ {
+ @Override
+ public String[] getParameterValues(String key)
+ {
+ return base.getStringArray(key);
+ }
+
+ @Override
+ public Map<?, ?> getParameterMap()
+ {
+ return new ConfigurationMap(base);
+ }
+ };
+
+ return new ServletRequestConfiguration(request);
+ }
+
+ @Override
+ @Test(expected = UnsupportedOperationException.class)
+ public void testAddPropertyDirect()
+ {
+ super.testAddPropertyDirect();
+ }
+
+ @Override
+ @Test(expected = UnsupportedOperationException.class)
+ public void testClearProperty()
+ {
+ super.testClearProperty();
+ }
+
+ /**
+ * Tests a list with elements that contain an escaped list delimiter.
+ */
+ @Test
+ public void testListWithEscapedElements()
+ {
+ String[] values = { "test1", "test2\\,test3", "test4\\,test5" };
+ String listKey = "test.list";
+
+ BaseConfiguration config = new BaseConfiguration();
+ config.setListDelimiter('\0');
+ config.addProperty(listKey, values);
+
+ assertEquals("Wrong number of list elements", values.length, config.getList(listKey).size());
+
+ Configuration c = createConfiguration(config);
+ List<?> v = c.getList(listKey);
+
+ assertEquals("Wrong number of elements in list", values.length, v.size());
+
+ for (int i = 0; i < values.length; i++)
+ {
+ assertEquals("Wrong value at index " + i, values[i].replaceAll("\\\\", ""), v.get(i));
+ }
+ }
+}
diff --git a/src/test/resources/01/testMultiConfiguration_1001.xml b/src/test/resources/01/testMultiConfiguration_1001.xml
new file mode 100644
index 0000000..6e96ee9
--- /dev/null
+++ b/src/test/resources/01/testMultiConfiguration_1001.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration>
+ <colors>
+ <background>#808080</background>
+ <text>#000000</text>
+ <header>#008000</header>
+ <link normal="#000080" visited="#800080"/>
+ <default>${colors.header}</default>
+ </colors>
+ <rowsPerPage>15</rowsPerPage>
+ <buttons>
+ <name>OK,Cancel,Help</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+ <Channels>
+ <Channel id="1">
+ <Name>My Channel</Name>
+ </Channel>
+ <Channel id="2">
+ <MoreChannelData>more test 2 data</MoreChannelData>
+ </Channel>
+ </Channels>
+</configuration>
diff --git a/src/test/resources/catalog.xml b/src/test/resources/catalog.xml
new file mode 100644
index 0000000..4d695c9
--- /dev/null
+++ b/src/test/resources/catalog.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">
+
+ <public publicId="-//Apache//DTD Resolver Test V1.0//EN"
+ uri="resolver.dtd"/>
+ <rewriteSystem systemIdStartString="http://java.sun.com/dtd/"
+ rewritePrefix="./"/>
+ <rewriteSystem systemIdStartString="http://commons.apache.org/" rewritePrefix="./"/>
+</catalog>
\ No newline at end of file
diff --git a/src/test/resources/catalog2.xml b/src/test/resources/catalog2.xml
new file mode 100644
index 0000000..a7d6de5
--- /dev/null
+++ b/src/test/resources/catalog2.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">
+
+ <public publicId="-//Apache//DTD Resolver Test V1.0//EN"
+ uri="resolver.dtd"/>
+ <rewriteSystem systemIdStartString="http://java.sun.com/dtd/"
+ rewritePrefix="./"/>
+ <rewriteSystem systemIdStartString="http://commons.apache.org/"
+ rewritePrefix="file://${sys:user.dir}/target/test-classes/"/>
+</catalog>
\ No newline at end of file
diff --git a/src/test/resources/config/deep/deepinclude.properties b/src/test/resources/config/deep/deepinclude.properties
new file mode 100644
index 0000000..fe58457
--- /dev/null
+++ b/src/test/resources/config/deep/deepinclude.properties
@@ -0,0 +1,15 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+deepinclude=true
diff --git a/src/test/resources/config/deep/deeptest.properties b/src/test/resources/config/deep/deeptest.properties
new file mode 100644
index 0000000..12991d1
--- /dev/null
+++ b/src/test/resources/config/deep/deeptest.properties
@@ -0,0 +1,17 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+include=deepinclude.properties
+
+deeptest=true
diff --git a/src/test/resources/config/deep/deeptestinvalid.properties b/src/test/resources/config/deep/deeptestinvalid.properties
new file mode 100644
index 0000000..5c92dd8
--- /dev/null
+++ b/src/test/resources/config/deep/deeptestinvalid.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Tries to include a non existing file
+include=nonexisting.properties
+
+deeptestinvalid=true
diff --git a/src/test/resources/config/deep/test.properties b/src/test/resources/config/deep/test.properties
new file mode 100644
index 0000000..5abfed8
--- /dev/null
+++ b/src/test/resources/config/deep/test.properties
@@ -0,0 +1,15 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+somekey=somevalue
\ No newline at end of file
diff --git a/src/test/resources/config/deep/testEqualDeep.properties b/src/test/resources/config/deep/testEqualDeep.properties
new file mode 100644
index 0000000..b5d818f
--- /dev/null
+++ b/src/test/resources/config/deep/testEqualDeep.properties
@@ -0,0 +1,26 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+property.a = a
+property.b = b
+property.c = 100
+
+#
+# Value set twice
+property.a = aa
+
+clear.property = delete me
+
+existing.property = i exist
+
diff --git a/src/test/resources/config/deep/testFileFromClasspath.xml b/src/test/resources/config/deep/testFileFromClasspath.xml
new file mode 100644
index 0000000..52b8216
--- /dev/null
+++ b/src/test/resources/config/deep/testFileFromClasspath.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<configuration>
+ <properties config-name="propConf" fileName="testEqual.properties"/>
+ <properties config-name="propConfDeep" fileName="testEqualDeep.properties"/>
+</configuration>
+
diff --git a/src/test/resources/config/test.properties b/src/test/resources/config/test.properties
new file mode 100644
index 0000000..74ea998
--- /dev/null
+++ b/src/test/resources/config/test.properties
@@ -0,0 +1,25 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+boolean=true
+overwrite=80
+key=jndivalue
+key2=jndivalue2
+jndi=only_jndi
+byte=10
+double=10.25
+float=20.25
+integer=10
+long=1000000
+short=1
\ No newline at end of file
diff --git a/src/test/resources/configA.xml b/src/test/resources/configA.xml
new file mode 100644
index 0000000..5f6d6c9
--- /dev/null
+++ b/src/test/resources/configA.xml
@@ -0,0 +1,19 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration>
+ <property name="config" value="100"/>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/configB.xml b/src/test/resources/configB.xml
new file mode 100644
index 0000000..56e53f1
--- /dev/null
+++ b/src/test/resources/configB.xml
@@ -0,0 +1,19 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration>
+ <stuff>test</stuff>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/dataset.xml b/src/test/resources/dataset.xml
new file mode 100644
index 0000000..6430406
--- /dev/null
+++ b/src/test/resources/dataset.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0"?>
+<!DOCTYPE dataset SYSTEM "dataset.dtd">
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- dbunit DataSet for the TestDatabaseConfiguration test -->
+<dataset>
+
+ <table name="configuration">
+ <column>key</column>
+ <column>value</column>
+ <row>
+ <value>key1</value>
+ <value>value1</value>
+ </row>
+ <row>
+ <value>key2</value>
+ <value>value2</value>
+ </row>
+ <row>
+ <value>keyMulti</value>
+ <value>a;b;c</value>
+ </row>
+ </table>
+
+ <table name="configurations">
+ <column>name</column>
+ <column>key</column>
+ <column>value</column>
+ <row>
+ <value>test</value>
+ <value>key1</value>
+ <value>value1</value>
+ </row>
+ <row>
+ <value>test</value>
+ <value>key2</value>
+ <value>value2</value>
+ </row>
+ </table>
+ <table name="configurationList">
+ <column>id</column>
+ <column>key</column>
+ <column>value</column>
+ <row>
+ <value>1</value>
+ <value>key3</value>
+ <value>value1</value>
+ </row>
+ <row>
+ <value>2</value>
+ <value>key3</value>
+ <value>value2</value>
+ </row>
+ <row>
+ <value>3</value>
+ <value>key3</value>
+ <value>value3</value>
+ </row>
+ </table>
+
+</dataset>
diff --git a/src/test/resources/include-interpol.properties b/src/test/resources/include-interpol.properties
new file mode 100644
index 0000000..8e4c167
--- /dev/null
+++ b/src/test/resources/include-interpol.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+include.interpol.loaded = true
+
+
+
diff --git a/src/test/resources/include.properties b/src/test/resources/include.properties
new file mode 100644
index 0000000..14f7a58
--- /dev/null
+++ b/src/test/resources/include.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+include.loaded = true
+
+packages = packageb, packagec
+
+
diff --git a/src/test/resources/jndi.properties b/src/test/resources/jndi.properties
new file mode 100644
index 0000000..87771b2
--- /dev/null
+++ b/src/test/resources/jndi.properties
@@ -0,0 +1,17 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+java.naming.factory.initial=org.osjava.jndi.PropertiesFactory
+org.osjava.jndi.root=classpath://config
+org.osjava.jndi.delimiter=/
diff --git a/src/test/resources/log4j-test.xml b/src/test/resources/log4j-test.xml
new file mode 100644
index 0000000..ad50c1d
--- /dev/null
+++ b/src/test/resources/log4j-test.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
+ <appender name="console" class="org.apache.log4j.ConsoleAppender">
+ <param name="Target" value="System.out"/>
+ <layout class="org.apache.log4j.PatternLayout">
+ <param name="ConversionPattern" value="%-5p %c{1} - %m%n"/>
+ </layout>
+ </appender>
+
+ <root>
+ <priority value ="debug" />
+ <appender-ref ref="console" />
+ </root>
+
+</log4j:configuration>
\ No newline at end of file
diff --git a/src/test/resources/resolver.dtd b/src/test/resources/resolver.dtd
new file mode 100644
index 0000000..67bb0e6
--- /dev/null
+++ b/src/test/resources/resolver.dtd
@@ -0,0 +1,19 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!ELEMENT test (foo|bar)+>
+<!ELEMENT foo EMPTY>
+<!ELEMENT bar EMPTY>
\ No newline at end of file
diff --git a/src/test/resources/sample.xml b/src/test/resources/sample.xml
new file mode 100644
index 0000000..fc5b61c
--- /dev/null
+++ b/src/test/resources/sample.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<Employees xmlns="http://commons.apache.org/employee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://commons.apache.org/employee http://commons.apache.org/sample.xsd">
+ <Employee>
+ <SSN>555121211</SSN>
+ <Name>John Doe</Name>
+ <DateOfBirth>1975-05-15</DateOfBirth>
+ <EmployeeType>Exempt</EmployeeType>
+ <Salary>100000</Salary>
+ </Employee>
+</Employees>
\ No newline at end of file
diff --git a/src/test/resources/sample.xsd b/src/test/resources/sample.xsd
new file mode 100644
index 0000000..aeda543
--- /dev/null
+++ b/src/test/resources/sample.xsd
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified"
+ targetNamespace="http://commons.apache.org/employee"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="Employees" xmlns="http://commons.apache.org/employee">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="Employee">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element type="xs:string" name="SSN" minOccurs="0"/>
+ <xs:element type="xs:string" name="Name" minOccurs="0"/>
+ <xs:element type="xs:string" name="DateOfBirth" minOccurs="0"/>
+ <xs:element type="xs:string" name="EmployeeType" minOccurs="0"/>
+ <xs:element type="xs:string" name="Salary" minOccurs="0"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>
\ No newline at end of file
diff --git a/src/test/resources/sample_1001.xml b/src/test/resources/sample_1001.xml
new file mode 100644
index 0000000..a0df635
--- /dev/null
+++ b/src/test/resources/sample_1001.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<Employees xmlns="http://commons.apache.org/employee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://commons.apache.org/employee http://commons.apache.org/sample.xsd">
+ <Employee>
+ <Name>Jane Doe</Name>
+ </Employee>
+</Employees>
\ No newline at end of file
diff --git a/src/test/resources/test.ini b/src/test/resources/test.ini
new file mode 100644
index 0000000..070e68c
--- /dev/null
+++ b/src/test/resources/test.ini
@@ -0,0 +1,17 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+; Test ini file to be included by a configuration definition
+[testini]
+loaded=yes
diff --git a/src/test/resources/test.plist b/src/test/resources/test.plist
new file mode 100644
index 0000000..60a2a76
--- /dev/null
+++ b/src/test/resources/test.plist
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+{
+ simple-string = string1;
+ quoted-string = "string2";
+ quoted-string2 = "this is a string";
+ "complex-string" = "this is a \"complex\" string {(=,;)}";
+
+ // This is a test single-line comment
+ array = ( "value1", "value2", "value3" );
+
+ empty-array = ();
+
+ nested-arrays = ( (a , b) , (c, d) );
+
+ dictionary-array =
+ (
+ { foo = bar },
+ { key = value }
+ )
+
+ dictionary =
+ {
+ foo1 = bar1;
+ foo2 = bar2;
+ }
+
+ empty-dictionary = { };
+
+ nested-dictionaries =
+ {
+ foo =
+ {
+ bar =
+ {
+ "key" = "value";
+ }
+ }
+ }
+
+ date = <*D2002-03-22 11:30:00 +0100>;
+
+ data = <666F6f 20 626172>;
+
+ empty-data = < >;
+}
diff --git a/src/test/resources/test.plist.xml b/src/test/resources/test.plist.xml
new file mode 100644
index 0000000..c706bde
--- /dev/null
+++ b/src/test/resources/test.plist.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0"?>
+<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<plist version="1.0">
+ <dict>
+
+ <key>string</key>
+ <string>value1</string>
+
+ <key>integer</key>
+ <integer>12345678900</integer>
+
+ <key>real</key>
+ <real>-123.45E-1</real>
+
+ <key>boolean1</key>
+ <true/>
+
+ <key>boolean2</key>
+ <false/>
+
+ <key>date</key>
+ <date>2005-01-01T12:00:00Z</date>
+
+ <key>date-gnustep</key>
+ <date>2002-03-22 11:30:00 +0100</date>
+
+ <key>data</key>
+ <data>RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==</data>
+
+ <key>array</key>
+ <array>
+ <string>value1</string>
+ <string>value2</string>
+ <string>value3</string>
+ </array>
+
+ <key>nested-array</key>
+ <array>
+ <array>
+ <string>a</string>
+ <string>b</string>
+ </array>
+ <array>
+ <string>c</string>
+ <string>d</string>
+ </array>
+ </array>
+
+ <key>dictionary-array</key>
+ <array>
+ <dict>
+ <key>foo</key>
+ <string>bar</string>
+ </dict>
+ <dict>
+ <key>key</key>
+ <string>value</string>
+ </dict>
+ </array>
+
+ <key>dictionary</key>
+ <dict>
+ <key>key1</key>
+ <string>value1</string>
+ <key>key2</key>
+ <string>value2</string>
+ <key>key3</key>
+ <string>value3</string>
+ </dict>
+
+ <key>nested</key>
+ <dict>
+ <key>node1</key>
+ <dict>
+ <key>node2</key>
+ <dict>
+ <key>node3</key>
+ <string>value</string>
+ </dict>
+ </dict>
+ </dict>
+
+ <key>empty-dictionary</key>
+ <dict/>
+
+ </dict>
+</plist>
diff --git a/src/test/resources/test.properties b/src/test/resources/test.properties
new file mode 100644
index 0000000..491742f
--- /dev/null
+++ b/src/test/resources/test.properties
@@ -0,0 +1,126 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+configuration.loaded = true
+
+packages = packagea
+propertyInOrder = test.properties
+
+include = include.properties
+
+include.file = include-interpol.properties
+include = ${include.file}
+
+test.unescape = This \n string \t contains \" escaped \\ character\u0073
+test.unescape.list-separator = This string contains \, an escaped list separator
+
+#
+# Other test properties
+#
+
+test.equals = value=one
+
+test.empty =
+
+test.mixed.array = a
+test.mixed.array = b, c, d
+
+test.multilines = This is a value spread out across several adjacent \
+ natural lines by escaping the line terminator with \
+ a backslash character.
+
+#
+# Test a property that uses a previous property
+#
+
+base = base
+base.reference = ${base}extra
+base.reference.array = ${base}extra
+base.reference.array = ${base}extra
+
+#
+# Non String Properties
+#
+
+test.boolean = true
+test.boolean.array = false
+test.boolean.array = true
+
+test.byte = 10
+test.byte.array = 20
+test.byte.array = 30
+
+test.double = 10.25
+test.double.array = 20.35
+test.double.array = 30.45
+
+test.float = 20.25
+test.float.array = 30.35
+test.float.array = 40.45
+
+test.integer = 10
+test.integer.array = 20
+test.integer.array = 30
+
+test.long = 1000000
+test.long.array = 2000000
+test.long.array = 3000000
+
+test.short = 1
+test.short.array = 2
+test.short.array = 3
+
+test.overwrite = 1
+
+#
+# Test complex line ending escaping
+#
+
+test.path = C:\\path1\\
+test.path = C:\\path2\\
+test.path = C:\\path3\\\
+complex\\test\\
+
+#
+# Test for the comment lines
+#
+
+#comment = this is not a property but a comment line starting with '#'
+!comment = this is not a property but a comment line starting with '!'
+
+#
+# Tests for the key/value separators ('=', ':' or white space, escaped or not)
+#
+
+test.separator\=in.key = foo
+test.separator\:in.key = bar
+test.separator\ in.key = foo
+test.separator\
in.key = bar
+test.separator\ in.key = foo
+
+test.separator.equal = foo
+test.separator.colon : foo
+test.separator.tab foo
+test.separator.formfeed
foo
+test.separator.whitespace foo
+test.separator.no.space=foo
+
+# Tests for backslash escaping in lists
+test.share1 = \\\\\\\\share1a, \\\\\\\\share1b
+test.share2 = \\\\share2a
+test.share2 = \\\\share2b
+test.share3 = \\\\\\\\share3a\\\\\\\\,\\\\\\\\share3b\\
+
+# This is a foot comment
+
diff --git a/src/test/resources/test.properties.xml b/src/test/resources/test.properties.xml
new file mode 100644
index 0000000..f6d10d2
--- /dev/null
+++ b/src/test/resources/test.properties.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<properties>
+ <comment>Description of the property list</comment>
+ <entry key="key1">value1</entry>
+ <entry key="key2">value2</entry>
+ <entry key="key3">value3</entry>
+</properties>
diff --git a/src/test/resources/test.xml b/src/test/resources/test.xml
new file mode 100644
index 0000000..4f84463
--- /dev/null
+++ b/src/test/resources/test.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Test file for XMLConfiguration -->
+<testconfig>
+ <element>value</element>
+ <element2>
+ <subelement>
+ <subsubelement>I'm complex!</subsubelement>
+ </subelement>
+ </element2>
+ <element3 name="foo">value</element3>
+ <test>
+ <comment><!-- this value is commented --></comment>
+ <cdata><![CDATA[<cdata value>]]></cdata>
+ <entity name="foo"bar">1<2</entity>
+ </test>
+ <mean>This is
+<![CDATA[ A long story...]]>
+ <submean>really complex structure</submean>
+And even longer.
+ </mean>
+
+ <!-- non string properties -->
+ <test>
+ <short>8</short>
+ </test>
+
+ <!-- list properties -->
+ <list>
+ <item name="one">one</item>
+ <item>two</item>
+ </list>
+ <list>
+ <item name="three">three</item>
+ <item>four</item>
+ <sublist>
+ <item>five</item>
+ <item>six</item>
+ </sublist>
+ </list>
+
+ <!-- Comma delimited lists -->
+ <split>
+ <list1>a,b,c</list1>
+ <list2>a\,b\,c</list2>
+ <list3 values="a,b,c"/>
+ <list4 values="a\,b\,c"/>
+ </split>
+
+ <!-- clear tests -->
+ <clear>
+ <element>value</element>
+ <element2 id="element2">value</element2>
+ <comment><!-- this value is commented --></comment>
+ <cdata><![CDATA[<cdata value>]]></cdata>
+ <list>
+ <item id="1">one</item>
+ <item>two</item>
+ <item id="3">three</item>
+ <item>four</item>
+ </list>
+ <list>
+ <item>one</item>
+ <item id="2">two</item>
+ <item>three</item>
+ <item id="4">four</item>
+ </list>
+ </clear>
+
+ <!-- Complex property names -->
+ <complexNames>
+ <my.elem>Name with dot
+ <sub.elem>Another dot</sub.elem>
+ </my.elem>
+ </complexNames>
+
+ <!-- An empty element. This should occur in the configuration with an
+ empty string as value.
+ -->
+ <empty/>
+
+ <!-- List nodes with attributes -->
+ <attrList>
+ <a name="x">ABC</a>
+ <a name="y">1,2,3</a>
+ <a name="u" test="yes">value1,value2</a>
+ </attrList>
+
+ <!-- An attribute with multiple values and escape characters for testing
+ splitting when delimiter parsing is disabled.
+ -->
+ <expressions value="a \|\| (b && c)|!d"
+ value2="a,b|c"/>
+
+ <!-- Tests for handling of spaces -->
+ <space xml:space="preserve">
+ <blanc> </blanc>
+ <stars> * * </stars>
+ <description xml:space="default"> Some text </description>
+ <testInvalid xml:space="invalid"> Some other text </testInvalid>
+ </space>
+ <spaceElement xml:space="preserve"> preserved </spaceElement>
+</testconfig>
diff --git a/src/test/resources/test2.plist.xml b/src/test/resources/test2.plist.xml
new file mode 100644
index 0000000..778a6b4
--- /dev/null
+++ b/src/test/resources/test2.plist.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<plist version="1.0">
+ <array>
+ <array>
+ <string>http://www.google.com/search?hl=en&client=safari&rls=en&q=RFC822MessageDatasPboardType&btnG=Search&aq=f&oq=&aqi=</string>
+ </array>
+ <array>
+ <string>RFC822MessageDatasPboardType - Google Search</string>
+ </array>
+ </array>
+</plist>
+
diff --git a/src/test/resources/test2.properties b/src/test/resources/test2.properties
new file mode 100644
index 0000000..eadcc99
--- /dev/null
+++ b/src/test/resources/test2.properties
@@ -0,0 +1,59 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+configuration.loaded = true
+
+packages = override.packages
+propertyInOrder=test2.properties
+
+
+#
+# Other test properties
+#
+
+test.equals = value=one
+
+test.empty=
+
+#
+# Non String Properties
+#
+
+test.boolean = true
+test.boolean.array = false
+test.boolean.array = true
+
+test.byte = 10
+test.byte.array = 20
+test.byte.array = 30
+
+test.double = 10.25
+test.double.array = 20.35
+test.double.array = 30.45
+
+test.float = 20.25
+test.float.array = 30.35
+test.float.array = 40.45
+
+test.integer = 10
+test.integer.array = 20
+test.integer.array = 30
+
+test.long = 1000000
+test.long.array = 2000000
+test.long.array = 3000000
+
+test.short = 1
+test.short.array = 2
+test.short.array = 3
diff --git a/src/test/resources/testClasspath.properties b/src/test/resources/testClasspath.properties
new file mode 100644
index 0000000..0566098
--- /dev/null
+++ b/src/test/resources/testClasspath.properties
@@ -0,0 +1,67 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+configuration.loaded = true
+
+packages = packagea
+
+include = include.properties
+
+#
+# Other test properties
+#
+
+test.equals = value=one
+
+test.empty=
+
+#
+# Test a property that uses a previous property
+#
+base=base
+base.reference=${base}extra
+
+#
+# Non String Properties
+#
+
+test.boolean = true
+test.boolean.array = false
+test.boolean.array = true
+
+test.byte = 10
+test.byte.array = 20
+test.byte.array = 30
+
+test.double = 10.25
+test.double.array = 20.35
+test.double.array = 30.45
+
+test.float = 20.25
+test.float.array = 30.35
+test.float.array = 40.45
+
+test.integer = 10
+test.integer.array = 20
+test.integer.array = 30
+
+test.long = 1000000
+test.long.array = 2000000
+test.long.array = 3000000
+
+test.short = 1
+test.short.array = 2
+test.short.array = 3
+
+test.overwrite = 1
\ No newline at end of file
diff --git a/src/test/resources/testComplexInitialization.xml b/src/test/resources/testComplexInitialization.xml
new file mode 100644
index 0000000..dc3999c
--- /dev/null
+++ b/src/test/resources/testComplexInitialization.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true"
+ ignoreReloadExceptions="true">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.OverrideCombiner"/>
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <combiner>
+ <override>
+ <list-nodes>
+ <node>table</node>
+ <node>list</node>
+ </list-nodes>
+ </override>
+ </combiner>
+ </header>
+ <system/>
+ <properties fileName="test.properties" throwExceptionOnMissing="true"
+ config-name="properties">
+ <reloadingStrategy config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"
+ refreshDelay="10000"/>
+ </properties>
+ <!-- Fetch the file name from a variable -->
+ <xml fileName="${test_file_xml}" config-name="xml">
+ <expressionEngine config-class="org.apache.commons.configuration.tree.DefaultExpressionEngine"
+ propertyDelimiter="/" indexStart="[" indexEnd="]"/>
+ </xml>
+ <additional>
+ <xml config-name="combiner1" fileName="${test_file_combine}"/> -->
+ <xml config-name="combiner2" fileName="testcombine2.xml"/>
+ </additional>
+</configuration>
diff --git a/src/test/resources/testConfigurationInterpolatorUpdate.xml b/src/test/resources/testConfigurationInterpolatorUpdate.xml
new file mode 100644
index 0000000..d857263
--- /dev/null
+++ b/src/test/resources/testConfigurationInterpolatorUpdate.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.OverrideCombiner"/>
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <providers>
+ <provider config-tag="test"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$XMLConfigurationProvider"
+ configurationClass="org.apache.commons.configuration.TestDefaultConfigurationBuilder$ExtendedXMLConfiguration"/>
+ </providers>
+ <combiner>
+ <override>
+ <list-nodes>
+ <node>table</node>
+ <node>list</node>
+ </list-nodes>
+ </override>
+ </combiner>
+ </header>
+ <system/>
+ <properties fileName="test.properties" throwExceptionOnMissing="true"
+ config-name="properties">
+ <reloadingStrategy config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"
+ refreshDelay="10000"/>
+ </properties>
+ <!-- Fetch the file name from a variable -->
+ <test fileName="${test_file_xml}" config-name="xml">
+ <expressionEngine config-class="org.apache.commons.configuration.tree.DefaultExpressionEngine"
+ propertyDelimiter="/" indexStart="[" indexEnd="]"/>
+ </test>
+ <additional>
+ <xml config-name="combiner1" fileName="${test_file_combine}"/> -->
+ <xml config-name="combiner2" fileName="testcombine2.xml"/>
+ </additional>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testConfigurationProvider.xml b/src/test/resources/testConfigurationProvider.xml
new file mode 100644
index 0000000..cfd0038
--- /dev/null
+++ b/src/test/resources/testConfigurationProvider.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.OverrideCombiner"/>
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <providers>
+ <provider config-tag="test"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$FileConfigurationProvider"/>
+ </providers>
+ <combiner>
+ <override>
+ <list-nodes>
+ <node>table</node>
+ <node>list</node>
+ </list-nodes>
+ </override>
+ </combiner>
+ </header>
+ <system/>
+ <properties fileName="test.properties" throwExceptionOnMissing="true"
+ config-name="properties">
+ <reloadingStrategy config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"
+ refreshDelay="10000"/>
+ </properties>
+ <!-- Fetch the file name from a variable -->
+ <xml fileName="${test_file_xml}" config-name="xml">
+ <expressionEngine config-class="org.apache.commons.configuration.tree.DefaultExpressionEngine"
+ propertyDelimiter="/" indexStart="[" indexEnd="]"/>
+ </xml>
+ <additional>
+ <xml config-name="combiner1" fileName="${test_file_combine}"/> -->
+ <xml config-name="combiner2" fileName="testcombine2.xml"/>
+ </additional>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testConfigurationXMLDocument.xml b/src/test/resources/testConfigurationXMLDocument.xml
new file mode 100644
index 0000000..7c017ca
--- /dev/null
+++ b/src/test/resources/testConfigurationXMLDocument.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Configuration test file that demonstrates usage of ConfigurationXMLDocument -->
+
+<configuration>
+ <additional>
+ <hierarchicalXml fileName="testHierarchicalXMLConfiguration.xml" at="database"/>
+ <hierarchicalXml fileName="testDigesterConfigurationInclude1.xml" at="database.tables"/>
+ <hierarchicalXml fileName="testDigesterCreateObject.xml" at="database"/>
+ </additional>
+</configuration>
diff --git a/src/test/resources/testDigesterBadXML.xml b/src/test/resources/testDigesterBadXML.xml
new file mode 100644
index 0000000..2f10a33
--- /dev/null
+++ b/src/test/resources/testDigesterBadXML.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Configuration test file that tests that bad
+ XML generates a SAXException that is properly
+ wrapped in a ConfigurationInitializationException -->
+
+<configuration>
+ <additional>
+ <hierarchicalXml fileName="testHierarchicalXMLConfiguration.xml"/>
+
+</configuration>
diff --git a/src/test/resources/testDigesterConfiguration.xml b/src/test/resources/testDigesterConfiguration.xml
new file mode 100644
index 0000000..a3253b5
--- /dev/null
+++ b/src/test/resources/testDigesterConfiguration.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<configuration>
+ <properties fileName="test.properties"/>
+ <properties fileName="test.properties.xml"/>
+ <xml fileName="test.xml"/>
+</configuration>
diff --git a/src/test/resources/testDigesterConfiguration2.xml b/src/test/resources/testDigesterConfiguration2.xml
new file mode 100644
index 0000000..81e1a8e
--- /dev/null
+++ b/src/test/resources/testDigesterConfiguration2.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Configuration test file that demonstrates the
+ override and additional sections -->
+
+<configuration>
+ <additional>
+ <hierarchicalXml fileName="testHierarchicalXMLConfiguration.xml"/>
+ <hierarchicalXml fileName="testDigesterConfigurationInclude1.xml" at="tables"/>
+ <properties fileName="testDigesterConfigurationInclude2.properties" at="mail"/>
+ </additional>
+
+ <override>
+ <properties fileName="testDigesterConfigurationOverwrite.properties"/>
+ </override>
+</configuration>
diff --git a/src/test/resources/testDigesterConfiguration3.xml b/src/test/resources/testDigesterConfiguration3.xml
new file mode 100644
index 0000000..6240d55
--- /dev/null
+++ b/src/test/resources/testDigesterConfiguration3.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Configuration test file that checks all known configuration types -->
+
+<configuration>
+ <additional>
+ <system/>
+ <xml fileName="test.xml"/>
+ <hierarchicalXml fileName="testDigesterConfigurationInclude1.xml" at="tables"/>
+ <properties fileName="testDigesterConfigurationInclude2.properties" at="mail"/>
+ <jndi prefix=""/>
+ <ini fileName="test.ini"/>
+ <env at="env"/>
+ </additional>
+</configuration>
diff --git a/src/test/resources/testDigesterConfigurationBasePath.xml b/src/test/resources/testDigesterConfigurationBasePath.xml
new file mode 100644
index 0000000..f1a5ea9
--- /dev/null
+++ b/src/test/resources/testDigesterConfigurationBasePath.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<configuration>
+ <properties fileName="test.properties"/>
+</configuration>
+
+
+
+
+
+
diff --git a/src/test/resources/testDigesterConfigurationInclude1.xml b/src/test/resources/testDigesterConfigurationInclude1.xml
new file mode 100644
index 0000000..eff6e6d
--- /dev/null
+++ b/src/test/resources/testDigesterConfigurationInclude1.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<config>
+ <table tableType="application">
+ <name>tasks</name>
+ <fields>
+ <field>
+ <name>taskid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>name</name>
+ <type>java.lang.String</type>
+ </field>
+ <field>
+ <name>description</name>
+ <type>java.lang.String</type>
+ </field>
+ <field>
+ <name>responsibleID</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>creatorID</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>startDate</name>
+ <type>java.util.Date</type>
+ </field>
+ <field>
+ <name>endDate</name>
+ <type>java.util.Date</type>
+ </field>
+ </fields>
+ </table>
+</config>
\ No newline at end of file
diff --git a/src/test/resources/testDigesterConfigurationInclude2.properties b/src/test/resources/testDigesterConfigurationInclude2.properties
new file mode 100644
index 0000000..d6d2b14
--- /dev/null
+++ b/src/test/resources/testDigesterConfigurationInclude2.properties
@@ -0,0 +1,22 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Properties for Email configuration
+
+host.smtp = smtp.mydomain.org
+host.pop = pop3.mydomain.org
+account.user = postmaster
+account.psswd = secret
+account.type = pop3
diff --git a/src/test/resources/testDigesterConfigurationNamespaceAware.xml b/src/test/resources/testDigesterConfigurationNamespaceAware.xml
new file mode 100644
index 0000000..4ef922d
--- /dev/null
+++ b/src/test/resources/testDigesterConfigurationNamespaceAware.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<configuration xmlns:foo="namespace-one"
+ xmlns:bar="namespace-two">
+ <foo:properties fileName="test.properties"/>
+ <bar:xml fileName="test.xml"/>
+</configuration>
diff --git a/src/test/resources/testDigesterConfigurationOverwrite.properties b/src/test/resources/testDigesterConfigurationOverwrite.properties
new file mode 100644
index 0000000..f6482c2
--- /dev/null
+++ b/src/test/resources/testDigesterConfigurationOverwrite.properties
@@ -0,0 +1,21 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Properties for Email configuration
+
+mail.account.user = masterOfPost
+mail.account.psswd = topsecret
+
+test.configuration = enhanced factory
diff --git a/src/test/resources/testDigesterConfigurationReverseOrder.xml b/src/test/resources/testDigesterConfigurationReverseOrder.xml
new file mode 100644
index 0000000..ffcbbb4
--- /dev/null
+++ b/src/test/resources/testDigesterConfigurationReverseOrder.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<configuration>
+ <xml fileName="test.xml"/>
+ <properties fileName="test.properties"/>
+</configuration>
diff --git a/src/test/resources/testDigesterConfigurationSysProps.xml b/src/test/resources/testDigesterConfigurationSysProps.xml
new file mode 100644
index 0000000..0afdd23
--- /dev/null
+++ b/src/test/resources/testDigesterConfigurationSysProps.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- A test configuration file for loading a file specified by a
+ system property.
+-->
+<configuration>
+ <properties fileName="${config.file}"/>
+</configuration>
diff --git a/src/test/resources/testDigesterConfigurationWithProps.xml b/src/test/resources/testDigesterConfigurationWithProps.xml
new file mode 100644
index 0000000..750e4ac
--- /dev/null
+++ b/src/test/resources/testDigesterConfigurationWithProps.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<configuration>
+ <properties fileName="test.properties" listDelimiter=";"/>
+</configuration>
diff --git a/src/test/resources/testDigesterCreateObject.xml b/src/test/resources/testDigesterCreateObject.xml
new file mode 100644
index 0000000..7399341
--- /dev/null
+++ b/src/test/resources/testDigesterCreateObject.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration>
+ <connection>
+ <class name="org.apache.commons.configuration.TestConfigurationXMLDocument$ConnectionData">
+ <property name="dsn" value="MyData"/>
+ <property name="user" value="scott"/>
+ <property name="passwd" value="tiger"/>
+ </class>
+ </connection>
+</configuration>
diff --git a/src/test/resources/testDigesterOptionalConfiguration.xml b/src/test/resources/testDigesterOptionalConfiguration.xml
new file mode 100644
index 0000000..a71d0ab
--- /dev/null
+++ b/src/test/resources/testDigesterOptionalConfiguration.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Configuration test file that demonstrates
+ optional configurations -->
+
+<configuration>
+ <properties fileName="test.properties"/>
+ <xml fileName="test.xml" optional="false"/>
+ <hierarchicalXml fileName="nonExistingHierarchicalXML1.xml" optional="true"/>
+ <properties fileName="nonExistingProperties1.properties" optional="yes"/>
+ <xml fileName="nonExistingXML1.xml" optional="true"/>
+
+ <additional>
+ <properties fileName="nonExistingProperties2.properties" optional="yes"/>
+ <hierarchicalXml fileName="nonExistingHierarchicalXML2.xml" optional="true"/>
+ <xml fileName="nonExistingXML2.xml" optional="true"/>
+ </additional>
+</configuration>
diff --git a/src/test/resources/testDigesterOptionalConfigurationEx.xml b/src/test/resources/testDigesterOptionalConfigurationEx.xml
new file mode 100644
index 0000000..f1fded4
--- /dev/null
+++ b/src/test/resources/testDigesterOptionalConfigurationEx.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Configuration test file that is used to test whether invalid
+ file names cause exceptions to be thrown -->
+
+<configuration>
+ <!-- This should not cause an exception -->
+ <properties fileName="unexisting.properties" optional="true"/>
+ <!-- But this should! -->
+ <properties fileName="unexisting.properties"/>
+</configuration>
diff --git a/src/test/resources/testDtd.xml b/src/test/resources/testDtd.xml
new file mode 100644
index 0000000..4d5bb54
--- /dev/null
+++ b/src/test/resources/testDtd.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Tests whether a DTD can be referenced when loaded from XMLConfiguration -->
+<!DOCTYPE properties SYSTEM "properties.dtd">
+<properties version="1.0">
+ <entry key="test1">value1</entry>
+ <entry key="test2">value2</entry>
+</properties>
\ No newline at end of file
diff --git a/src/test/resources/testEncoding.xml b/src/test/resources/testEncoding.xml
new file mode 100644
index 0000000..9fdf77a
Binary files /dev/null and b/src/test/resources/testEncoding.xml differ
diff --git a/src/test/resources/testEqual.properties b/src/test/resources/testEqual.properties
new file mode 100644
index 0000000..b5d818f
--- /dev/null
+++ b/src/test/resources/testEqual.properties
@@ -0,0 +1,26 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+property.a = a
+property.b = b
+property.c = 100
+
+#
+# Value set twice
+property.a = aa
+
+clear.property = delete me
+
+existing.property = i exist
+
diff --git a/src/test/resources/testEqualDigester.xml b/src/test/resources/testEqualDigester.xml
new file mode 100644
index 0000000..086c0d2
--- /dev/null
+++ b/src/test/resources/testEqualDigester.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<configuration>
+ <properties fileName="testEqual.properties"/>
+</configuration>
diff --git a/src/test/resources/testExpression.xml b/src/test/resources/testExpression.xml
new file mode 100644
index 0000000..2d90e40
--- /dev/null
+++ b/src/test/resources/testExpression.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true"
+ config-class="org.apache.commons.configuration.DynamicCombinedConfiguration"
+ keyPattern="$${sys:Id}">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.MergeCombiner"/>
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <lookups>
+ <lookup config-prefix="mdc" config-class="org.slf4j.ext.MDCStrLookup"/>
+ <lookup config-prefix="expr"
+ config-class="org.apache.commons.configuration.interpol.ExprLookup">
+ <variables>
+ <variable name="String" value="Class:org.apache.commons.lang.StringUtils"/>
+ <variable name="MDC" value="Class:org.slf4j.MDC"/>
+ </variables>
+ </lookup>
+ </lookups>
+ <providers>
+ <provider config-tag="multifile"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$FileConfigurationProvider"
+ configurationClass="org.apache.commons.configuration.MultiFileHierarchicalConfiguration"/>
+ </providers>
+ </header>
+ <override>
+ <multifile filePattern='$$${expr:String.right(MDC.get("Id"), 2)}/testMultiConfiguration_$$${sys:Id}.xml'
+ config-name="clientConfig" delimiterParsingDisabled="true">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </multifile>
+ <xml fileName="testMultiConfiguration_default.xml"
+ config-name="defaultConfig" delimiterParsingDisabled="true">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </xml>
+ </override>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testExtendedClass.xml b/src/test/resources/testExtendedClass.xml
new file mode 100644
index 0000000..f87c5b1
--- /dev/null
+++ b/src/test/resources/testExtendedClass.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true"
+ config-class="org.apache.commons.configuration.TestDefaultConfigurationBuilder$ExtendedCombinedConfiguration">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.OverrideCombiner"/>
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <combiner>
+ <override>
+ <list-nodes>
+ <node>table</node>
+ <node>list</node>
+ </list-nodes>
+ </override>
+ </combiner>
+ </header>
+ <system/>
+ <properties fileName="test.properties" throwExceptionOnMissing="true"
+ config-name="properties">
+ <reloadingStrategy config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"
+ refreshDelay="10000"/>
+ </properties>
+ <!-- Fetch the file name from a variable -->
+ <xml fileName="${test_file_xml}" config-name="xml">
+ <expressionEngine config-class="org.apache.commons.configuration.tree.DefaultExpressionEngine"
+ propertyDelimiter="/" indexStart="[" indexEnd="]"/>
+ </xml>
+ <additional>
+ <xml config-name="combiner1" fileName="${test_file_combine}"/> -->
+ <xml config-name="combiner2" fileName="testcombine2.xml"/>
+ </additional>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testExtendedXMLConfigurationProvider.xml b/src/test/resources/testExtendedXMLConfigurationProvider.xml
new file mode 100644
index 0000000..d857263
--- /dev/null
+++ b/src/test/resources/testExtendedXMLConfigurationProvider.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.OverrideCombiner"/>
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <providers>
+ <provider config-tag="test"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$XMLConfigurationProvider"
+ configurationClass="org.apache.commons.configuration.TestDefaultConfigurationBuilder$ExtendedXMLConfiguration"/>
+ </providers>
+ <combiner>
+ <override>
+ <list-nodes>
+ <node>table</node>
+ <node>list</node>
+ </list-nodes>
+ </override>
+ </combiner>
+ </header>
+ <system/>
+ <properties fileName="test.properties" throwExceptionOnMissing="true"
+ config-name="properties">
+ <reloadingStrategy config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"
+ refreshDelay="10000"/>
+ </properties>
+ <!-- Fetch the file name from a variable -->
+ <test fileName="${test_file_xml}" config-name="xml">
+ <expressionEngine config-class="org.apache.commons.configuration.tree.DefaultExpressionEngine"
+ propertyDelimiter="/" indexStart="[" indexEnd="]"/>
+ </test>
+ <additional>
+ <xml config-name="combiner1" fileName="${test_file_combine}"/> -->
+ <xml config-name="combiner2" fileName="testcombine2.xml"/>
+ </additional>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testFactoryPropertiesInclude.xml b/src/test/resources/testFactoryPropertiesInclude.xml
new file mode 100644
index 0000000..f4b4137
--- /dev/null
+++ b/src/test/resources/testFactoryPropertiesInclude.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Tests embedding properties in a deep directory structure, which in turn
+ include another property file.
+-->
+<configuration>
+ <properties fileName="config/deep/deeptest.properties"/>
+ <properties fileName="config/deep/deeptestinvalid.properties" optional="true"/>
+</configuration>
diff --git a/src/test/resources/testFileReloadConfigurationBuilder.xml b/src/test/resources/testFileReloadConfigurationBuilder.xml
new file mode 100644
index 0000000..fc70ec2
--- /dev/null
+++ b/src/test/resources/testFileReloadConfigurationBuilder.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true"
+ config-class="org.apache.commons.configuration.DynamicCombinedConfiguration"
+ keyPattern="$${sys:Id}">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.MergeCombiner"/>
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <fileSystem config-class="org.apache.commons.configuration.VFSFileSystem"/>
+ <providers>
+ <provider config-tag="multifile"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$FileConfigurationProvider"
+ configurationClass="org.apache.commons.configuration.MultiFileHierarchicalConfiguration"/>
+ </providers>
+ </header>
+ <override>
+ <multifile filePattern="testwrite/testMultiConfiguration_$$${sys:Id}.xml"
+ config-name="clientConfig" delimiterParsingDisabled="true" schemaValidation="false">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="0"
+ config-class="org.apache.commons.configuration.reloading.VFSFileChangedReloadingStrategy"/>
+ </multifile>
+ <xml fileName="testMultiConfiguration_default.xml"
+ config-name="defaultConfig" delimiterParsingDisabled="true">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="0"
+ config-class="org.apache.commons.configuration.reloading.VFSFileChangedReloadingStrategy"/>
+ </xml>
+ </override>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testFileReloadConfigurationBuilder2.xml b/src/test/resources/testFileReloadConfigurationBuilder2.xml
new file mode 100644
index 0000000..99295ce
--- /dev/null
+++ b/src/test/resources/testFileReloadConfigurationBuilder2.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true"
+ config-class="org.apache.commons.configuration.DynamicCombinedConfiguration"
+ keyPattern="$${sys:Id}">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.MergeCombiner"/>
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <fileSystem config-class="org.apache.commons.configuration.VFSFileSystem"/>
+ <providers>
+ <provider config-tag="multifile"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$FileConfigurationProvider"
+ configurationClass="org.apache.commons.configuration.MultiFileHierarchicalConfiguration"/>
+ </providers>
+ </header>
+ <override>
+ <multifile filePattern="${sys:basePath}/testwrite/testMultiConfiguration_$$${sys:Id}.xml"
+ config-name="clientConfig" delimiterParsingDisabled="true" schemaValidation="false">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="500"
+ config-class="org.apache.commons.configuration.reloading.VFSFileChangedReloadingStrategy"/>
+ </multifile>
+ <xml fileName="testMultiConfiguration_default.xml"
+ config-name="defaultConfig" delimiterParsingDisabled="true">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="500"
+ config-class="org.apache.commons.configuration.reloading.VFSFileChangedReloadingStrategy"/>
+ </xml>
+ </override>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testFileSystem.xml b/src/test/resources/testFileSystem.xml
new file mode 100644
index 0000000..200f459
--- /dev/null
+++ b/src/test/resources/testFileSystem.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.OverrideCombiner"/>
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <fileSystem config-class="org.apache.commons.configuration.VFSFileSystem"/>
+ <providers>
+ <provider config-tag="test"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$FileConfigurationProvider"/>
+ </providers>
+ <combiner>
+ <override>
+ <list-nodes>
+ <node>table</node>
+ <node>list</node>
+ </list-nodes>
+ </override>
+ </combiner>
+ </header>
+ <system/>
+ <properties fileName="test.properties" throwExceptionOnMissing="true"
+ config-name="properties">
+ <reloadingStrategy config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"
+ refreshDelay="10000"/>
+ </properties>
+ <!-- Fetch the file name from a variable -->
+ <xml fileName="${test_file_xml}" config-name="xml">
+ <expressionEngine config-class="org.apache.commons.configuration.tree.DefaultExpressionEngine"
+ propertyDelimiter="/" indexStart="[" indexEnd="]"/>
+ </xml>
+ <additional>
+ <xml config-name="combiner1" fileName="${test_file_combine}"/> -->
+ <xml config-name="combiner2" fileName="testcombine2.xml"/>
+ </additional>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testGlobalLookup.xml b/src/test/resources/testGlobalLookup.xml
new file mode 100644
index 0000000..0fa2a8a
--- /dev/null
+++ b/src/test/resources/testGlobalLookup.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.OverrideCombiner"/>
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <lookups>
+ <lookup config-prefix="test"
+ config-class="org.apache.commons.configuration.TestDefaultConfigurationBuilder$TestLookup"/>
+ </lookups>
+ <combiner>
+ <override>
+ <list-nodes>
+ <node>table</node>
+ <node>list</node>
+ </list-nodes>
+ </override>
+ </combiner>
+ </header>
+ <system/>
+ <properties fileName="test.properties" throwExceptionOnMissing="true"
+ config-name="properties">
+ <reloadingStrategy config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"
+ refreshDelay="10000"/>
+ </properties>
+ <!-- Fetch the file name from a variable -->
+ <xml fileName="${test:test_file_xml}" config-name="xml">
+ <expressionEngine config-class="org.apache.commons.configuration.tree.DefaultExpressionEngine"
+ propertyDelimiter="/" indexStart="[" indexEnd="]"/>
+ </xml>
+ <additional>
+ <xml config-name="combiner1" fileName="${test:test_file_combine}"/> -->
+ <xml config-name="combiner2" fileName="testcombine2.xml"/>
+ </additional>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testHierarchicalXMLConfiguration.xml b/src/test/resources/testHierarchicalXMLConfiguration.xml
new file mode 100644
index 0000000..626fadd
--- /dev/null
+++ b/src/test/resources/testHierarchicalXMLConfiguration.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<database>
+ <tables>
+ <table tableType="system">
+ <name>users</name>
+ <fields>
+ <field>
+ <name>uid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>uname</name>
+ <type>java.lang.String</type>
+ </field>
+ <field>
+ <name>firstName</name>
+ <type>java.lang.String</type>
+ </field>
+ <field>
+ <name>lastName</name>
+ <type>java.lang.String</type>
+ </field>
+ <field>
+ <name>email</name>
+ <type>java.lang.String</type>
+ </field>
+ </fields>
+ </table>
+ <table tableType="application">
+ <name>documents</name>
+ <fields>
+ <field>
+ <name>docid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>name</name>
+ <type>java.lang.String</type>
+ </field>
+ <field>
+ <name>creationDate</name>
+ <type>java.util.Date</type>
+ </field>
+ <field>
+ <name>authorID</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>version</name>
+ <type>int</type>
+ </field>
+ </fields>
+ </table>
+ </tables>
+</database>
diff --git a/src/test/resources/testHierarchicalXMLConfiguration2.xml b/src/test/resources/testHierarchicalXMLConfiguration2.xml
new file mode 100644
index 0000000..5072566
--- /dev/null
+++ b/src/test/resources/testHierarchicalXMLConfiguration2.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Test config to confirm proper consumption of various XML nodes. -->
+<config>
+ <case1>
+ <!-- Comment contained in element. -->
+ Case1Text
+ </case1>
+ <case2>
+ <!-- Comment sibling to element. -->
+ <child>
+
+ Case2Text
+ </child>
+ </case2>
+ <case3>
+ <!-- Comment sibling to CDATA. -->
+ <![CDATA[
+ Case3Text
+ ]]>
+ </case3>
+ <case4>
+ <!-- Comment sibling to PI and text. -->
+
+ <?xml-stylesheet href="pi.css" type="text/css" ?>
+ Case4Text
+ </case4>
+ <case5 attr="Case5Text">
+ <!-- Comment in element with text in attribute -->
+ </case5>
+</config>
diff --git a/src/test/resources/testInterpolation.properties b/src/test/resources/testInterpolation.properties
new file mode 100644
index 0000000..31a0580
--- /dev/null
+++ b/src/test/resources/testInterpolation.properties
@@ -0,0 +1,22 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# A test configuration file for testing variable substitution across multiple
+# configuration sources created by a DefaultConfigurationBuilder.
+# This properties configuration defines a property which is referenced by
+# another configuration source.
+# $Id: testInterpolation.properties 1295276 2012-02-29 21:11:35Z oheger $
+myvar=abc
+
diff --git a/src/test/resources/testInterpolation.xml b/src/test/resources/testInterpolation.xml
new file mode 100644
index 0000000..2f7b3ee
--- /dev/null
+++ b/src/test/resources/testInterpolation.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!--
+ A test configuration file for testing variable substitution across multiple
+ configuration sources created by a DefaultConfigurationBuilder.
+ This XML configuration references a variable defined by another configuration
+ source.
+ $Id: testInterpolation.xml 1295276 2012-02-29 21:11:35Z oheger $
+-->
+<test>
+ <products>
+ <product name="abc">
+ <desc>${myvar}-product</desc>
+ </product>
+ </products>
+</test>
diff --git a/src/test/resources/testInterpolationBuilder.xml b/src/test/resources/testInterpolationBuilder.xml
new file mode 100644
index 0000000..55651b6
--- /dev/null
+++ b/src/test/resources/testInterpolationBuilder.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!--
+ A test configuration file for testing variable substitution across multiple
+ configuration sources created by a DefaultConfigurationBuilder.
+ This configuration definition file for a DefaultConfigurationBuilder
+ references multiple sources which contain variable references.
+ $Id: testInterpolationBuilder.xml 1295276 2012-02-29 21:11:35Z oheger $
+-->
+<configuration>
+ <system/>
+ <properties fileName="testInterpolation.properties"/>
+ <xml fileName="testInterpolation.xml" config-name="test">
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </xml>
+</configuration>
diff --git a/src/test/resources/testMultiConfiguration.xsd b/src/test/resources/testMultiConfiguration.xsd
new file mode 100644
index 0000000..84c7c56
--- /dev/null
+++ b/src/test/resources/testMultiConfiguration.xsd
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="configuration" type="configurationType"/>
+ <xs:complexType name="configurationType">
+ <xs:sequence>
+ <xs:element type="colorsType" name="colors" minOccurs="0"/>
+ <xs:element type="xs:integer" name="rowsPerPage" minOccurs="0"/>
+ <xs:element type="buttonsType" name="buttons" minOccurs="0"/>
+ <xs:element type="numberFormatType" name="numberFormat" minOccurs="0"/>
+ <xs:element type="splitType" name="split" minOccurs="0">
+ <xs:annotation>
+ <xs:documentation>Comma delimited lists</xs:documentation>
+ </xs:annotation>
+ </xs:element>
+ <xs:element type="ChannelsType" name="Channels" minOccurs="0"/>
+ </xs:sequence>
+ </xs:complexType>
+ <xs:complexType name="splitType">
+ <xs:sequence>
+ <xs:element type="xs:string" name="list1" minOccurs="0"/>
+ <xs:element type="xs:string" name="list2" minOccurs="0"/>
+ <xs:element type="list3Type" name="list3" minOccurs="0"/>
+ <xs:element type="list4Type" name="list4" minOccurs="0"/>
+ </xs:sequence>
+ </xs:complexType>
+ <xs:complexType name="colorsType">
+ <xs:sequence>
+ <xs:element type="xs:string" name="background" minOccurs="0"/>
+ <xs:element type="xs:string" name="text" minOccurs="0"/>
+ <xs:element type="xs:string" name="header" minOccurs="0"/>
+ <xs:element type="linkType" name="link" minOccurs="0"/>
+ <xs:element type="xs:string" name="default" minOccurs="0"/>
+ </xs:sequence>
+ </xs:complexType>
+ <xs:complexType name="list4Type">
+ <xs:simpleContent>
+ <xs:extension base="xs:string">
+ <xs:attribute type="xs:string" name="values"/>
+ </xs:extension>
+ </xs:simpleContent>
+ </xs:complexType>
+ <xs:complexType name="linkType">
+ <xs:simpleContent>
+ <xs:extension base="xs:string">
+ <xs:attribute type="xs:string" name="normal"/>
+ <xs:attribute type="xs:string" name="visited"/>
+ </xs:extension>
+ </xs:simpleContent>
+ </xs:complexType>
+ <xs:complexType name="list3Type">
+ <xs:simpleContent>
+ <xs:extension base="xs:string">
+ <xs:attribute type="xs:string" name="values"/>
+ </xs:extension>
+ </xs:simpleContent>
+ </xs:complexType>
+ <xs:complexType name="buttonsType">
+ <xs:sequence>
+ <xs:element type="xs:string" name="name" minOccurs="0"/>
+ </xs:sequence>
+ </xs:complexType>
+ <xs:complexType name="numberFormatType">
+ <xs:simpleContent>
+ <xs:extension base="xs:string">
+ <xs:attribute type="xs:string" name="pattern"/>
+ </xs:extension>
+ </xs:simpleContent>
+ </xs:complexType>
+ <xs:complexType name="ChannelType">
+ <xs:sequence>
+ <xs:element name="Name" minOccurs="0">
+ <xs:simpleType>
+ <xs:restriction base="xs:string">
+ </xs:restriction>
+ </xs:simpleType>
+ </xs:element>
+ <xs:element name="ChannelData" minOccurs="0">
+ <xs:simpleType>
+ <xs:restriction base="xs:string">
+ </xs:restriction>
+ </xs:simpleType>
+ </xs:element>
+ <xs:element name="MoreChannelData" minOccurs="0">
+ <xs:simpleType>
+ <xs:restriction base="xs:string">
+ </xs:restriction>
+ </xs:simpleType>
+ </xs:element>
+ </xs:sequence>
+ <xs:attribute type="xs:string" name="id" use="optional"/>
+ </xs:complexType>
+ <xs:complexType name="ChannelsType">
+ <xs:choice maxOccurs="unbounded" minOccurs="0">
+ <xs:element type="ChannelType" name="Channel" minOccurs="0"/>
+ </xs:choice>
+ </xs:complexType>
+</xs:schema>
\ No newline at end of file
diff --git a/src/test/resources/testMultiConfiguration_1001.xml b/src/test/resources/testMultiConfiguration_1001.xml
new file mode 100644
index 0000000..6e96ee9
--- /dev/null
+++ b/src/test/resources/testMultiConfiguration_1001.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration>
+ <colors>
+ <background>#808080</background>
+ <text>#000000</text>
+ <header>#008000</header>
+ <link normal="#000080" visited="#800080"/>
+ <default>${colors.header}</default>
+ </colors>
+ <rowsPerPage>15</rowsPerPage>
+ <buttons>
+ <name>OK,Cancel,Help</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+ <Channels>
+ <Channel id="1">
+ <Name>My Channel</Name>
+ </Channel>
+ <Channel id="2">
+ <MoreChannelData>more test 2 data</MoreChannelData>
+ </Channel>
+ </Channels>
+</configuration>
diff --git a/src/test/resources/testMultiConfiguration_1002.xml b/src/test/resources/testMultiConfiguration_1002.xml
new file mode 100644
index 0000000..908ac81
--- /dev/null
+++ b/src/test/resources/testMultiConfiguration_1002.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration>
+ <colors>
+ <background>#2222222</background>
+ <text>#000000</text>
+ <header>#222222</header>
+ <link normal="#020202" visited="#202020"/>
+ <default>${colors.header3}</default>
+ </colors>
+ <rowsPerPage>25</rowsPerPage>
+ <buttons>
+ <name>OK-2,Cancel-2,Help-2</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+</configuration>
diff --git a/src/test/resources/testMultiConfiguration_1003.xml b/src/test/resources/testMultiConfiguration_1003.xml
new file mode 100644
index 0000000..ee54e92
--- /dev/null
+++ b/src/test/resources/testMultiConfiguration_1003.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration>
+ <colors>
+ <background>#333333</background>
+ <text>#FFFFFF</text>
+ <header>#333333</header>
+ <link normal="#030303" visited="#303030"/>
+ <default>${colors.header3}</default>
+ </colors>
+ <rowsPerPage>35</rowsPerPage>
+ <buttons>
+ <name>OK-3,Cancel-3,Help-3</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+</configuration>
diff --git a/src/test/resources/testMultiConfiguration_1004.xml b/src/test/resources/testMultiConfiguration_1004.xml
new file mode 100644
index 0000000..2fafc2e
--- /dev/null
+++ b/src/test/resources/testMultiConfiguration_1004.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration>
+ <colors>
+ <default>${colors.header4}</default>
+ </colors>
+ <buttons>
+ <name>OK-1,Cancel-2,Help-3</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testMultiConfiguration_2001.xml b/src/test/resources/testMultiConfiguration_2001.xml
new file mode 100644
index 0000000..a1173f3
--- /dev/null
+++ b/src/test/resources/testMultiConfiguration_2001.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="http://commons.apache.org/testMultiConfiguration.xsd">
+ <colors>
+ <undefined>This will throw a schema exception</undefined>
+ </colors>
+ <buttons>
+ <name>OK-1,Cancel-2,Help-3</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testMultiConfiguration_2002.xml b/src/test/resources/testMultiConfiguration_2002.xml
new file mode 100644
index 0000000..4c52d04
--- /dev/null
+++ b/src/test/resources/testMultiConfiguration_2002.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="http://commons.apache.org/testMultiConfiguration.xsd">
+ <colors>
+ <background>#2222222</background>
+ <text>#000000</text>
+ <header>#222222</header>
+ <link normal="#020202" visited="#202020"/>
+ <default>${colors.header3}</default>
+ </colors>
+ <rowsPerPage>25</rowsPerPage>
+ <buttons>
+ <name>OK-2,Cancel-2,Help-2</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testMultiConfiguration_3001.xml b/src/test/resources/testMultiConfiguration_3001.xml
new file mode 100644
index 0000000..44e665c
--- /dev/null
+++ b/src/test/resources/testMultiConfiguration_3001.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="http://commons.apache.org/testMultiConfiguration.xsd">
+ <colors>
+ <background>#808080</background>
+ <text>#000000</text>
+ <header>#008000</header>
+ <link normal="#000080" visited="#800080"/>
+ <default>${colors.header}</default>
+ </colors>
+ <rowsPerPage>15</rowsPerPage>
+ <buttons>
+ <name>OK,Cancel,Help</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+ <Channels>
+ <Channel id="1">
+ <Name>My Channel</Name>
+ </Channel>
+ <Channel id="2">
+ <MoreChannelData>more test 2 data</MoreChannelData>
+ </Channel>
+ </Channels>
+</configuration>
diff --git a/src/test/resources/testMultiConfiguration_3002.xml b/src/test/resources/testMultiConfiguration_3002.xml
new file mode 100644
index 0000000..3fe8d97
--- /dev/null
+++ b/src/test/resources/testMultiConfiguration_3002.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="http://commons.apache.org/testMultiConfiguration.xsd">
+ <colors>
+ <background>#2222222</background>
+ <text>#000000</text>
+ <header>#222222</header>
+ <link normal="#020202" visited="#202020"/>
+ <default>${colors.header3}</default>
+ </colors>
+ <rowsPerPage>25</rowsPerPage>
+ <buttons>
+ <name>OK-2,Cancel-2,Help-2</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+</configuration>
diff --git a/src/test/resources/testMultiConfiguration_default.xml b/src/test/resources/testMultiConfiguration_default.xml
new file mode 100644
index 0000000..cc23f25
--- /dev/null
+++ b/src/test/resources/testMultiConfiguration_default.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="http://commons.apache.org/testMultiConfiguration.xsd">
+ <colors>
+ <background>#40404040</background>
+ <text>#000000</text>
+ <header>#444444</header>
+ <link normal="#040404" visited="#404040"/>
+ <default>${colors.default}</default>
+ </colors>
+ <rowsPerPage>50</rowsPerPage>
+ <buttons>
+ <name>OK-4,Cancel-4,Help-4</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+ <!-- Comma delimited lists -->
+ <split>
+ <list1>a,b,c</list1>
+ <list2>a\,b\,c</list2>
+ <list3 values="a,b,c"/>
+ <list4 values="a\,b\,c"/>
+ </split>
+ <Channels>
+ <Channel id="1">
+ <Name>Channel 1</Name>
+ <ChannelData>test 1 data</ChannelData>
+ </Channel>
+ <Channel id="2">
+ <Name>Channel 2</Name>
+ <ChannelData>test 2 data</ChannelData>
+ </Channel>
+ </Channels>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testMultiDynamic_default.xml b/src/test/resources/testMultiDynamic_default.xml
new file mode 100644
index 0000000..bc4b6a8
--- /dev/null
+++ b/src/test/resources/testMultiDynamic_default.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="http://commons.apache.org/testMultiConfiguration.xsd">
+ <colors>
+ <background>#40404040</background>
+ <text>#000000</text>
+ <header>#444444</header>
+ <link normal="#040404" visited="#404040"/>
+ <default>${colors.default}</default>
+ </colors>
+ <Product>
+ <FIIndex>
+ <FI id="123456781">ID0001</FI>
+ <FI id="AUTO01">IDAUTO01</FI>
+ <FI id="AUTO02">IDAUTO02</FI>
+ </FIIndex>
+ </Product>
+ <rowsPerPage>50</rowsPerPage>
+ <buttons>
+ <name>OK-4,Cancel-4,Help-4</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+ <!-- Comma delimited lists -->
+ <split>
+ <list1>a,b,c</list1>
+ <list2>a\,b\,c</list2>
+ <list3 values="a,b,c"/>
+ <list4 values="a\,b\,c"/>
+ </split>
+ <Channels>
+ <Channel id="1">
+ <Name>Channel 1</Name>
+ <ChannelData>test 1 data</ChannelData>
+ </Channel>
+ <Channel id="2">
+ <Name>Channel 2</Name>
+ <ChannelData>test 2 data</ChannelData>
+ </Channel>
+ </Channels>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testMultiDynamic_default2.xml b/src/test/resources/testMultiDynamic_default2.xml
new file mode 100644
index 0000000..44ff7fc
--- /dev/null
+++ b/src/test/resources/testMultiDynamic_default2.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="http://commons.apache.org/testMultiConfiguration.xsd">
+ <colors>
+ <background>#40404040</background>
+ <text>#000000</text>
+ <header>#444444</header>
+ <link normal="#040404" visited="#404040"/>
+ <default>${colors.default}</default>
+ </colors>
+ <Product>
+ <FIIndex>
+ <FI id="123456781">ID0001</FI>
+ <FI id="123456782">ID0002</FI>
+ <FI id="AUTO01">IDAUTO01</FI>
+ <FI id="AUTO02">IDAUTO02</FI>
+ </FIIndex>
+ </Product>
+ <rowsPerPage>25</rowsPerPage>
+ <buttons>
+ <name>OK-4,Cancel-4,Help-4</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+ <!-- Comma delimited lists -->
+ <split>
+ <list1>a,b,c</list1>
+ <list2>a\,b\,c</list2>
+ <list3 values="a,b,c"/>
+ <list4 values="a\,b\,c"/>
+ </split>
+ <Channels>
+ <Channel id="1">
+ <Name>Channel 1</Name>
+ <ChannelData>test 1 data</ChannelData>
+ </Channel>
+ <Channel id="2">
+ <Name>Channel 2</Name>
+ <ChannelData>test 2 data</ChannelData>
+ </Channel>
+ </Channels>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testMultiTenentConfigurationBuilder.xml b/src/test/resources/testMultiTenentConfigurationBuilder.xml
new file mode 100644
index 0000000..a053316
--- /dev/null
+++ b/src/test/resources/testMultiTenentConfigurationBuilder.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true" loggerName="TestLogger"
+ config-class="org.apache.commons.configuration.DynamicCombinedConfiguration"
+ keyPattern="$${sys:Id}">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.MergeCombiner"/>
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <providers>
+ <provider config-tag="multifile"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$FileConfigurationProvider"
+ configurationClass="org.apache.commons.configuration.MultiFileHierarchicalConfiguration"/>
+ </providers>
+ </header>
+ <override>
+ <multifile filePattern="testMultiConfiguration_$$${sys:Id}.xml"
+ config-name="clientConfig" delimiterParsingDisabled="true" schemaValidation="false">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </multifile>
+ <xml fileName="testMultiConfiguration_default.xml"
+ config-name="defaultConfig" delimiterParsingDisabled="true">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </xml>
+ </override>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testMultiTenentConfigurationBuilder2.xml b/src/test/resources/testMultiTenentConfigurationBuilder2.xml
new file mode 100644
index 0000000..df2ba5c
--- /dev/null
+++ b/src/test/resources/testMultiTenentConfigurationBuilder2.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true" loggerName="TestLogger"
+ config-class="org.apache.commons.configuration.DynamicCombinedConfiguration"
+ keyPattern="$${sys:Id}">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.MergeCombiner"/>
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <entity-resolver catalogFiles="catalog.xml"/>
+ <providers>
+ <provider config-tag="multifile"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$FileConfigurationProvider"
+ configurationClass="org.apache.commons.configuration.MultiFileHierarchicalConfiguration"/>
+ </providers>
+ </header>
+ <override>
+ <multifile filePattern="testMultiConfiguration_$$${sys:Id}.xml"
+ config-name="clientConfig" delimiterParsingDisabled="true" schemaValidation="true">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="500"
+ config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"/>
+ </multifile>
+ <xml fileName="testMultiConfiguration_default.xml"
+ config-name="defaultConfig" delimiterParsingDisabled="true" schemaValidation="true">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="500"
+ config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"/>
+ </xml>
+ </override>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testMultiTenentConfigurationBuilder3.xml b/src/test/resources/testMultiTenentConfigurationBuilder3.xml
new file mode 100644
index 0000000..1481cb3
--- /dev/null
+++ b/src/test/resources/testMultiTenentConfigurationBuilder3.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true" loggerName="TestLogger"
+ config-class="org.apache.commons.configuration.DynamicCombinedConfiguration"
+ keyPattern="$${sys:Id}">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.MergeCombiner"/>
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <entity-resolver catalogFiles="catalog.xml"/>
+ <providers>
+ <provider config-tag="multifile"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$FileConfigurationProvider"
+ configurationClass="org.apache.commons.configuration.MultiFileHierarchicalConfiguration"/>
+ </providers>
+ </header>
+ <override>
+ <multifile filePattern="testwrite/testMultiConfiguration_$$${sys:Id}.xml"
+ config-name="clientConfig" delimiterParsingDisabled="true" schemaValidation="true">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="500"
+ config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"/>
+ </multifile>
+ <xml fileName="testMultiConfiguration_default.xml"
+ config-name="defaultConfig" delimiterParsingDisabled="true" schemaValidation="true">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="500"
+ config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"/>
+ </xml>
+ </override>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testMultiTenentConfigurationBuilder4.xml b/src/test/resources/testMultiTenentConfigurationBuilder4.xml
new file mode 100644
index 0000000..8d5ccf5
--- /dev/null
+++ b/src/test/resources/testMultiTenentConfigurationBuilder4.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true" loggerName="TestLogger"
+ config-class="org.apache.commons.configuration.DynamicCombinedConfiguration"
+ keyPattern="$${test:Id}">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.MergeCombiner"/>
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <lookups>
+ <lookup config-prefix="test"
+ config-class="org.apache.commons.configuration.TestDynamicCombinedConfiguration$ThreadLookup"/>
+ </lookups>
+ <entity-resolver catalogFiles="catalog.xml"/>
+ <providers>
+ <provider config-tag="multifile"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$FileConfigurationProvider"
+ configurationClass="org.apache.commons.configuration.MultiFileHierarchicalConfiguration"/>
+ </providers>
+ </header>
+ <override>
+ <multifile filePattern="testMultiConfiguration_$$${test:Id}.xml"
+ config-name="clientConfig" delimiterParsingDisabled="true" schemaValidation="true">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="500"
+ config-class="org.apache.commons.configuration.reloading.FileRandomReloadingStrategy"/>
+ </multifile>
+ <xml fileName="testMultiConfiguration_default.xml"
+ config-name="defaultConfig" delimiterParsingDisabled="true" schemaValidation="true">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="500"
+ config-class="org.apache.commons.configuration.reloading.FileRandomReloadingStrategy"/>
+ </xml>
+ </override>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testMultiTenentConfigurationBuilder5.xml b/src/test/resources/testMultiTenentConfigurationBuilder5.xml
new file mode 100644
index 0000000..de93caa
--- /dev/null
+++ b/src/test/resources/testMultiTenentConfigurationBuilder5.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true" loggerName="TestLogger"
+ config-class="org.apache.commons.configuration.DynamicCombinedConfiguration"
+ keyPattern="$${sys:Id}">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.MergeCombiner"/>
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <entity-resolver catalogFiles="catalog.xml"/>
+ <providers>
+ <provider config-tag="multifile"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$FileConfigurationProvider"
+ configurationClass="org.apache.commons.configuration.MultiFileHierarchicalConfiguration"/>
+ </providers>
+ </header>
+ <override>
+ <multifile filePattern="testwrite/testMultiConfiguration_$$${sys:Id}.xml"
+ config-name="clientConfig" delimiterParsingDisabled="true" schemaValidation="false">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="500"
+ config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"/>
+ </multifile>
+ <xml fileName="testwrite/testMultiDynamic_default.xml"
+ config-name="defaultConfig" delimiterParsingDisabled="true" schemaValidation="false">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="500"
+ config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"/>
+ </xml>
+ </override>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testPatternSubtreeConfig.xml b/src/test/resources/testPatternSubtreeConfig.xml
new file mode 100644
index 0000000..994bbf9
--- /dev/null
+++ b/src/test/resources/testPatternSubtreeConfig.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<Configuration>
+ <BusinessClient name="1001">
+ <colors>
+ <background>#808080</background>
+ <text>#000000</text>
+ <header>#008000</header>
+ <link normal="#000080" visited="#800080"/>
+ <default>${colors.header}</default>
+ </colors>
+ <rowsPerPage>15</rowsPerPage>
+ <buttons>
+ <name>OK,Cancel,Help</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+ </BusinessClient>
+ <BusinessClient name="1002">
+ <colors>
+ <background>#2222222</background>
+ <text>#000000</text>
+ <header>#222222</header>
+ <link normal="#020202" visited="#202020"/>
+ <default>${colors.header3}</default>
+ </colors>
+ <rowsPerPage>25</rowsPerPage>
+ <buttons>
+ <name>OK-2,Cancel-2,Help-2</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+ </BusinessClient>
+ <BusinessClient name="1003">
+ <colors>
+ <background>#333333</background>
+ <text>#FFFFFF</text>
+ <header>#333333</header>
+ <link normal="#030303" visited="#303030"/>
+ <default>${colors.header3}</default>
+ </colors>
+ <rowsPerPage>35</rowsPerPage>
+ <buttons>
+ <name>OK-3,Cancel-3,Help-3</name>
+ </buttons>
+ <numberFormat pattern="###\,###.##"/>
+ </BusinessClient>
+</Configuration>
\ No newline at end of file
diff --git a/src/test/resources/testResolver.xml b/src/test/resources/testResolver.xml
new file mode 100644
index 0000000..7d04dc7
--- /dev/null
+++ b/src/test/resources/testResolver.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!DOCTYPE test PUBLIC "-//Apache//DTD Resolver Test V1.0//EN"
+ "http://example.com/does-not-exist">
+<test>
+<foo/>
+</test>
\ No newline at end of file
diff --git a/src/test/resources/testSequence.properties b/src/test/resources/testSequence.properties
new file mode 100644
index 0000000..7d6d0bd
--- /dev/null
+++ b/src/test/resources/testSequence.properties
@@ -0,0 +1,23 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+prefix.Fa.postfix=value.Fa
+prefix.Po.postfix=value.Po
+prefix.Ru.postfix=value.Ru
+prefix.Se.postfix=value.Se
+prefix.As.postfix=value.As
+prefix.Gl.postfix=value.Gl
+prefix.Pu.postfix=value.Pu
+prefix.Te.postfix=value.Te
+prefix.Ve.postfix=value.Ve
diff --git a/src/test/resources/testSequenceDigester.xml b/src/test/resources/testSequenceDigester.xml
new file mode 100644
index 0000000..6b9aac5
--- /dev/null
+++ b/src/test/resources/testSequenceDigester.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<configuration>
+ <properties fileName="testSequence.properties"/>
+</configuration>
diff --git a/src/test/resources/testSystemProperties.xml b/src/test/resources/testSystemProperties.xml
new file mode 100644
index 0000000..015de57
--- /dev/null
+++ b/src/test/resources/testSystemProperties.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that sets system properties from an XML file -->
+<configuration systemProperties="test.properties.xml">
+ <header>
+ <result delimiterParsingDisabled="true">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.OverrideCombiner"/>
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ </header>
+ <system/>
+ <properties fileName="test2.properties" throwExceptionOnMissing="true"
+ config-name="properties">
+ <reloadingStrategy config-class="org.apache.commons.configuration.reloading.FileChangedReloadingStrategy"
+ refreshDelay="10000"/>
+ </properties>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testVFSMultiTenentConfigurationBuilder1.xml b/src/test/resources/testVFSMultiTenentConfigurationBuilder1.xml
new file mode 100644
index 0000000..565b230
--- /dev/null
+++ b/src/test/resources/testVFSMultiTenentConfigurationBuilder1.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true" loggerName="TestLogger"
+ config-class="org.apache.commons.configuration.DynamicCombinedConfiguration"
+ keyPattern="$${sys:Id}">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.MergeCombiner"/>
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <fileSystem config-class="org.apache.commons.configuration.VFSFileSystem"/>
+ <entity-resolver catalogFiles="catalog.xml"/>
+ <providers>
+ <provider config-tag="multifile"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$FileConfigurationProvider"
+ configurationClass="org.apache.commons.configuration.MultiFileHierarchicalConfiguration"/>
+ </providers>
+ </header>
+ <override>
+ <multifile filePattern="testMultiConfiguration_$$${sys:Id}.xml"
+ config-name="clientConfig" delimiterParsingDisabled="true" schemaValidation="true">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="500"
+ config-class="org.apache.commons.configuration.reloading.VFSFileChangedReloadingStrategy"/>
+ </multifile>
+ <xml fileName="testMultiConfiguration_default.xml"
+ config-name="defaultConfig" delimiterParsingDisabled="true" schemaValidation="true">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="500"
+ config-class="org.apache.commons.configuration.reloading.VFSFileChangedReloadingStrategy"/>
+ </xml>
+ </override>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testVFSMultiTenentConfigurationBuilder2.xml b/src/test/resources/testVFSMultiTenentConfigurationBuilder2.xml
new file mode 100644
index 0000000..fd5dc23
--- /dev/null
+++ b/src/test/resources/testVFSMultiTenentConfigurationBuilder2.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true" loggerName="TestLogger"
+ config-class="org.apache.commons.configuration.DynamicCombinedConfiguration"
+ keyPattern="$${sys:Id}">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.MergeCombiner"/>
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <fileSystem config-class="org.apache.commons.configuration.VFSFileSystem"/>
+ <entity-resolver catalogFiles="catalog.xml"/>
+ <providers>
+ <provider config-tag="multifile"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$FileConfigurationProvider"
+ configurationClass="org.apache.commons.configuration.MultiFileHierarchicalConfiguration"/>
+ </providers>
+ </header>
+ <override>
+ <multifile filePattern="testwrite/testMultiConfiguration_$$${sys:Id}.xml"
+ config-name="clientConfig" delimiterParsingDisabled="true" schemaValidation="true">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="500"
+ config-class="org.apache.commons.configuration.reloading.VFSFileChangedReloadingStrategy"/>
+ </multifile>
+ <xml fileName="testMultiConfiguration_default.xml"
+ config-name="defaultConfig" delimiterParsingDisabled="true" schemaValidation="true">
+ <expressionEngine
+ config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ <reloadingStrategy refreshDelay="500"
+ config-class="org.apache.commons.configuration.reloading.VFSFileChangedReloadingStrategy"/>
+ </xml>
+ </override>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testValidateInvalid.xml b/src/test/resources/testValidateInvalid.xml
new file mode 100644
index 0000000..0261046
--- /dev/null
+++ b/src/test/resources/testValidateInvalid.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!--
+ This is an invalid XML document, it does not conform to the declared DTD
+ (a type is missing in one field element). This document is used for testing
+ XMLConfiguration with a validating parser. It should be possible to load
+ it if validation is disabled, but if validation is enabled, an exception
+ should be thrown.
+-->
+<!DOCTYPE database [
+<!ELEMENT database (table+)>
+<!ELEMENT table (name, fields)>
+
+<!ELEMENT fields (field+)>
+<!ELEMENT field (name, type)>
+
+<!ELEMENT name (#PCDATA)>
+<!ELEMENT type (#PCDATA)>
+]>
+<database>
+ <table>
+ <name>customers</name>
+ <fields>
+ <field>
+ <name>custID</name>
+ <type>java.lang.Long</type>
+ </field>
+ <field>
+ <name>custName</name>
+ </field>
+ </fields>
+ </table>
+</database>
\ No newline at end of file
diff --git a/src/test/resources/testValidateValid.xml b/src/test/resources/testValidateValid.xml
new file mode 100644
index 0000000..52ee767
--- /dev/null
+++ b/src/test/resources/testValidateValid.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!--
+ A valid XML document according to its DTD.
+-->
+<!DOCTYPE database [
+<!ELEMENT database (table+)>
+<!ELEMENT table (name, fields)>
+
+<!ELEMENT fields (field+)>
+<!ELEMENT field (name, type)>
+
+<!ELEMENT name (#PCDATA)>
+<!ELEMENT type (#PCDATA)>
+]>
+
+<database>
+ <table>
+ <name>customers</name>
+ <fields>
+ <field>
+ <name>custID</name>
+ <type>java.lang.Long</type>
+ </field>
+ <field>
+ <name>custName</name>
+ <type>java.lang.String</type>
+ </field>
+ </fields>
+ </table>
+</database>
\ No newline at end of file
diff --git a/src/test/resources/testValidation.xml b/src/test/resources/testValidation.xml
new file mode 100644
index 0000000..6a6f5a7
--- /dev/null
+++ b/src/test/resources/testValidation.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.OverrideCombiner"/>
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <entity-resolver catalogFiles="catalog.xml"/>
+ </header>
+ <system/>
+ <properties fileName="test.properties.xml" throwExceptionOnMissing="true"
+ config-name="properties">
+ </properties>
+ <xml fileName="sample.xml" config-name="xml" schemaValidation="true"/>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testValidation2.xml b/src/test/resources/testValidation2.xml
new file mode 100644
index 0000000..f80bfd9
--- /dev/null
+++ b/src/test/resources/testValidation2.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.OverrideCombiner"/>
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <entity-resolver catalogFiles="file://${sys:user.dir}/target/test-classes/catalog2.xml"/>
+ </header>
+ <system/>
+ <properties fileName="test.properties.xml" throwExceptionOnMissing="true"
+ config-name="properties">
+ </properties>
+ <xml fileName="file://${sys:user.dir}/target/test-classes/sample.xml" config-name="xml" schemaValidation="true"/>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/testValidation3.xml b/src/test/resources/testValidation3.xml
new file mode 100644
index 0000000..9015217
--- /dev/null
+++ b/src/test/resources/testValidation3.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration definition file that demonstrates complex initialization -->
+<configuration>
+ <header>
+ <result delimiterParsingDisabled="true" forceReloadCheck="true" loggerName="TestLogger"
+ config-class="org.apache.commons.configuration.DynamicCombinedConfiguration"
+ keyPattern="$${sys:Id}">
+ <nodeCombiner config-class="org.apache.commons.configuration.tree.MergeCombiner"/>
+ <expressionEngine config-class="org.apache.commons.configuration.tree.xpath.XPathExpressionEngine"/>
+ </result>
+ <providers>
+ <provider config-tag="multifile"
+ config-class="org.apache.commons.configuration.DefaultConfigurationBuilder$FileConfigurationProvider"
+ configurationClass="org.apache.commons.configuration.MultiFileHierarchicalConfiguration"/>
+ </providers><entity-resolver catalogFiles="catalog.xml"/>
+ </header>
+ <system/>
+ <properties fileName="test.properties.xml" throwExceptionOnMissing="true"
+ config-name="properties">
+ </properties>
+ <multifile filePattern="sample_$$${sys:Id}.xml"
+ config-name="clientConfig" delimiterParsingDisabled="true" schemaValidation="true">
+ </multifile>
+ <xml fileName="sample.xml" config-name="xml" schemaValidation="true"/>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/test_invalid_date.plist.xml b/src/test/resources/test_invalid_date.plist.xml
new file mode 100644
index 0000000..c78d175
--- /dev/null
+++ b/src/test/resources/test_invalid_date.plist.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Test configuration file with an invalid date property.
+ $Id: test_invalid_date.plist.xml 1367253 2012-07-30 19:59:36Z oheger $
+-->
+<plist version="1.0">
+ <dict>
+
+ <key>string</key>
+ <string>value1</string>
+
+ <key>date</key>
+ <date>not a valid date</date>
+
+ </dict>
+</plist>
diff --git a/src/test/resources/testcombine1.xml b/src/test/resources/testcombine1.xml
new file mode 100644
index 0000000..72c3428
--- /dev/null
+++ b/src/test/resources/testcombine1.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<config>
+ <gui>
+ <bgcolor>green</bgcolor>
+ <selcolor>yellow</selcolor>
+ <level default="2">1</level>
+ </gui>
+ <net>
+ <proxy>
+ <url>http://www.url1.org</url>
+ <url>http://www.url2.org</url>
+ <url>http://www.url3.org</url>
+ </proxy>
+ <service>
+ <url>http://service1.org</url>
+ </service>
+ <server>
+ </server>
+ </net>
+ <base>
+ <services>
+ <security>
+ <login>
+ <user>Admin</user>
+ <passwd type="secret"/>
+ </login>
+ </security>
+ </services>
+ </base>
+ <database>
+ <tables>
+ <table id="1">
+ <name>documents</name>
+ <fields>
+ <field>
+ <name>docid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>docname</name>
+ <type>varchar</type>
+ </field>
+ <field>
+ <name>authorID</name>
+ <type>int</type>
+ </field>
+ </fields>
+ </table>
+ </tables>
+ </database>
+ <Channels>
+ <Channel id="1" type="half">
+ <Name>My Channel</Name>
+ </Channel>
+ <Channel id="2">
+ <MoreChannelData>more test 2 data</MoreChannelData>
+ </Channel>
+ <Channel id="3" type="half">
+ <Name>Test Channel</Name>
+ </Channel>
+ </Channels>
+</config>
diff --git a/src/test/resources/testcombine2.xml b/src/test/resources/testcombine2.xml
new file mode 100644
index 0000000..5ef048c
--- /dev/null
+++ b/src/test/resources/testcombine2.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<config>
+ <base>
+ <services>
+ <security>
+ <login>
+ <user type="default">scotty</user>
+ <passwd>BeamMeUp</passwd>
+ </login>
+ </security>
+ </services>
+ </base>
+ <gui>
+ <bgcolor>black</bgcolor>
+ <fgcolor>blue</fgcolor>
+ <level min="1">4</level>
+ </gui>
+ <net>
+ <server>
+ <url>http://appsvr1.com</url>
+ <url>http://appsvr2.com</url>
+ <url>http://testsvr.com</url>
+ <url>http://backupsvr.com</url>
+ </server>
+ <service>
+ <url type="2">http://service2.org</url>
+ <url type="2">http://service3.org</url>
+ </service>
+ </net>
+ <database>
+ <tables>
+ <table id="2">
+ <name>tasks</name>
+ <fields>
+ <field>
+ <name>taskid</name>
+ <type>long</type>
+ </field>
+ <field>
+ <name>taskname</name>
+ <type>varchar</type>
+ </field>
+ </fields>
+ </table>
+ </tables>
+ </database>
+ <Channels>
+ <Channel id="1">
+ <Name>Channel 1</Name>
+ <ChannelData>test 1 data</ChannelData>
+ </Channel>
+ <Channel id="2" type="full">
+ <Name>Channel 2</Name>
+ <ChannelData>test 2 data</ChannelData>
+ </Channel>
+ <Channel id="3" type="full">
+ <Name>Channel 3</Name>
+ <ChannelData>test 3 data</ChannelData>
+ </Channel>
+ </Channels>
+</config>
diff --git a/src/test/resources/testdb.script b/src/test/resources/testdb.script
new file mode 100644
index 0000000..30a57b5
--- /dev/null
+++ b/src/test/resources/testdb.script
@@ -0,0 +1,95 @@
+; Licensed to the Apache Software Foundation (ASF) under one or more
+; contributor license agreements. See the NOTICE file distributed with
+; this work for additional information regarding copyright ownership.
+; The ASF licenses this file to You under the Apache License, Version 2.0
+; (the "License"); you may not use this file except in compliance with
+; the License. You may obtain a copy of the License at
+;
+; http://www.apache.org/licenses/LICENSE-2.0
+;
+; Unless required by applicable law or agreed to in writing, software
+; distributed under the License is distributed on an "AS IS" BASIS,
+; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+; See the License for the specific language governing permissions and
+; limitations under the License.
+
+DROP TABLE CONFIGURATION IF EXISTS;
+CREATE TABLE CONFIGURATION(KEY VARCHAR(256) NOT NULL PRIMARY KEY,VALUE VARCHAR(256));
+
+DROP TABLE CONFIGURATIONS IF EXISTS;
+CREATE TABLE CONFIGURATIONS(NAME VARCHAR(256) NOT NULL,KEY VARCHAR(256) NOT NULL,VALUE VARCHAR(256),CONSTRAINT SYS_PK_CONFIGURATIONS PRIMARY KEY(NAME,KEY));
+
+DROP TABLE CONFIGURATIONLIST IF EXISTS;
+CREATE TABLE CONFIGURATIONLIST(ID VARCHAR(256) NOT NULL PRIMARY KEY, KEY VARCHAR(256) NOT NULL,VALUE VARCHAR(256));;
+;
+GRANT ALL ON CLASS "java.lang.Math" TO PUBLIC;
+GRANT ALL ON CLASS "org.hsqldb.Library" TO PUBLIC;
+CREATE USER SA PASSWORD "" ADMIN;
+CREATE ALIAS DAYNAME FOR "org.hsqldb.Library.dayname";
+CREATE ALIAS SPACE FOR "org.hsqldb.Library.space";
+CREATE ALIAS SUBSTRING FOR "org.hsqldb.Library.substring";
+CREATE ALIAS HEXTORAW FOR "org.hsqldb.Library.hexToRaw";
+CREATE ALIAS SQRT FOR "java.lang.Math.sqrt";
+CREATE ALIAS ABS FOR "org.hsqldb.Library.abs";
+CREATE ALIAS POWER FOR "java.lang.Math.pow";
+CREATE ALIAS CHAR FOR "org.hsqldb.Library.character";
+CREATE ALIAS CONCAT FOR "org.hsqldb.Library.concat";
+CREATE ALIAS PI FOR "org.hsqldb.Library.pi";
+CREATE ALIAS RAWTOHEX FOR "org.hsqldb.Library.rawToHex";
+CREATE ALIAS SECOND FOR "org.hsqldb.Library.second";
+CREATE ALIAS TRUNCATE FOR "org.hsqldb.Library.truncate";
+CREATE ALIAS MONTH FOR "org.hsqldb.Library.month";
+CREATE ALIAS LOWER FOR "org.hsqldb.Library.lcase";
+CREATE ALIAS ATAN2 FOR "java.lang.Math.atan2";
+CREATE ALIAS REPEAT FOR "org.hsqldb.Library.repeat";
+CREATE ALIAS DAYOFMONTH FOR "org.hsqldb.Library.dayofmonth";
+CREATE ALIAS TAN FOR "java.lang.Math.tan";
+CREATE ALIAS RADIANS FOR "java.lang.Math.toRadians";
+CREATE ALIAS FLOOR FOR "java.lang.Math.floor";
+CREATE ALIAS NOW FOR "org.hsqldb.Library.now";
+CREATE ALIAS ACOS FOR "java.lang.Math.acos";
+CREATE ALIAS DAYOFWEEK FOR "org.hsqldb.Library.dayofweek";
+CREATE ALIAS CEILING FOR "java.lang.Math.ceil";
+CREATE ALIAS DAYOFYEAR FOR "org.hsqldb.Library.dayofyear";
+CREATE ALIAS LCASE FOR "org.hsqldb.Library.lcase";
+CREATE ALIAS WEEK FOR "org.hsqldb.Library.week";
+CREATE ALIAS SOUNDEX FOR "org.hsqldb.Library.soundex";
+CREATE ALIAS ASIN FOR "java.lang.Math.asin";
+CREATE ALIAS LOCATE FOR "org.hsqldb.Library.locate";
+CREATE ALIAS EXP FOR "java.lang.Math.exp";
+CREATE ALIAS MONTHNAME FOR "org.hsqldb.Library.monthname";
+CREATE ALIAS YEAR FOR "org.hsqldb.Library.year";
+CREATE ALIAS LEFT FOR "org.hsqldb.Library.left";
+CREATE ALIAS ROUNDMAGIC FOR "org.hsqldb.Library.roundMagic";
+CREATE ALIAS BITOR FOR "org.hsqldb.Library.bitor";
+CREATE ALIAS LTRIM FOR "org.hsqldb.Library.ltrim";
+CREATE ALIAS COT FOR "org.hsqldb.Library.cot";
+CREATE ALIAS COS FOR "java.lang.Math.cos";
+CREATE ALIAS MOD FOR "org.hsqldb.Library.mod";
+CREATE ALIAS SIGN FOR "org.hsqldb.Library.sign";
+CREATE ALIAS DEGREES FOR "java.lang.Math.toDegrees";
+CREATE ALIAS LOG FOR "java.lang.Math.log";
+CREATE ALIAS SIN FOR "java.lang.Math.sin";
+CREATE ALIAS CURTIME FOR "org.hsqldb.Library.curtime";
+CREATE ALIAS DIFFERENCE FOR "org.hsqldb.Library.difference";
+CREATE ALIAS INSERT FOR "org.hsqldb.Library.insert";
+CREATE ALIAS SUBSTR FOR "org.hsqldb.Library.substring";
+CREATE ALIAS DATABASE FOR "org.hsqldb.Library.database";
+CREATE ALIAS MINUTE FOR "org.hsqldb.Library.minute";
+CREATE ALIAS HOUR FOR "org.hsqldb.Library.hour";
+CREATE ALIAS IDENTITY FOR "org.hsqldb.Library.identity";
+CREATE ALIAS QUARTER FOR "org.hsqldb.Library.quarter";
+CREATE ALIAS CURDATE FOR "org.hsqldb.Library.curdate";
+CREATE ALIAS BITAND FOR "org.hsqldb.Library.bitand";
+CREATE ALIAS USER FOR "org.hsqldb.Library.user";
+CREATE ALIAS UCASE FOR "org.hsqldb.Library.ucase";
+CREATE ALIAS RTRIM FOR "org.hsqldb.Library.rtrim";
+CREATE ALIAS LOG10 FOR "org.hsqldb.Library.log10";
+CREATE ALIAS RIGHT FOR "org.hsqldb.Library.right";
+CREATE ALIAS ATAN FOR "java.lang.Math.atan";
+CREATE ALIAS UPPER FOR "org.hsqldb.Library.ucase";
+CREATE ALIAS ASCII FOR "org.hsqldb.Library.ascii";
+CREATE ALIAS RAND FOR "java.lang.Math.random";
+CREATE ALIAS LENGTH FOR "org.hsqldb.Library.length";
+CREATE ALIAS ROUND FOR "org.hsqldb.Library.round";
+CREATE ALIAS REPLACE FOR "org.hsqldb.Library.replace";
diff --git a/src/test/resources/threesome.properties b/src/test/resources/threesome.properties
new file mode 100644
index 0000000..5fec7d1
--- /dev/null
+++ b/src/test/resources/threesome.properties
@@ -0,0 +1,22 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+test.threesome.one = aaa
+test.threesome.one = bbb, ccc
+
+test.threesome.two = aaa, bbb, ccc
+
+test.threesome.three = aaa
+test.threesome.three = bbb
+test.threesome.three = ccc
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-java/commons-configuration.git
More information about the pkg-java-commits
mailing list