001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.commons.configuration;
018    
019    import static org.junit.Assert.assertEquals;
020    import static org.junit.Assert.assertFalse;
021    import static org.junit.Assert.assertNotNull;
022    import static org.junit.Assert.assertNull;
023    import static org.junit.Assert.assertSame;
024    import static org.junit.Assert.assertTrue;
025    import static org.junit.Assert.fail;
026    
027    import java.io.File;
028    import java.io.StringWriter;
029    import java.lang.reflect.InvocationHandler;
030    import java.lang.reflect.Method;
031    import java.lang.reflect.Proxy;
032    import java.net.URL;
033    import java.util.Collection;
034    import java.util.HashMap;
035    import java.util.Iterator;
036    import java.util.List;
037    import java.util.Map;
038    import java.util.Set;
039    
040    import org.apache.commons.configuration.beanutils.BeanHelper;
041    import org.apache.commons.configuration.event.ConfigurationListenerTestImpl;
042    import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;
043    import org.apache.commons.configuration.tree.ConfigurationNode;
044    import org.apache.commons.configuration.tree.DefaultConfigurationNode;
045    import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
046    import org.apache.commons.lang.SystemUtils;
047    import org.apache.commons.lang.text.StrLookup;
048    import org.apache.commons.logging.Log;
049    import org.apache.commons.logging.LogFactory;
050    import org.apache.commons.logging.impl.Log4JLogger;
051    import org.apache.log4j.Level;
052    import org.apache.log4j.Logger;
053    import org.apache.log4j.SimpleLayout;
054    import org.apache.log4j.WriterAppender;
055    import org.junit.Before;
056    import org.junit.Test;
057    
058    /**
059     * Test class for DefaultConfigurationBuilder.
060     *
061     * @author <a
062     * href="http://commons.apache.org/configuration/team-list.html">Commons
063     * Configuration team</a>
064     * @version $Id: TestDefaultConfigurationBuilder.java 1301997 2012-03-17 20:32:02Z sebb $
065     */
066    public class TestDefaultConfigurationBuilder
067    {
068        /** Test configuration definition file. */
069        private static final File TEST_FILE = ConfigurationAssert
070                .getTestFile("testDigesterConfiguration.xml");
071    
072        private static final File ADDITIONAL_FILE = ConfigurationAssert
073                .getTestFile("testDigesterConfiguration2.xml");
074    
075        private static final File OPTIONAL_FILE = ConfigurationAssert
076                .getTestFile("testDigesterOptionalConfiguration.xml");
077    
078        private static final File OPTIONALEX_FILE = ConfigurationAssert
079                .getTestFile("testDigesterOptionalConfigurationEx.xml");
080    
081        private static final File MULTI_FILE = ConfigurationAssert
082                .getTestFile("testDigesterConfiguration3.xml");
083    
084        private static final File INIT_FILE = ConfigurationAssert
085                .getTestFile("testComplexInitialization.xml");
086    
087        private static final File CLASS_FILE = ConfigurationAssert
088                .getTestFile("testExtendedClass.xml");
089    
090        private static final File PROVIDER_FILE = ConfigurationAssert
091                .getTestFile("testConfigurationProvider.xml");
092    
093        private static final File EXTENDED_PROVIDER_FILE = ConfigurationAssert
094                .getTestFile("testExtendedXMLConfigurationProvider.xml");
095    
096        private static final File GLOBAL_LOOKUP_FILE = ConfigurationAssert
097                .getTestFile("testGlobalLookup.xml");
098    
099        private static final File SYSTEM_PROPS_FILE = ConfigurationAssert
100                .getTestFile("testSystemProperties.xml");
101    
102        private static final File VALIDATION_FILE = ConfigurationAssert
103                .getTestFile("testValidation.xml");
104    
105        private static final File VALIDATION3_FILE = ConfigurationAssert
106                .getTestFile("testValidation3.xml");
107    
108        private static final File MULTI_TENENT_FILE = ConfigurationAssert
109                .getTestFile("testMultiTenentConfigurationBuilder.xml");
110    
111        private static final File EXPRESSION_FILE = ConfigurationAssert
112                .getTestFile("testExpression.xml");
113    
114        /** Constant for the name of an optional configuration.*/
115        private static final String OPTIONAL_NAME = "optionalConfig";
116    
117        /** Stores the object to be tested. */
118        DefaultConfigurationBuilder factory;
119    
120        @Before
121        public void setUp() throws Exception
122        {
123            System
124                    .setProperty("java.naming.factory.initial",
125                            "org.apache.commons.configuration.MockInitialContextFactory");
126            System.setProperty("test_file_xml", "test.xml");
127            System.setProperty("test_file_combine", "testcombine1.xml");
128            factory = new DefaultConfigurationBuilder();
129            factory.clearErrorListeners();  // avoid exception messages
130        }
131    
132        /**
133         * Tests the isReservedNode() method of ConfigurationDeclaration.
134         */
135        @Test
136        public void testConfigurationDeclarationIsReserved()
137        {
138            DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
139                    factory, factory);
140            DefaultConfigurationNode parent = new DefaultConfigurationNode();
141            DefaultConfigurationNode nd = new DefaultConfigurationNode("at");
142            parent.addAttribute(nd);
143            assertTrue("Attribute at not recognized", decl.isReservedNode(nd));
144            nd = new DefaultConfigurationNode("optional");
145            parent.addAttribute(nd);
146            assertTrue("Attribute optional not recognized", decl.isReservedNode(nd));
147            nd = new DefaultConfigurationNode("config-class");
148            parent.addAttribute(nd);
149            assertTrue("Inherited attribute not recognized", decl
150                    .isReservedNode(nd));
151            nd = new DefaultConfigurationNode("different");
152            parent.addAttribute(nd);
153            assertFalse("Wrong reserved attribute", decl.isReservedNode(nd));
154            nd = new DefaultConfigurationNode("at");
155            parent.addChild(nd);
156            assertFalse("Node type not evaluated", decl.isReservedNode(nd));
157        }
158    
159        /**
160         * Tests if the at attribute is correctly detected as reserved attribute.
161         */
162        @Test
163        public void testConfigurationDeclarationIsReservedAt()
164        {
165            checkOldReservedAttribute("at");
166        }
167    
168        /**
169         * Tests if the optional attribute is correctly detected as reserved
170         * attribute.
171         */
172        @Test
173        public void testConfigurationDeclarationIsReservedOptional()
174        {
175            checkOldReservedAttribute("optional");
176        }
177    
178        /**
179         * Tests if special reserved attributes are recognized by the
180         * isReservedNode() method. For compatibility reasons the attributes "at"
181         * and "optional" are also treated as reserved attributes, but only if there
182         * are no corresponding attributes with the "config-" prefix.
183         *
184         * @param name the attribute name
185         */
186        private void checkOldReservedAttribute(String name)
187        {
188            DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
189                    factory, factory);
190            DefaultConfigurationNode parent = new DefaultConfigurationNode();
191            DefaultConfigurationNode nd = new DefaultConfigurationNode("config-"
192                    + name);
193            parent.addAttribute(nd);
194            assertTrue("config-" + name + " attribute not recognized", decl
195                    .isReservedNode(nd));
196            DefaultConfigurationNode nd2 = new DefaultConfigurationNode(name);
197            parent.addAttribute(nd2);
198            assertFalse(name + " is reserved though config- exists", decl
199                    .isReservedNode(nd2));
200            assertTrue("config- attribute not recognized when " + name + " exists",
201                    decl.isReservedNode(nd));
202        }
203    
204        /**
205         * Tests access to certain reserved attributes of a
206         * ConfigurationDeclaration.
207         */
208        public void testConfigurationDeclarationGetAttributes()
209        {
210            factory.addProperty("xml.fileName", "test.xml");
211            DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
212                    factory, factory.configurationAt("xml"));
213            assertNull("Found an at attribute", decl.getAt());
214            assertFalse("Found an optional attribute", decl.isOptional());
215            factory.addProperty("xml[@config-at]", "test1");
216            assertEquals("Wrong value of at attribute", "test1", decl.getAt());
217            factory.addProperty("xml[@at]", "test2");
218            assertEquals("Wrong value of config-at attribute", "test1", decl.getAt());
219            factory.clearProperty("xml[@config-at]");
220            assertEquals("Old at attribute not detected", "test2", decl.getAt());
221            factory.addProperty("xml[@config-optional]", "true");
222            assertTrue("Wrong value of optional attribute", decl.isOptional());
223            factory.addProperty("xml[@optional]", "false");
224            assertTrue("Wrong value of config-optional attribute", decl.isOptional());
225            factory.clearProperty("xml[@config-optional]");
226            factory.setProperty("xml[@optional]", Boolean.TRUE);
227            assertTrue("Old optional attribute not detected", decl.isOptional());
228        }
229    
230        /**
231         * Tests whether an invalid value of an optional attribute is detected.
232         */
233        @Test(expected = ConfigurationRuntimeException.class)
234        public void testConfigurationDeclarationOptionalAttributeInvalid()
235        {
236            factory.addProperty("xml.fileName", "test.xml");
237            DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
238                    factory, factory.configurationAt("xml"));
239            factory.setProperty("xml[@optional]", "invalid value");
240            decl.isOptional();
241        }
242    
243        /**
244         * Tests adding a new configuration provider.
245         */
246        @Test
247        public void testAddConfigurationProvider()
248        {
249            DefaultConfigurationBuilder.ConfigurationProvider provider = new DefaultConfigurationBuilder.ConfigurationProvider();
250            assertNull("Provider already registered", factory
251                    .providerForTag("test"));
252            factory.addConfigurationProvider("test", provider);
253            assertSame("Provider not registered", provider, factory
254                    .providerForTag("test"));
255        }
256    
257        /**
258         * Tries to register a null configuration provider. This should cause an
259         * exception.
260         */
261        @Test(expected = IllegalArgumentException.class)
262        public void testAddConfigurationProviderNull()
263        {
264            factory.addConfigurationProvider("test", null);
265        }
266    
267        /**
268         * Tries to register a configuration provider for a null tag. This should
269         * cause an exception to be thrown.
270         */
271        @Test(expected = IllegalArgumentException.class)
272        public void testAddConfigurationProviderNullTag()
273        {
274            factory.addConfigurationProvider(null,
275                    new DefaultConfigurationBuilder.ConfigurationProvider());
276        }
277    
278        /**
279         * Tests removing configuration providers.
280         */
281        @Test
282        public void testRemoveConfigurationProvider()
283        {
284            assertNull("Removing unknown provider", factory
285                    .removeConfigurationProvider("test"));
286            assertNull("Removing provider for null tag", factory
287                    .removeConfigurationProvider(null));
288            DefaultConfigurationBuilder.ConfigurationProvider provider = new DefaultConfigurationBuilder.ConfigurationProvider();
289            factory.addConfigurationProvider("test", provider);
290            assertSame("Failed to remove provider", provider, factory
291                    .removeConfigurationProvider("test"));
292            assertNull("Provider still registered", factory.providerForTag("test"));
293        }
294    
295        /**
296         * Tests creating a configuration object from a configuration declaration.
297         */
298        @Test
299        public void testConfigurationBeanFactoryCreateBean()
300        {
301            factory.addConfigurationProvider("test",
302                    new DefaultConfigurationBuilder.ConfigurationProvider(
303                            PropertiesConfiguration.class));
304            factory.addProperty("test[@throwExceptionOnMissing]", "true");
305            DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
306                    factory, factory.configurationAt("test"));
307            PropertiesConfiguration conf = (PropertiesConfiguration) BeanHelper
308                    .createBean(decl);
309            assertTrue("Property was not initialized", conf
310                    .isThrowExceptionOnMissing());
311        }
312    
313        /**
314         * Tests creating a configuration object from an unknown tag. This should
315         * cause an exception.
316         */
317        @Test(expected = ConfigurationRuntimeException.class)
318        public void testConfigurationBeanFactoryCreateUnknownTag()
319        {
320            factory.addProperty("test[@throwExceptionOnMissing]", "true");
321            DefaultConfigurationBuilder.ConfigurationDeclaration decl = new DefaultConfigurationBuilder.ConfigurationDeclaration(
322                    factory, factory.configurationAt("test"));
323            BeanHelper.createBean(decl);
324        }
325    
326        /**
327         * Tests loading a simple configuration definition file.
328         */
329        @Test
330        public void testLoadConfiguration() throws ConfigurationException
331        {
332            factory.setFile(TEST_FILE);
333            checkConfiguration();
334        }
335    
336        /**
337         * Tests the file constructor.
338         */
339        @Test
340        public void testLoadConfigurationFromFile() throws ConfigurationException
341        {
342            factory = new DefaultConfigurationBuilder(TEST_FILE);
343            checkConfiguration();
344        }
345    
346        /**
347         * Tests the file name constructor.
348         */
349        @Test
350        public void testLoadConfigurationFromFileName()
351                throws ConfigurationException
352        {
353            factory = new DefaultConfigurationBuilder(TEST_FILE.getAbsolutePath());
354            checkConfiguration();
355        }
356    
357        /**
358         * Tests the URL constructor.
359         */
360        @Test
361        public void testLoadConfigurationFromURL() throws Exception
362        {
363            factory = new DefaultConfigurationBuilder(TEST_FILE.toURI().toURL());
364            checkConfiguration();
365        }
366    
367        /**
368         * Tests if the configuration was correctly created by the factory.
369         */
370        private void checkConfiguration() throws ConfigurationException
371        {
372            CombinedConfiguration compositeConfiguration = (CombinedConfiguration) factory
373                    .getConfiguration();
374    
375            assertEquals("Number of configurations", 3, compositeConfiguration
376                    .getNumberOfConfigurations());
377            assertEquals(PropertiesConfiguration.class, compositeConfiguration
378                    .getConfiguration(0).getClass());
379            assertEquals(XMLPropertiesConfiguration.class, compositeConfiguration
380                    .getConfiguration(1).getClass());
381            assertEquals(XMLConfiguration.class, compositeConfiguration
382                    .getConfiguration(2).getClass());
383    
384            // check the first configuration
385            PropertiesConfiguration pc = (PropertiesConfiguration) compositeConfiguration
386                    .getConfiguration(0);
387            assertNotNull("Make sure we have a fileName: " + pc.getFileName(), pc
388                    .getFileName());
389    
390            // check some properties
391            checkProperties(compositeConfiguration);
392        }
393    
394        /**
395         * Checks if the passed in configuration contains the expected properties.
396         *
397         * @param compositeConfiguration the configuration to check
398         */
399        private void checkProperties(Configuration compositeConfiguration)
400        {
401            assertTrue("Make sure we have loaded our key", compositeConfiguration
402                    .getBoolean("test.boolean"));
403            assertEquals("I'm complex!", compositeConfiguration
404                    .getProperty("element2.subelement.subsubelement"));
405            assertEquals("property in the XMLPropertiesConfiguration", "value1",
406                    compositeConfiguration.getProperty("key1"));
407        }
408    
409        /**
410         * Tests loading a configuration definition file with an additional section.
411         */
412        @Test
413        public void testLoadAdditional() throws ConfigurationException
414        {
415            factory.setFile(ADDITIONAL_FILE);
416            CombinedConfiguration compositeConfiguration = (CombinedConfiguration) factory
417                    .getConfiguration();
418            assertEquals("Verify how many configs", 2, compositeConfiguration
419                    .getNumberOfConfigurations());
420    
421            // Test if union was constructed correctly
422            Object prop = compositeConfiguration.getProperty("tables.table.name");
423            assertTrue(prop instanceof Collection);
424            assertEquals(3, ((Collection<?>) prop).size());
425            assertEquals("users", compositeConfiguration
426                    .getProperty("tables.table(0).name"));
427            assertEquals("documents", compositeConfiguration
428                    .getProperty("tables.table(1).name"));
429            assertEquals("tasks", compositeConfiguration
430                    .getProperty("tables.table(2).name"));
431    
432            prop = compositeConfiguration
433                    .getProperty("tables.table.fields.field.name");
434            assertTrue(prop instanceof Collection);
435            assertEquals(17, ((Collection<?>) prop).size());
436    
437            assertEquals("smtp.mydomain.org", compositeConfiguration
438                    .getString("mail.host.smtp"));
439            assertEquals("pop3.mydomain.org", compositeConfiguration
440                    .getString("mail.host.pop"));
441    
442            // This was overriden
443            assertEquals("masterOfPost", compositeConfiguration
444                    .getString("mail.account.user"));
445            assertEquals("topsecret", compositeConfiguration
446                    .getString("mail.account.psswd"));
447    
448            // This was overriden, too, but not in additional section
449            assertEquals("enhanced factory", compositeConfiguration
450                    .getString("test.configuration"));
451        }
452    
453        /**
454         * Tests whether a default log error listener is registered at the builder
455         * instance.
456         */
457        @Test
458        public void testLogErrorListener()
459        {
460            assertEquals("No default error listener registered", 1,
461                    new DefaultConfigurationBuilder().getErrorListeners().size());
462        }
463    
464        /**
465         * Tests loading a definition file that contains optional configurations.
466         */
467        @Test
468        public void testLoadOptional() throws Exception
469        {
470            factory.setURL(OPTIONAL_FILE.toURI().toURL());
471            Configuration config = factory.getConfiguration();
472            assertTrue(config.getBoolean("test.boolean"));
473            assertEquals("value", config.getProperty("element"));
474        }
475    
476        /**
477         * Tests whether loading a failing optional configuration causes an error
478         * event.
479         */
480        @Test
481        public void testLoadOptionalErrorEvent() throws Exception
482        {
483            factory.clearErrorListeners();
484            ConfigurationErrorListenerImpl listener = new ConfigurationErrorListenerImpl();
485            factory.addErrorListener(listener);
486            prepareOptionalTest("configuration", false);
487            listener.verify(DefaultConfigurationBuilder.EVENT_ERR_LOAD_OPTIONAL,
488                    OPTIONAL_NAME, null);
489        }
490    
491        /**
492         * Tests loading a definition file with optional and non optional
493         * configuration sources. One non optional does not exist, so this should
494         * cause an exception.
495         */
496        @Test(expected = ConfigurationException.class)
497        public void testLoadOptionalWithException() throws ConfigurationException
498        {
499            factory.setFile(OPTIONALEX_FILE);
500            factory.getConfiguration();
501        }
502    
503        /**
504         * Tries to load a configuration file with an optional, non file-based
505         * configuration. The optional attribute should work for other configuration
506         * classes, too.
507         */
508        @Test
509        public void testLoadOptionalNonFileBased() throws ConfigurationException
510        {
511            CombinedConfiguration config = prepareOptionalTest("configuration", false);
512            assertTrue("Configuration not empty", config.isEmpty());
513            assertEquals("Wrong number of configurations", 0, config
514                    .getNumberOfConfigurations());
515        }
516    
517        /**
518         * Tests an optional, non existing configuration with the forceCreate
519         * attribute. This configuration should be added to the resulting
520         * configuration.
521         */
522        @Test
523        public void testLoadOptionalForceCreate() throws ConfigurationException
524        {
525            factory.setBasePath(TEST_FILE.getParent());
526            CombinedConfiguration config = prepareOptionalTest("xml", true);
527            assertEquals("Wrong number of configurations", 1, config
528                    .getNumberOfConfigurations());
529            FileConfiguration fc = (FileConfiguration) config
530                    .getConfiguration(OPTIONAL_NAME);
531            assertNotNull("Optional config not found", fc);
532            assertEquals("File name was not set", "nonExisting.xml", fc
533                    .getFileName());
534            assertNotNull("Base path was not set", fc.getBasePath());
535        }
536    
537        /**
538         * Tests loading an embedded optional configuration builder with the force
539         * create attribute.
540         */
541        @Test
542        public void testLoadOptionalBuilderForceCreate()
543                throws ConfigurationException
544        {
545            CombinedConfiguration config = prepareOptionalTest("configuration",
546                    true);
547            assertEquals("Wrong number of configurations", 1, config
548                    .getNumberOfConfigurations());
549            assertTrue(
550                    "Wrong optional configuration type",
551                    config.getConfiguration(OPTIONAL_NAME) instanceof CombinedConfiguration);
552        }
553    
554        /**
555         * Tests loading an optional configuration with the force create attribute
556         * set. The provider will always throw an exception. In this case the
557         * configuration will not be added to the resulting combined configuration.
558         */
559        @Test
560        public void testLoadOptionalForceCreateWithException()
561                throws ConfigurationException
562        {
563            factory.addConfigurationProvider("test",
564                    new DefaultConfigurationBuilder.ConfigurationBuilderProvider()
565                    {
566                        // Throw an exception here, too
567                        @Override
568                        public AbstractConfiguration getEmptyConfiguration(
569                                DefaultConfigurationBuilder.ConfigurationDeclaration decl) throws Exception
570                        {
571                            throw new Exception("Unable to create configuration!");
572                        }
573                    });
574            CombinedConfiguration config = prepareOptionalTest("test", true);
575            assertEquals("Optional configuration could be created", 0, config
576                    .getNumberOfConfigurations());
577        }
578    
579        /**
580         * Prepares a test for loading a configuration definition file with an
581         * optional configuration declaration.
582         *
583         * @param tag the tag name with the optional configuration
584         * @param force the forceCreate attribute
585         * @return the combined configuration obtained from the builder
586         * @throws ConfigurationException if an error occurs
587         */
588        private CombinedConfiguration prepareOptionalTest(String tag, boolean force)
589                throws ConfigurationException
590        {
591            String prefix = "override." + tag;
592            factory.addProperty(prefix + "[@fileName]", "nonExisting.xml");
593            factory.addProperty(prefix + "[@config-optional]", Boolean.TRUE);
594            factory.addProperty(prefix + "[@config-name]", OPTIONAL_NAME);
595            if (force)
596            {
597                factory.addProperty(prefix + "[@config-forceCreate]", Boolean.TRUE);
598            }
599            return factory.getConfiguration(false);
600        }
601    
602        /**
603         * Tests whether the error log message caused by an optional configuration
604         * can be suppressed if a child builder is involved.
605         */
606        @Test
607        public void testLoadOptionalChildBuilderSuppressErrorLog()
608                throws ConfigurationException
609        {
610            factory.addProperty("override.configuration[@fileName]",
611                    OPTIONAL_FILE.getAbsolutePath());
612            // a special invocation handler which checks that the warn() method of
613            // a logger is not called
614            InvocationHandler handler = new InvocationHandler()
615            {
616                public Object invoke(Object proxy, Method method, Object[] args)
617                        throws Throwable
618                {
619                    String methodName = method.getName();
620                    if (methodName.startsWith("is"))
621                    {
622                        return Boolean.TRUE;
623                    }
624                    if ("warn".equals(methodName))
625                    {
626                        fail("Unexpected log output!");
627                    }
628                    return null;
629                }
630            };
631            factory.setLogger((Log) Proxy.newProxyInstance(getClass()
632                    .getClassLoader(), new Class[] {
633                Log.class
634            }, handler));
635            factory.getConfiguration(false);
636        }
637    
638        /**
639         * Tests loading a definition file with multiple different sources.
640         */
641        @Test
642        public void testLoadDifferentSources() throws ConfigurationException
643        {
644            factory.setFile(MULTI_FILE);
645            Configuration config = factory.getConfiguration();
646            assertFalse(config.isEmpty());
647            assertTrue(config instanceof CombinedConfiguration);
648            CombinedConfiguration cc = (CombinedConfiguration) config;
649            assertEquals("Wrong number of configurations", 1, cc
650                    .getNumberOfConfigurations());
651    
652            assertNotNull(config
653                    .getProperty("tables.table(0).fields.field(2).name"));
654            assertNotNull(config.getProperty("element2.subelement.subsubelement"));
655            assertEquals("value", config.getProperty("element3"));
656            assertEquals("foo", config.getProperty("element3[@name]"));
657            assertNotNull(config.getProperty("mail.account.user"));
658    
659            // test JNDIConfiguration
660            assertNotNull(config.getProperty("test.onlyinjndi"));
661            assertTrue(config.getBoolean("test.onlyinjndi"));
662    
663            Configuration subset = config.subset("test");
664            assertNotNull(subset.getProperty("onlyinjndi"));
665            assertTrue(subset.getBoolean("onlyinjndi"));
666    
667            // test SystemConfiguration
668            assertNotNull(config.getProperty("java.version"));
669            assertEquals(System.getProperty("java.version"), config
670                    .getString("java.version"));
671    
672            // test INIConfiguration
673            assertEquals("Property from ini file not found", "yes",
674                    config.getString("testini.loaded"));
675    
676            // test environment configuration
677            EnvironmentConfiguration envConf = new EnvironmentConfiguration();
678            for (Iterator<String> it = envConf.getKeys(); it.hasNext();)
679            {
680                String key = it.next();
681                String combinedKey = "env." + key;
682                assertEquals("Wrong value for env property " + key,
683                        envConf.getString(key), config.getString(combinedKey));
684            }
685        }
686    
687        /**
688         * Tests if the base path is correctly evaluated.
689         */
690        @Test
691        public void testSetConfigurationBasePath() throws ConfigurationException
692        {
693            factory.addProperty("properties[@fileName]", "test.properties");
694            File deepDir = new File(ConfigurationAssert.TEST_DIR, "config/deep");
695            factory.setConfigurationBasePath(deepDir.getAbsolutePath());
696    
697            Configuration config = factory.getConfiguration(false);
698            assertEquals("Wrong property value", "somevalue", config
699                    .getString("somekey"));
700        }
701    
702        /**
703         * Tests reading a configuration definition file that contains complex
704         * initialization of properties of the declared configuration sources.
705         */
706        @Test
707        public void testComplexInitialization() throws ConfigurationException
708        {
709            factory.setFile(INIT_FILE);
710            CombinedConfiguration cc = (CombinedConfiguration) factory
711                    .getConfiguration();
712    
713            assertEquals("System property not found", "test.xml",
714                    cc.getString("test_file_xml"));
715            PropertiesConfiguration c1 = (PropertiesConfiguration) cc
716                    .getConfiguration(1);
717            assertTrue(
718                    "Reloading strategy was not set",
719                    c1.getReloadingStrategy() instanceof FileChangedReloadingStrategy);
720            assertEquals("Refresh delay was not set", 10000,
721                    ((FileChangedReloadingStrategy) c1.getReloadingStrategy())
722                            .getRefreshDelay());
723    
724            Configuration xmlConf = cc.getConfiguration("xml");
725            assertEquals("Property not found", "I'm complex!", xmlConf
726                    .getString("element2/subelement/subsubelement"));
727            assertEquals("List index not found", "two", xmlConf
728                    .getString("list[0]/item[1]"));
729            assertEquals("Property in combiner file not found", "yellow", cc
730                    .getString("/gui/selcolor"));
731    
732            assertTrue("Delimiter flag was not set", cc
733                    .isDelimiterParsingDisabled());
734            assertTrue("Expression engine was not set",
735                    cc.getExpressionEngine() instanceof XPathExpressionEngine);
736        }
737    
738        /**
739         * Tests if the returned combined configuration has the expected structure.
740         */
741        @Test
742        public void testCombinedConfigurationStructure() throws ConfigurationException
743        {
744            factory.setFile(INIT_FILE);
745            CombinedConfiguration cc = (CombinedConfiguration) factory
746                    .getConfiguration();
747            assertNotNull("Properties configuration not found", cc
748                    .getConfiguration("properties"));
749            assertNotNull("XML configuration not found", cc.getConfiguration("xml"));
750            assertEquals("Wrong number of contained configs", 4, cc
751                    .getNumberOfConfigurations());
752    
753            CombinedConfiguration cc2 = (CombinedConfiguration) cc
754                    .getConfiguration(DefaultConfigurationBuilder.ADDITIONAL_NAME);
755            assertNotNull("No additional configuration found", cc2);
756            Set<String> names = cc2.getConfigurationNames();
757            assertEquals("Wrong number of contained additional configs", 2, names
758                    .size());
759            assertTrue("Config 1 not contained", names.contains("combiner1"));
760            assertTrue("Config 2 not contained", names.contains("combiner2"));
761        }
762    
763        /**
764         * Helper method for testing the attributes of a combined configuration
765         * created by the builder.
766         *
767         * @param cc the configuration to be checked
768         */
769        private void checkCombinedConfigAttrs(CombinedConfiguration cc)
770        {
771            assertTrue("Wrong delimiter parsing flag",
772                    cc.isDelimiterParsingDisabled());
773            assertTrue("Wrong reload check", cc.isForceReloadCheck());
774            assertTrue("Wrong ignore reload ex flag", cc.isIgnoreReloadExceptions());
775        }
776    
777        /**
778         * Tests whether attributes are correctly set on the combined configurations
779         * for the override and additional sections.
780         */
781        @Test
782        public void testCombinedConfigurationAttributes() throws ConfigurationException
783        {
784            factory.setFile(INIT_FILE);
785            CombinedConfiguration cc = (CombinedConfiguration) factory
786                    .getConfiguration();
787            checkCombinedConfigAttrs(cc);
788            CombinedConfiguration cc2 = (CombinedConfiguration) cc
789                    .getConfiguration(DefaultConfigurationBuilder.ADDITIONAL_NAME);
790            checkCombinedConfigAttrs(cc2);
791        }
792    
793        /**
794         * Tests the structure of the returned combined configuration if there is no
795         * additional section.
796         */
797        @Test
798        public void testCombinedConfigurationNoAdditional()
799                throws ConfigurationException
800        {
801            factory.setFile(TEST_FILE);
802            CombinedConfiguration cc = factory.getConfiguration(true);
803            assertNull("Additional configuration was found", cc
804                    .getConfiguration(DefaultConfigurationBuilder.ADDITIONAL_NAME));
805        }
806    
807        /**
808         * Tests whether the list node definition was correctly processed.
809         */
810        @Test
811        public void testCombinedConfigurationListNodes()
812                throws ConfigurationException
813        {
814            factory.setFile(INIT_FILE);
815            CombinedConfiguration cc = factory.getConfiguration(true);
816            Set<String> listNodes = cc.getNodeCombiner().getListNodes();
817            assertEquals("Wrong number of list nodes", 2, listNodes.size());
818            assertTrue("table node not a list node", listNodes.contains("table"));
819            assertTrue("list node not a list node", listNodes.contains("list"));
820    
821            CombinedConfiguration cca = (CombinedConfiguration) cc
822                    .getConfiguration(DefaultConfigurationBuilder.ADDITIONAL_NAME);
823            listNodes = cca.getNodeCombiner().getListNodes();
824            assertTrue("Found list nodes for additional combiner", listNodes
825                    .isEmpty());
826        }
827    
828        /**
829         * Tests whether a configuration builder can itself be declared in a
830         * configuration definition file.
831         */
832        @Test
833        public void testConfigurationBuilderProvider()
834                throws ConfigurationException
835        {
836            factory.addProperty("override.configuration[@fileName]", TEST_FILE
837                    .getAbsolutePath());
838            CombinedConfiguration cc = factory.getConfiguration(false);
839            assertEquals("Wrong number of configurations", 1, cc
840                    .getNumberOfConfigurations());
841            checkProperties(cc);
842        }
843    
844        /**
845         * Tests whether settings of the builder are propagated to child builders.
846         */
847        @Test
848        public void testConfigurationBuilderProviderInheritProperties()
849                throws Exception
850        {
851            factory.addProperty("override.configuration[@fileName]",
852                    TEST_FILE.getAbsolutePath());
853            factory.setBasePath("conf");
854            factory.setAttributeSplittingDisabled(true);
855            factory.setDelimiterParsingDisabled(true);
856            factory.setListDelimiter('/');
857            factory.setThrowExceptionOnMissing(true);
858            Log log = LogFactory.getLog(getClass());
859            factory.setLogger(log);
860            factory.clearErrorListeners();
861            factory.clearConfigurationListeners();
862            ConfigurationListenerTestImpl l =
863                    new ConfigurationListenerTestImpl(factory);
864            factory.addConfigurationListener(l);
865            DefaultConfigurationBuilder.ConfigurationDeclaration decl =
866                    new DefaultConfigurationBuilder.ConfigurationDeclaration(
867                            factory,
868                            factory.configurationAt("override.configuration"));
869            DefaultConfigurationBuilder.ConfigurationBuilderProvider provider =
870                    new DefaultConfigurationBuilder.ConfigurationBuilderProvider();
871            DefaultConfigurationBuilder child =
872                    (DefaultConfigurationBuilder) provider.createBean(
873                            provider.fetchConfigurationClass(), decl, null);
874            assertEquals("Wrong base path", factory.getBasePath(),
875                    child.getBasePath());
876            assertEquals("Wrong attribute splitting flag",
877                    factory.isAttributeSplittingDisabled(),
878                    child.isAttributeSplittingDisabled());
879            assertEquals("Wrong delimiter parsing flag",
880                    factory.isDelimiterParsingDisabled(),
881                    child.isDelimiterParsingDisabled());
882            assertEquals("Wrong list delimiter", factory.getListDelimiter(),
883                    child.getListDelimiter());
884            assertEquals("Wrong exception flag",
885                    factory.isThrowExceptionOnMissing(),
886                    child.isThrowExceptionOnMissing());
887            assertSame("Wrong logger", log, child.getLogger());
888            assertTrue("Got error listeners", child.getErrorListeners().isEmpty());
889            assertEquals("Wrong number of listeners", 1, child
890                    .getConfigurationListeners().size());
891            assertEquals("Wrong listener", l, child.getConfigurationListeners()
892                    .iterator().next());
893        }
894    
895        /**
896         * Tests whether properties of the parent configuration can be overridden.
897         */
898        @Test
899        public void testConfigurationBuilderProviderOverrideProperties()
900                throws Exception
901        {
902            factory.addProperty("override.configuration[@fileName]",
903                    TEST_FILE.getAbsolutePath());
904            factory.addProperty("override.configuration[@basePath]", "base");
905            factory.addProperty("override.configuration[@throwExceptionOnMissing]",
906                    "false");
907            factory.setBasePath("conf");
908            factory.setThrowExceptionOnMissing(true);
909            DefaultConfigurationBuilder.ConfigurationDeclaration decl =
910                    new DefaultConfigurationBuilder.ConfigurationDeclaration(
911                            factory,
912                            factory.configurationAt("override.configuration"));
913            DefaultConfigurationBuilder.ConfigurationBuilderProvider provider =
914                    new DefaultConfigurationBuilder.ConfigurationBuilderProvider();
915            DefaultConfigurationBuilder child =
916                    (DefaultConfigurationBuilder) provider.createBean(
917                            provider.fetchConfigurationClass(), decl, null);
918            assertEquals("Wrong base path", "base", child.getBasePath());
919            assertFalse("Wrong exception flag", child.isThrowExceptionOnMissing());
920        }
921    
922        /**
923         * Tests whether XML settings can be inherited.
924         */
925        @Test
926        public void testLoadXMLWithSettings() throws Exception
927        {
928            File confDir = new File("conf");
929            File targetDir = new File("target");
930            File testXMLValidationSource = new File(confDir,
931                    "testValidateInvalid.xml");
932            File testSavedXML = new File(targetDir, "testSave.xml");
933            File testSavedFactory = new File(targetDir, "testSaveFactory.xml");
934            URL dtdFile = getClass().getResource("/properties.dtd");
935            final String publicId = "http://commons.apache.org/test.dtd";
936    
937            XMLConfiguration config = new XMLConfiguration("testDtd.xml");
938            config.setPublicID(publicId);
939            config.save(testSavedXML);
940            factory.addProperty("xml[@fileName]", testSavedXML.getAbsolutePath());
941            factory.addProperty("xml(0)[@validating]", "true");
942            factory.addProperty("xml(-1)[@fileName]", testXMLValidationSource
943                    .getAbsolutePath());
944            factory.addProperty("xml(1)[@config-optional]", "true");
945            factory.addProperty("xml(1)[@validating]", "true");
946            factory.save(testSavedFactory);
947    
948            factory = new DefaultConfigurationBuilder();
949            factory.setFile(testSavedFactory);
950            factory.registerEntityId(publicId, dtdFile);
951            factory.clearErrorListeners();
952            Configuration c = factory.getConfiguration();
953            assertEquals("Wrong property value", "value1", c.getString("entry(0)"));
954            assertFalse("Invalid XML source was loaded", c
955                    .containsKey("table.name"));
956    
957            testSavedXML.delete();
958            testSavedFactory.delete();
959        }
960    
961        /**
962         * Tests loading a configuration definition file that defines a custom
963         * result class.
964         */
965        @Test
966        public void testExtendedClass() throws ConfigurationException
967        {
968            factory.setFile(CLASS_FILE);
969            CombinedConfiguration cc = factory.getConfiguration(true);
970            assertEquals("Extended", cc.getProperty("test"));
971            assertTrue("Wrong result class: " + cc.getClass(),
972                    cc instanceof ExtendedCombinedConfiguration);
973        }
974    
975        /**
976         * Tests loading a configuration definition file that defines new providers.
977         */
978        @Test
979        public void testConfigurationProvider() throws ConfigurationException
980        {
981            factory.setFile(PROVIDER_FILE);
982            factory.getConfiguration(true);
983            DefaultConfigurationBuilder.ConfigurationProvider provider = factory
984                    .providerForTag("test");
985            assertNotNull("Provider 'test' not registered", provider);
986        }
987    
988        /**
989         * Tests loading a configuration definition file that defines new providers.
990         */
991        @Test
992        public void testExtendedXMLConfigurationProvider() throws ConfigurationException
993        {
994            factory.setFile(EXTENDED_PROVIDER_FILE);
995            CombinedConfiguration cc = factory.getConfiguration(true);
996            DefaultConfigurationBuilder.ConfigurationProvider provider = factory
997                    .providerForTag("test");
998            assertNotNull("Provider 'test' not registered", provider);
999            Configuration config = cc.getConfiguration("xml");
1000            assertNotNull("Test configuration not present", config);
1001            assertTrue("Configuration is not ExtendedXMLConfiguration, is " +
1002                    config.getClass().getName(), config instanceof ExtendedXMLConfiguration);
1003        }
1004    
1005        @Test
1006        public void testGlobalLookup() throws Exception
1007        {
1008            factory.setFile(GLOBAL_LOOKUP_FILE);
1009            CombinedConfiguration cc = factory.getConfiguration(true);
1010            String value = cc.getInterpolator().lookup("test:test_key");
1011            assertNotNull("The test key was not located", value);
1012            assertEquals("Incorrect value retrieved","test.value",value);
1013        }
1014    
1015        @Test
1016        public void testSystemProperties() throws Exception
1017        {
1018            factory.setFile(SYSTEM_PROPS_FILE);
1019            factory.getConfiguration(true);
1020            String value = System.getProperty("key1");
1021            assertNotNull("The test key was not located", value);
1022            assertEquals("Incorrect value retrieved","value1",value);
1023        }
1024    
1025        @Test
1026        public void testValidation() throws Exception
1027        {
1028            factory.setFile(VALIDATION_FILE);
1029            factory.getConfiguration(true);
1030            String value = System.getProperty("key1");
1031            assertNotNull("The test key was not located", value);
1032            assertEquals("Incorrect value retrieved","value1",value);
1033        }
1034    
1035        @Test
1036        public void testValidation3() throws Exception
1037        {
1038            System.getProperties().remove("Id");
1039            factory.setFile(VALIDATION3_FILE);
1040            CombinedConfiguration config = factory.getConfiguration(true);
1041            String value = config.getString("Employee/Name");
1042            assertNotNull("The test key was not located", value);
1043            assertEquals("Incorrect value retrieved","John Doe",value);
1044            System.setProperty("Id", "1001");
1045            value = config.getString("Employee/Name");
1046            assertNotNull("The test key was not located", value);
1047            assertEquals("Incorrect value retrieved","Jane Doe",value);
1048        }
1049    
1050        @Test
1051        public void testMultiTenentConfiguration() throws Exception
1052        {
1053            factory.setFile(MULTI_TENENT_FILE);
1054            System.getProperties().remove("Id");
1055    
1056            CombinedConfiguration config = factory.getConfiguration(true);
1057            assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
1058    
1059            verify("1001", config, 15);
1060            verify("1002", config, 25);
1061            verify("1003", config, 35);
1062            verify("1004", config, 50);
1063            verify("1005", config, 50);
1064        }
1065    
1066        @Test
1067        public void testMultiTenentConfiguration2() throws Exception
1068        {
1069            factory.setFile(MULTI_TENENT_FILE);
1070            System.setProperty("Id", "1004");
1071    
1072            CombinedConfiguration config = factory.getConfiguration(true);
1073            assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
1074    
1075            verify("1001", config, 15);
1076            verify("1002", config, 25);
1077            verify("1003", config, 35);
1078            verify("1004", config, 50);
1079            verify("1005", config, 50);
1080        }
1081    
1082        @Test
1083        public void testMultiTenentConfiguration3() throws Exception
1084        {
1085            factory.setFile(MULTI_TENENT_FILE);
1086            StringWriter writer = new StringWriter();
1087            WriterAppender app = new WriterAppender(new SimpleLayout(), writer);
1088            Log log = LogFactory.getLog("TestLogger");
1089            Logger logger = ((Log4JLogger)log).getLogger();
1090            logger.addAppender(app);
1091            logger.setLevel(Level.DEBUG);
1092            logger.setAdditivity(false);
1093    
1094            System.setProperty("Id", "1005");
1095    
1096            CombinedConfiguration config = factory.getConfiguration(true);
1097            assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
1098    
1099            verify("1001", config, 15);
1100            String xml = writer.getBuffer().toString();
1101            assertNotNull("No XML returned", xml);
1102            assertTrue("Incorect configuration data", xml.indexOf("<rowsPerPage>15</rowsPerPage>") >= 0);
1103            logger.removeAppender(app);
1104            logger.setLevel(Level.OFF);
1105            verify("1002", config, 25);
1106            verify("1003", config, 35);
1107            verify("1004", config, 50);
1108            verify("1005", config, 50);
1109        }
1110    
1111        @Test
1112        public void testMultiTenantConfigurationAt() throws Exception
1113        {
1114            factory.setFile(MULTI_TENENT_FILE);
1115            System.setProperty("Id", "1001");
1116            CombinedConfiguration config = factory.getConfiguration(true);
1117            HierarchicalConfiguration sub1 = config.configurationAt("Channels/Channel[@id='1']");
1118            assertEquals("My Channel", sub1.getString("Name"));
1119            assertEquals("test 1 data", sub1.getString("ChannelData"));
1120            HierarchicalConfiguration sub2 = config.configurationAt("Channels/Channel[@id='2']");
1121            assertEquals("Channel 2", sub2.getString("Name"));
1122            assertEquals("more test 2 data", sub2.getString("MoreChannelData"));
1123        }
1124    
1125        @Test
1126        public void testMerge() throws Exception
1127        {
1128            factory.setFile(MULTI_TENENT_FILE);
1129            System.setProperty("Id", "1004");
1130            Map<String, String> map = new HashMap<String, String>();
1131            map.put("default", "${colors.header4}");
1132            map.put("background", "#40404040");
1133            map.put("text", "#000000");
1134            map.put("header", "#444444");
1135    
1136            CombinedConfiguration config = factory.getConfiguration(true);
1137            assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
1138    
1139            List<HierarchicalConfiguration> list = config.configurationsAt("colors/*");
1140            Iterator<HierarchicalConfiguration> iter = list.iterator();
1141            while (iter.hasNext())
1142            {
1143                SubnodeConfiguration sub = (SubnodeConfiguration)iter.next();
1144                ConfigurationNode node = sub.getRootNode();
1145                String value = (node.getValue() == null) ? "null" : node.getValue().toString();
1146                if (map.containsKey(node.getName()))
1147                {
1148                    assertEquals(map.get(node.getName()), value);
1149                }
1150            }
1151    
1152        }
1153    
1154        @Test
1155        public void testDelimiterParsingDisabled() throws Exception
1156        {
1157            factory.setFile(MULTI_TENENT_FILE);
1158            System.setProperty("Id", "1004");
1159    
1160            CombinedConfiguration config = factory.getConfiguration(true);
1161            assertTrue("Incorrect configuration", config instanceof DynamicCombinedConfiguration);
1162    
1163            assertEquals("a,b,c", config.getString("split/list3/@values"));
1164            assertEquals(0, config.getMaxIndex("split/list3/@values"));
1165            assertEquals("a\\,b\\,c", config.getString("split/list4/@values"));
1166            assertEquals("a,b,c", config.getString("split/list1"));
1167            assertEquals(0, config.getMaxIndex("split/list1"));
1168            assertEquals("a\\,b\\,c", config.getString("split/list2"));
1169        }
1170    
1171        @Test
1172        public void testExpression() throws Exception
1173        {
1174            if (SystemUtils.isJavaVersionAtLeast(150))
1175            {
1176                factory.setFile(EXPRESSION_FILE);
1177                factory.setAttributeSplittingDisabled(true);
1178                System.getProperties().remove("Id");
1179                org.slf4j.MDC.clear();
1180    
1181                CombinedConfiguration config = factory.getConfiguration(true);
1182                assertTrue("Incorrect configuration",
1183                        config instanceof DynamicCombinedConfiguration);
1184    
1185                verify("1001", config, 15);
1186            }
1187        }
1188    
1189        /**
1190         * Tests whether variable substitution works across multiple child
1191         * configurations. This test is related to CONFIGURATION-481.
1192         */
1193        @Test
1194        public void testInterpolationOverMultipleSources()
1195                throws ConfigurationException
1196        {
1197            File testFile =
1198                    ConfigurationAssert.getTestFile("testInterpolationBuilder.xml");
1199            factory.setFile(testFile);
1200            CombinedConfiguration combConfig = factory.getConfiguration(true);
1201            assertEquals("Wrong value", "abc-product",
1202                    combConfig.getString("products.product.desc"));
1203            XMLConfiguration xmlConfig =
1204                    (XMLConfiguration) combConfig.getConfiguration("test");
1205            assertEquals("Wrong value from XML config", "abc-product",
1206                    xmlConfig.getString("products/product/desc"));
1207            SubnodeConfiguration subConfig =
1208                    xmlConfig
1209                            .configurationAt("products/product[@name='abc']", true);
1210            assertEquals("Wrong value from sub config", "abc-product",
1211                    subConfig.getString("desc"));
1212        }
1213    
1214        private void verify(String key, CombinedConfiguration config, int rows)
1215        {
1216            System.setProperty("Id", key);
1217            org.slf4j.MDC.put("Id", key);
1218            int actual = config.getInt("rowsPerPage");
1219            assertTrue("expected: " + rows + " actual: " + actual, actual == rows);
1220        }
1221    
1222    
1223        /**
1224         * A specialized combined configuration implementation used for testing
1225         * custom result classes.
1226         */
1227        public static class ExtendedCombinedConfiguration extends
1228                CombinedConfiguration
1229        {
1230            /**
1231             * The serial version UID.
1232             */
1233            private static final long serialVersionUID = 4678031745085083392L;
1234    
1235            @Override
1236            public Object getProperty(String key)
1237            {
1238                if (key.equals("test"))
1239                {
1240                    return "Extended";
1241                }
1242                return super.getProperty(key);
1243            }
1244        }
1245    
1246        public static class ExtendedXMLConfiguration extends XMLConfiguration
1247        {
1248            private static final long serialVersionUID = 1L;
1249    
1250            public ExtendedXMLConfiguration()
1251            {
1252            }
1253    
1254        }
1255    
1256        public static class TestLookup extends StrLookup
1257        {
1258            Map<String, String> map = new HashMap<String, String>();
1259    
1260            public TestLookup()
1261            {
1262                map.put("test_file_xml", "test.xml");
1263                map.put("test_file_combine", "testcombine1.xml");
1264                map.put("test_key", "test.value");
1265            }
1266    
1267            @Override
1268            public String lookup(String key)
1269            {
1270                if (key == null)
1271                {
1272                    return null;
1273                }
1274                return map.get(key);
1275    
1276            }
1277        }
1278    }
1279