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.assertNull;
021    import static org.junit.Assert.assertSame;
022    import static org.junit.Assert.assertTrue;
023    
024    import java.io.File;
025    import java.io.IOException;
026    import java.util.ArrayList;
027    import java.util.HashSet;
028    import java.util.List;
029    import java.util.NoSuchElementException;
030    import java.util.Set;
031    
032    import org.apache.commons.collections.CollectionUtils;
033    import org.apache.commons.configuration.event.ConfigurationEvent;
034    import org.apache.commons.configuration.event.ConfigurationListener;
035    import org.apache.commons.configuration.interpol.ConfigurationInterpolator;
036    import org.apache.commons.configuration.reloading.FileAlwaysReloadingStrategy;
037    import org.apache.commons.configuration.tree.ConfigurationNode;
038    import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
039    import org.apache.commons.lang.text.StrLookup;
040    import org.junit.Before;
041    import org.junit.Rule;
042    import org.junit.Test;
043    import org.junit.rules.TemporaryFolder;
044    
045    /**
046     * Test case for SubnodeConfiguration.
047     *
048     * @author <a
049     * href="http://commons.apache.org/configuration/team-list.html">Commons
050     * Configuration team</a>
051     * @version $Id: TestSubnodeConfiguration.java 1225018 2011-12-27 21:14:59Z oheger $
052     */
053    public class TestSubnodeConfiguration
054    {
055        /** An array with names of tables (test data). */
056        private static final String[] TABLE_NAMES =
057        { "documents", "users" };
058    
059        /** An array with the fields of the test tables (test data). */
060        private static final String[][] TABLE_FIELDS =
061        {
062        { "docid", "docname", "author", "dateOfCreation", "version", "size" },
063        { "userid", "uname", "firstName", "lastName" } };
064    
065        /** Constant for an updated table name.*/
066        private static final String NEW_TABLE_NAME = "newTable";
067    
068        /** A helper object for creating temporary files. */
069        @Rule
070        public TemporaryFolder folder = new TemporaryFolder();
071    
072        /** The parent configuration. */
073        HierarchicalConfiguration parent;
074    
075        /** The subnode configuration to be tested. */
076        SubnodeConfiguration config;
077    
078        /** Stores a counter for the created nodes. */
079        int nodeCounter;
080    
081        @Before
082        public void setUp() throws Exception
083        {
084            parent = setUpParentConfig();
085            nodeCounter = 0;
086        }
087    
088        /**
089         * Tests creation of a subnode config.
090         */
091        @Test
092        public void testInitSubNodeConfig()
093        {
094            setUpSubnodeConfig();
095            assertSame("Wrong root node in subnode", getSubnodeRoot(parent), config
096                    .getRoot());
097            assertSame("Wrong parent config", parent, config.getParent());
098        }
099    
100        /**
101         * Tests constructing a subnode configuration with a null parent. This
102         * should cause an exception.
103         */
104        @Test(expected = IllegalArgumentException.class)
105        public void testInitSubNodeConfigWithNullParent()
106        {
107            config = new SubnodeConfiguration(null, getSubnodeRoot(parent));
108        }
109    
110        /**
111         * Tests constructing a subnode configuration with a null root node. This
112         * should cause an exception.
113         */
114        @Test(expected = IllegalArgumentException.class)
115        public void testInitSubNodeConfigWithNullNode()
116        {
117            config = new SubnodeConfiguration(parent, null);
118        }
119    
120        /**
121         * Tests if properties of the sub node can be accessed.
122         */
123        @Test
124        public void testGetProperties()
125        {
126            setUpSubnodeConfig();
127            assertEquals("Wrong table name", TABLE_NAMES[0], config
128                    .getString("name"));
129            List<Object> fields = config.getList("fields.field.name");
130            assertEquals("Wrong number of fields", TABLE_FIELDS[0].length, fields
131                    .size());
132            for (int i = 0; i < TABLE_FIELDS[0].length; i++)
133            {
134                assertEquals("Wrong field at position " + i, TABLE_FIELDS[0][i],
135                        fields.get(i));
136            }
137        }
138    
139        /**
140         * Tests setting of properties in both the parent and the subnode
141         * configuration and whether the changes are visible to each other.
142         */
143        @Test
144        public void testSetProperty()
145        {
146            setUpSubnodeConfig();
147            config.setProperty(null, "testTable");
148            config.setProperty("name", TABLE_NAMES[0] + "_tested");
149            assertEquals("Root value was not set", "testTable", parent
150                    .getString("tables.table(0)"));
151            assertEquals("Table name was not changed", TABLE_NAMES[0] + "_tested",
152                    parent.getString("tables.table(0).name"));
153    
154            parent.setProperty("tables.table(0).fields.field(1).name", "testField");
155            assertEquals("Field name was not changed", "testField", config
156                    .getString("fields.field(1).name"));
157        }
158    
159        /**
160         * Tests adding of properties.
161         */
162        @Test
163        public void testAddProperty()
164        {
165            setUpSubnodeConfig();
166            config.addProperty("[@table-type]", "test");
167            assertEquals("parent.createNode() was not called", 1, nodeCounter);
168            assertEquals("Attribute not set", "test", parent
169                    .getString("tables.table(0)[@table-type]"));
170    
171            parent.addProperty("tables.table(0).fields.field(-1).name", "newField");
172            List<Object> fields = config.getList("fields.field.name");
173            assertEquals("New field was not added", TABLE_FIELDS[0].length + 1,
174                    fields.size());
175            assertEquals("Wrong last field", "newField", fields
176                    .get(fields.size() - 1));
177        }
178    
179        /**
180         * Tests listing the defined keys.
181         */
182        @Test
183        public void testGetKeys()
184        {
185            setUpSubnodeConfig();
186            Set<String> keys = new HashSet<String>();
187            CollectionUtils.addAll(keys, config.getKeys());
188            assertEquals("Incorrect number of keys", 2, keys.size());
189            assertTrue("Key 1 not contained", keys.contains("name"));
190            assertTrue("Key 2 not contained", keys.contains("fields.field.name"));
191        }
192    
193        /**
194         * Tests setting the exception on missing flag. The subnode config obtains
195         * this flag from its parent.
196         */
197        @Test(expected = NoSuchElementException.class)
198        public void testSetThrowExceptionOnMissing()
199        {
200            parent.setThrowExceptionOnMissing(true);
201            setUpSubnodeConfig();
202            assertTrue("Exception flag not fetchted from parent", config
203                    .isThrowExceptionOnMissing());
204            config.getString("non existing key");
205        }
206    
207        /**
208         * Tests whether the exception flag can be set independently from the parent.
209         */
210        @Test
211        public void testSetThrowExceptionOnMissingAffectsParent()
212        {
213            parent.setThrowExceptionOnMissing(true);
214            setUpSubnodeConfig();
215            config.setThrowExceptionOnMissing(false);
216            assertTrue("Exception flag reset on parent", parent
217                    .isThrowExceptionOnMissing());
218        }
219    
220        /**
221         * Tests handling of the delimiter parsing disabled flag. This is shared
222         * with the parent, too.
223         */
224        @Test
225        public void testSetDelimiterParsingDisabled()
226        {
227            parent.setDelimiterParsingDisabled(true);
228            setUpSubnodeConfig();
229            parent.setDelimiterParsingDisabled(false);
230            assertTrue("Delimiter parsing flag was not received from parent",
231                    config.isDelimiterParsingDisabled());
232            config.addProperty("newProp", "test1,test2,test3");
233            assertEquals("New property was splitted", "test1,test2,test3", parent
234                    .getString("tables.table(0).newProp"));
235            parent.setDelimiterParsingDisabled(true);
236            config.setDelimiterParsingDisabled(false);
237            assertTrue("Delimiter parsing flag was reset on parent", parent
238                    .isDelimiterParsingDisabled());
239        }
240    
241        /**
242         * Tests manipulating the list delimiter. This piece of data is derived from
243         * the parent.
244         */
245        @Test
246        public void testSetListDelimiter()
247        {
248            parent.setListDelimiter('/');
249            setUpSubnodeConfig();
250            parent.setListDelimiter(';');
251            assertEquals("List delimiter not obtained from parent", '/', config
252                    .getListDelimiter());
253            config.addProperty("newProp", "test1,test2/test3");
254            assertEquals("List was incorrectly splitted", "test1,test2", parent
255                    .getString("tables.table(0).newProp"));
256            config.setListDelimiter(',');
257            assertEquals("List delimiter changed on parent", ';', parent
258                    .getListDelimiter());
259        }
260    
261        /**
262         * Tests changing the expression engine.
263         */
264        @Test
265        public void testSetExpressionEngine()
266        {
267            parent.setExpressionEngine(new XPathExpressionEngine());
268            setUpSubnodeConfig();
269            assertEquals("Wrong field name", TABLE_FIELDS[0][1], config
270                    .getString("fields/field[2]/name"));
271            Set<String> keys = new HashSet<String>();
272            CollectionUtils.addAll(keys, config.getKeys());
273            assertEquals("Wrong number of keys", 2, keys.size());
274            assertTrue("Key 1 not contained", keys.contains("name"));
275            assertTrue("Key 2 not contained", keys.contains("fields/field/name"));
276            config.setExpressionEngine(null);
277            assertTrue("Expression engine reset on parent", parent
278                    .getExpressionEngine() instanceof XPathExpressionEngine);
279        }
280    
281        /**
282         * Tests the configurationAt() method.
283         */
284        @Test
285        public void testConfiguarationAt()
286        {
287            setUpSubnodeConfig();
288            SubnodeConfiguration sub2 = config
289                    .configurationAt("fields.field(1)");
290            assertEquals("Wrong value of property", TABLE_FIELDS[0][1], sub2
291                    .getString("name"));
292            assertEquals("Wrong parent", config.getParent(), sub2.getParent());
293        }
294    
295        /**
296         * Tests interpolation features. The subnode config should use its parent
297         * for interpolation.
298         */
299        @Test
300        public void testInterpolation()
301        {
302            parent.addProperty("tablespaces.tablespace.name", "default");
303            parent.addProperty("tablespaces.tablespace(-1).name", "test");
304            parent.addProperty("tables.table(0).tablespace",
305                    "${tablespaces.tablespace(0).name}");
306            assertEquals("Wrong interpolated tablespace", "default", parent
307                    .getString("tables.table(0).tablespace"));
308    
309            setUpSubnodeConfig();
310            assertEquals("Wrong interpolated tablespace in subnode", "default",
311                    config.getString("tablespace"));
312        }
313    
314        /**
315         * An additional test for interpolation when the configurationAt() method is
316         * involved.
317         */
318        @Test
319        public void testInterpolationFromConfigurationAt()
320        {
321            parent.addProperty("base.dir", "/home/foo");
322            parent.addProperty("test.absolute.dir.dir1", "${base.dir}/path1");
323            parent.addProperty("test.absolute.dir.dir2", "${base.dir}/path2");
324            parent.addProperty("test.absolute.dir.dir3", "${base.dir}/path3");
325    
326            Configuration sub = parent.configurationAt("test.absolute.dir");
327            for (int i = 1; i < 4; i++)
328            {
329                assertEquals("Wrong interpolation in parent", "/home/foo/path" + i,
330                        parent.getString("test.absolute.dir.dir" + i));
331                assertEquals("Wrong interpolation in subnode",
332                        "/home/foo/path" + i, sub.getString("dir" + i));
333            }
334        }
335    
336        /**
337         * An additional test for interpolation when the configurationAt() method is
338         * involved for a local interpolation.
339         */
340        @Test
341        public void testLocalInterpolationFromConfigurationAt()
342        {
343            parent.addProperty("base.dir", "/home/foo");
344            parent.addProperty("test.absolute.dir.dir1", "${base.dir}/path1");
345            parent.addProperty("test.absolute.dir.dir2", "${dir1}");
346    
347            Configuration sub = parent.configurationAt("test.absolute.dir");
348            assertEquals("Wrong interpolation in subnode",
349                "/home/foo/path1", sub.getString("dir1"));
350            assertEquals("Wrong local interpolation in subnode",
351                "/home/foo/path1", sub.getString("dir2"));
352        }
353    
354        /**
355         * Tests manipulating the interpolator.
356         */
357        @Test
358        public void testInterpolator()
359        {
360            parent.addProperty("tablespaces.tablespace.name", "default");
361            parent.addProperty("tablespaces.tablespace(-1).name", "test");
362    
363            setUpSubnodeConfig();
364            InterpolationTestHelper.testGetInterpolator(config);
365        }
366    
367        @Test
368        public void testLocalLookupsInInterpolatorAreInherited() {
369            parent.addProperty("tablespaces.tablespace.name", "default");
370            parent.addProperty("tablespaces.tablespace(-1).name", "test");
371            parent.addProperty("tables.table(0).var", "${brackets:x}");
372    
373            ConfigurationInterpolator interpolator = parent.getInterpolator();
374            interpolator.registerLookup("brackets", new StrLookup(){
375    
376                @Override
377                public String lookup(String key) {
378                    return "(" + key +")";
379                }
380    
381            });
382            setUpSubnodeConfig();
383            assertEquals("Local lookup was not inherited", "(x)", config.getString("var", ""));
384        }
385    
386        /**
387         * Tests a reload operation for the parent configuration when the subnode
388         * configuration does not support reloads. Then the new value should not be
389         * detected.
390         */
391        @Test
392        public void testParentReloadNotSupported() throws ConfigurationException
393        {
394            Configuration c = setUpReloadTest(false);
395            assertEquals("Name changed in sub config", TABLE_NAMES[1], config
396                    .getString("name"));
397            assertEquals("Name not changed in parent", NEW_TABLE_NAME, c
398                    .getString("tables.table(1).name"));
399        }
400    
401        /**
402         * Tests a reload operation for the parent configuration when the subnode
403         * configuration does support reloads. The new value should be returned.
404         */
405        @Test
406        public void testParentReloadSupported() throws ConfigurationException
407        {
408            Configuration c = setUpReloadTest(true);
409            assertEquals("Name not changed in sub config", NEW_TABLE_NAME, config
410                    .getString("name"));
411            assertEquals("Name not changed in parent", NEW_TABLE_NAME, c
412                    .getString("tables.table(1).name"));
413        }
414    
415        /**
416         * Tests whether events are fired if a change of the parent is detected.
417         */
418        @Test
419        public void testParentReloadEvents() throws ConfigurationException
420        {
421            setUpReloadTest(true);
422            ConfigurationListenerTestImpl l = new ConfigurationListenerTestImpl();
423            config.addConfigurationListener(l);
424            config.getString("name");
425            assertEquals("Wrong number of events", 2, l.events.size());
426            boolean before = true;
427            for (ConfigurationEvent e : l.events)
428            {
429                assertEquals("Wrong configuration", config, e.getSource());
430                assertEquals("Wrong event type",
431                        HierarchicalConfiguration.EVENT_SUBNODE_CHANGED, e
432                                .getType());
433                assertNull("Got a property name", e.getPropertyName());
434                assertNull("Got a property value", e.getPropertyValue());
435                assertEquals("Wrong before flag", before, e.isBeforeUpdate());
436                before = !before;
437            }
438        }
439    
440        /**
441         * Tests a reload operation for the parent configuration when the subnode
442         * configuration is aware of reloads, and the parent configuration is
443         * accessed first. The new value should be returned.
444         */
445        @Test
446        public void testParentReloadSupportAccessParent()
447                throws ConfigurationException
448        {
449            Configuration c = setUpReloadTest(true);
450            assertEquals("Name not changed in parent", NEW_TABLE_NAME, c
451                    .getString("tables.table(1).name"));
452            assertEquals("Name not changed in sub config", NEW_TABLE_NAME, config
453                    .getString("name"));
454        }
455    
456        /**
457         * Tests whether reloads work with sub subnode configurations.
458         */
459        @Test
460        public void testParentReloadSubSubnode() throws ConfigurationException
461        {
462            setUpReloadTest(true);
463            SubnodeConfiguration sub = config.configurationAt("fields", true);
464            assertEquals("Wrong subnode key", "tables.table(1).fields", sub
465                    .getSubnodeKey());
466            assertEquals("Changed field not detected", "newField", sub
467                    .getString("field(0).name"));
468        }
469    
470        /**
471         * Tests creating a sub sub config when the sub config is not aware of
472         * changes. Then the sub sub config shouldn't be either.
473         */
474        @Test
475        public void testParentReloadSubSubnodeNoChangeSupport()
476                throws ConfigurationException
477        {
478            setUpReloadTest(false);
479            SubnodeConfiguration sub = config.configurationAt("fields", true);
480            assertNull("Sub sub config is attached to parent", sub.getSubnodeKey());
481            assertEquals("Changed field name returned", TABLE_FIELDS[1][0], sub
482                    .getString("field(0).name"));
483        }
484    
485        /**
486         * Prepares a test for a reload operation.
487         *
488         * @param supportReload a flag whether the subnode configuration should
489         * support reload operations
490         * @return the parent configuration that can be used for testing
491         * @throws ConfigurationException if an error occurs
492         */
493        private XMLConfiguration setUpReloadTest(boolean supportReload)
494                throws ConfigurationException
495        {
496            try
497            {
498                File testFile = folder.newFile();
499                XMLConfiguration xmlConf = new XMLConfiguration(parent);
500                xmlConf.setFile(testFile);
501                xmlConf.save();
502                config = xmlConf.configurationAt("tables.table(1)", supportReload);
503                assertEquals("Wrong table name", TABLE_NAMES[1],
504                        config.getString("name"));
505                xmlConf.setReloadingStrategy(new FileAlwaysReloadingStrategy());
506                // Now change the configuration file
507                XMLConfiguration confUpdate = new XMLConfiguration(testFile);
508                confUpdate.setProperty("tables.table(1).name", NEW_TABLE_NAME);
509                confUpdate.setProperty("tables.table(1).fields.field(0).name",
510                        "newField");
511                confUpdate.save();
512                return xmlConf;
513            }
514            catch (IOException ioex)
515            {
516                throw new ConfigurationException(ioex);
517            }
518        }
519    
520        /**
521         * Tests a manipulation of the parent configuration that causes the subnode
522         * configuration to become invalid. In this case the sub config should be
523         * detached and keep its old values.
524         */
525        @Test
526        public void testParentChangeDetach()
527        {
528            final String key = "tables.table(1)";
529            config = parent.configurationAt(key, true);
530            assertEquals("Wrong subnode key", key, config.getSubnodeKey());
531            assertEquals("Wrong table name", TABLE_NAMES[1], config
532                    .getString("name"));
533            parent.clearTree(key);
534            assertEquals("Wrong table name after change", TABLE_NAMES[1], config
535                    .getString("name"));
536            assertNull("Sub config was not detached", config.getSubnodeKey());
537        }
538    
539        /**
540         * Tests detaching a subnode configuration when an exception is thrown
541         * during reconstruction. This can happen e.g. if the expression engine is
542         * changed for the parent.
543         */
544        @Test
545        public void testParentChangeDetatchException()
546        {
547            config = parent.configurationAt("tables.table(1)", true);
548            parent.setExpressionEngine(new XPathExpressionEngine());
549            assertEquals("Wrong name of table", TABLE_NAMES[1], config
550                    .getString("name"));
551            assertNull("Sub config was not detached", config.getSubnodeKey());
552        }
553    
554        /**
555         * Initializes the parent configuration. This method creates the typical
556         * structure of tables and fields nodes.
557         *
558         * @return the parent configuration
559         */
560        protected HierarchicalConfiguration setUpParentConfig()
561        {
562            HierarchicalConfiguration conf = new HierarchicalConfiguration()
563            {
564                /**
565                 * Serial version UID.
566                 */
567                private static final long serialVersionUID = 1L;
568    
569                // Provide a special implementation of createNode() to check
570                // if it is called by the subnode config
571                @Override
572                protected Node createNode(String name)
573                {
574                    nodeCounter++;
575                    return super.createNode(name);
576                }
577            };
578            for (int i = 0; i < TABLE_NAMES.length; i++)
579            {
580                conf.addProperty("tables.table(-1).name", TABLE_NAMES[i]);
581                for (int j = 0; j < TABLE_FIELDS[i].length; j++)
582                {
583                    conf.addProperty("tables.table.fields.field(-1).name",
584                            TABLE_FIELDS[i][j]);
585                }
586            }
587            return conf;
588        }
589    
590        /**
591         * Returns the root node for the subnode config. This method returns the
592         * first table node.
593         *
594         * @param conf the parent config
595         * @return the root node for the subnode config
596         */
597        protected ConfigurationNode getSubnodeRoot(HierarchicalConfiguration conf)
598        {
599            ConfigurationNode root = conf.getRoot();
600            return root.getChild(0).getChild(0);
601        }
602    
603        /**
604         * Performs a standard initialization of the subnode config to test.
605         */
606        protected void setUpSubnodeConfig()
607        {
608            config = new SubnodeConfiguration(parent, getSubnodeRoot(parent));
609        }
610    
611        /**
612         * A specialized configuration listener for testing whether the expected
613         * events are fired.
614         */
615        private static class ConfigurationListenerTestImpl implements ConfigurationListener
616        {
617            /** Stores the events received.*/
618            final List<ConfigurationEvent> events = new ArrayList<ConfigurationEvent>();
619    
620            public void configurationChanged(ConfigurationEvent event)
621            {
622                events.add(event);
623            }
624        }
625    }