    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  package org.apache.commons.configuration.tree.xpath;
    18
    19  import java.util.ArrayList;
    20  import java.util.Iterator;
    21  import java.util.List;
    22
    23  import org.apache.commons.configuration.tree.ConfigurationNode;
    24  import org.apache.commons.configuration.tree.DefaultConfigurationNode;
    25  import org.apache.commons.configuration.tree.NodeAddData;
    26  import org.apache.commons.jxpath.JXPathContext;
    27  import org.apache.commons.jxpath.JXPathContextFactory;
    28  import org.apache.commons.jxpath.JXPathContextFactoryConfigurationError;
    29  import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
    30  import org.apache.commons.jxpath.ri.model.NodePointerFactory;
    31
    32  import junit.framework.TestCase;
    33
    34  /***
    35   * Test class for XPathExpressionEngine.
    36   *
    37   * @author Oliver Heger
    38   * @version $Id: TestXPathExpressionEngine.java 439648 2006-09-02 20:42:10Z oheger $
    39   */
    40  public class TestXPathExpressionEngine extends TestCase
    41  {
    42      /*** Constant for the test root node. */
    43      static final ConfigurationNode ROOT = new DefaultConfigurationNode(
    44              "testRoot");
    45
    46      /*** Constant for the valid test key. */
    47      static final String TEST_KEY = "TESTKEY";
    48
    49      /*** The expression engine to be tested. */
    50      XPathExpressionEngine engine;
    51
    52      protected void setUp() throws Exception
    53      {
    54          super.setUp();
    55          initMockContextFactory();
    56          engine = new XPathExpressionEngine();
    57      }
    58
    59      protected void tearDown() throws Exception
    60      {
    61          MockJXPathContextFactory.context = null; // reset context
    62          super.tearDown();
    63      }
    64
    65      /***
    66       * Tests the query() method with a normal expression.
    67       */
    68      public void testQueryExpression()
    69      {
    70          List nodes = engine.query(ROOT, TEST_KEY);
    71          assertEquals("Incorrect number of results", 1, nodes.size());
    72          assertSame("Wrong result node", ROOT, nodes.get(0));
    73          checkSelectCalls(1);
    74      }
    75
    76      /***
    77       * Tests a query that has no results. This should return an empty list.
    78       */
    79      public void testQueryWithoutResult()
    80      {
    81          List nodes = engine.query(ROOT, "a non existing key");
    82          assertTrue("Result list is not empty", nodes.isEmpty());
    83          checkSelectCalls(1);
    84      }
    85
    86      /***
    87       * Tests a query with an empty key. This should directly return the root
    88       * node without invoking the JXPathContext.
    89       */
    90      public void testQueryWithEmptyKey()
    91      {
    92          checkEmptyKey("");
    93      }
    94
    95      /***
    96       * Tests a query with a null key. Same as an empty key.
    97       */
    98      public void testQueryWithNullKey()
    99      {
    100         checkEmptyKey(null);
    101     }
    102
    103     /***
    104      * Helper method for testing undefined keys.
    105      *
    106      * @param key the key
    107      */
    108     private void checkEmptyKey(String key)
    109     {
    110         List nodes = engine.query(ROOT, key);
    111         assertEquals("Incorrect number of results", 1, nodes.size());
    112         assertSame("Wrong result node", ROOT, nodes.get(0));
    113         checkSelectCalls(0);
    114     }
    115
    116     /***
    117      * Tests if the used JXPathContext is correctly initialized.
    118      */
    119     public void testCreateContext()
    120     {
    121         JXPathContext ctx = engine.createContext(ROOT, TEST_KEY);
    122         assertNotNull("Context is null", ctx);
    123         assertTrue("Lenient mode is not set", ctx.isLenient());
    124         assertSame("Incorrect context bean set", ROOT, ctx.getContextBean());
    125
    126         NodePointerFactory[] factories = JXPathContextReferenceImpl
    127                 .getNodePointerFactories();
    128         boolean found = false;
    129         for (int i = 0; i < factories.length; i++)
    130         {
    131             if (factories[i] instanceof ConfigurationNodePointerFactory)
    132             {
    133                 found = true;
    134             }
    135         }
    136         assertTrue("No configuration pointer factory found", found);
    137     }
    138
    139     /***
    140      * Tests a normal call of nodeKey().
    141      */
    142     public void testNodeKeyNormal()
    143     {
    144         assertEquals("Wrong node key", "parent/child", engine.nodeKey(
    145                 new DefaultConfigurationNode("child"), "parent"));
    146     }
    147
    148     /***
    149      * Tests nodeKey() for an attribute node.
    150      */
    151     public void testNodeKeyAttribute()
    152     {
    153         ConfigurationNode node = new DefaultConfigurationNode("attr");
    154         node.setAttribute(true);
    155         assertEquals("Wrong attribute key", "node@attr", engine.nodeKey(node,
    156                 "node"));
    157     }
    158
    159     /***
    160      * Tests nodeKey() for the root node.
    161      */
    162     public void testNodeKeyForRootNode()
    163     {
    164         assertEquals("Wrong key for root node", "", engine.nodeKey(ROOT, null));
    165         assertEquals("Null name not detected", "test", engine.nodeKey(
    166                 new DefaultConfigurationNode(), "test"));
    167     }
    168
    169     /***
    170      * Tests node key() for direct children of the root node.
    171      */
    172     public void testNodeKeyForRootChild()
    173     {
    174         ConfigurationNode node = new DefaultConfigurationNode("child");
    175         assertEquals("Wrong key for root child node", "child", engine.nodeKey(
    176                 node, ""));
    177         node.setAttribute(true);
    178         assertEquals("Wrong key for root attribute", "@child", engine.nodeKey(
    179                 node, ""));
    180     }
    181
    182     /***
    183      * Tests adding a single child node.
    184      */
    185     public void testPrepareAddNode()
    186     {
    187         NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY + "  newNode");
    188         checkAddPath(data, new String[]
    189         { "newNode" }, false);
    190         checkSelectCalls(1);
    191     }
    192
    193     /***
    194      * Tests adding a new attribute node.
    195      */
    196     public void testPrepareAddAttribute()
    197     {
    198         NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY + "\t@newAttr");
    199         checkAddPath(data, new String[]
    200         { "newAttr" }, true);
    201         checkSelectCalls(1);
    202     }
    203
    204     /***
    205      * Tests adding a complete path.
    206      */
    207     public void testPrepareAddPath()
    208     {
    209         NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY
    210                 + " \t a/full/path/node");
    211         checkAddPath(data, new String[]
    212         { "a", "full", "path", "node" }, false);
    213         checkSelectCalls(1);
    214     }
    215
    216     /***
    217      * Tests adding a complete path whose final node is an attribute.
    218      */
    219     public void testPrepareAddAttributePath()
    220     {
    221         NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY
    222                 + " a/full/path@attr");
    223         checkAddPath(data, new String[]
    224         { "a", "full", "path", "attr" }, true);
    225         checkSelectCalls(1);
    226     }
    227
    228     /***
    229      * Tests adding a new node to the root.
    230      */
    231     public void testPrepareAddRootChild()
    232     {
    233         NodeAddData data = engine.prepareAdd(ROOT, " newNode");
    234         checkAddPath(data, new String[]
    235         { "newNode" }, false);
    236         checkSelectCalls(0);
    237     }
    238
    239     /***
    240      * Tests adding a new attribute to the root.
    241      */
    242     public void testPrepareAddRootAttribute()
    243     {
    244         NodeAddData data = engine.prepareAdd(ROOT, " @attr");
    245         checkAddPath(data, new String[]
    246         { "attr" }, true);
    247         checkSelectCalls(0);
    248     }
    249
    250     /***
    251      * Tests an add operation with a query that does not return a single node.
    252      */
    253     public void testPrepareAddInvalidParent()
    254     {
    255         try
    256         {
    257             engine.prepareAdd(ROOT, "invalidKey newNode");
    258             fail("Could add to invalid parent!");
    259         }
    260         catch (IllegalArgumentException iex)
    261         {
    262             // ok
    263         }
    264     }
    265
    266     /***
    267      * Tests an add operation where the passed in key has an invalid format: it
    268      * does not contain a whitspace. This will cause an error.
    269      */
    270     public void testPrepareAddInvalidFormat()
    271     {
    272         try
    273         {
    274             engine.prepareAdd(ROOT, "anInvalidKey");
    275             fail("Could add an invalid key!");
    276         }
    277         catch (IllegalArgumentException iex)
    278         {
    279             // ok
    280         }
    281     }
    282
    283     /***
    284      * Tests an add operation with an empty path for the new node.
    285      */
    286     public void testPrepareAddEmptyPath()
    287     {
    288         try
    289         {
    290             engine.prepareAdd(ROOT, TEST_KEY + " ");
    291             fail("Could add empty path!");
    292         }
    293         catch (IllegalArgumentException iex)
    294         {
    295             // ok
    296         }
    297     }
    298
    299     /***
    300      * Tests an add operation where the key is null.
    301      */
    302     public void testPrepareAddNullKey()
    303     {
    304         try
    305         {
    306             engine.prepareAdd(ROOT, null);
    307             fail("Could add null path!");
    308         }
    309         catch (IllegalArgumentException iex)
    310         {
    311             // ok
    312         }
    313     }
    314
    315     /***
    316      * Tests an add operation where the key is null.
    317      */
    318     public void testPrepareAddEmptyKey()
    319     {
    320         try
    321         {
    322             engine.prepareAdd(ROOT, "");
    323             fail("Could add empty path!");
    324         }
    325         catch (IllegalArgumentException iex)
    326         {
    327             // ok
    328         }
    329     }
    330
    331     /***
    332      * Tests an add operation with an invalid path.
    333      */
    334     public void testPrepareAddInvalidPath()
    335     {
    336         try
    337         {
    338             engine.prepareAdd(ROOT, TEST_KEY + " an/invalid//path");
    339             fail("Could add invalid path!");
    340         }
    341         catch (IllegalArgumentException iex)
    342         {
    343             // ok
    344         }
    345     }
    346
    347     /***
    348      * Tests an add operation with an invalid path: the path contains an
    349      * attribute in the middle part.
    350      */
    351     public void testPrepareAddInvalidAttributePath()
    352     {
    353         try
    354         {
    355             engine.prepareAdd(ROOT, TEST_KEY + " a/path/with@an/attribute");
    356             fail("Could add invalid attribute path!");
    357         }
    358         catch (IllegalArgumentException iex)
    359         {
    360             // ok
    361         }
    362     }
    363
    364     /***
    365      * Tests an add operation with an invalid path: the path contains an
    366      * attribute after a slash.
    367      */
    368     public void testPrepareAddInvalidAttributePath2()
    369     {
    370         try
    371         {
    372             engine.prepareAdd(ROOT, TEST_KEY + " a/path/with/@attribute");
    373             fail("Could add invalid attribute path!");
    374         }
    375         catch (IllegalArgumentException iex)
    376         {
    377             // ok
    378         }
    379     }
    380
    381     /***
    382      * Tests an add operation with an invalid path that starts with a slash.
    383      */
    384     public void testPrepareAddInvalidPathWithSlash()
    385     {
    386         try
    387         {
    388             engine.prepareAdd(ROOT, TEST_KEY + " /a/path/node");
    389             fail("Could add path starting with a slash!");
    390         }
    391         catch (IllegalArgumentException iex)
    392         {
    393             // ok
    394         }
    395     }
    396
    397     /***
    398      * Tests an add operation with an invalid path that contains multiple
    399      * attribute components.
    400      */
    401     public void testPrepareAddInvalidPathMultipleAttributes()
    402     {
    403         try
    404         {
    405             engine.prepareAdd(ROOT, TEST_KEY + " an@attribute@path");
    406             fail("Could add path with multiple attributes!");
    407         }
    408         catch (IllegalArgumentException iex)
    409         {
    410             // ok
    411         }
    412     }
    413
    414     /***
    415      * Helper method for testing the path nodes in the given add data object.
    416      *
    417      * @param data the data object to check
    418      * @param expected an array with the expected path elements
    419      * @param attr a flag if the new node is an attribute
    420      */
    421     private void checkAddPath(NodeAddData data, String[] expected, boolean attr)
    422     {
    423         assertSame("Wrong parent node", ROOT, data.getParent());
    424         List path = data.getPathNodes();
    425         assertEquals("Incorrect number of path nodes", expected.length - 1,
    426                 path.size());
    427         Iterator it = path.iterator();
    428         for (int idx = 0; idx < expected.length - 1; idx++)
    429         {
    430             assertEquals("Wrong node at position " + idx, expected[idx], it
    431                     .next());
    432         }
    433         assertEquals("Wrong name of new node", expected[expected.length - 1],
    434                 data.getNewNodeName());
    435         assertEquals("Incorrect attribute flag", attr, data.isAttribute());
    436     }
    437
    438     /***
    439      * Initializes the mock JXPath context factory. Sets a system property, so
    440      * that this implementation will be used.
    441      */
    442     protected void initMockContextFactory()
    443     {
    444         System.setProperty(JXPathContextFactory.FACTORY_NAME_PROPERTY,
    445                 MockJXPathContextFactory.class.getName());
    446     }
    447
    448     /***
    449      * Checks if the JXPath context's selectNodes() method was called as often
    450      * as expected.
    451      *
    452      * @param expected the number of expected calls
    453      */
    454     protected void checkSelectCalls(int expected)
    455     {
    456         MockJXPathContext ctx = MockJXPathContextFactory.getContext();
    457         int calls = (ctx == null) ? 0 : ctx.selectInvocations;
    458         assertEquals("Incorrect number of select calls", expected, calls);
    459     }
    460
    461     /***
    462      * A mock implementation of the JXPathContext class. This implementation
    463      * will overwrite the <code>selectNodes()</code> method that is used by
    464      * <code>XPathExpressionEngine</code> to count the invocations of this
    465      * method.
    466      */
    467     static class MockJXPathContext extends JXPathContextReferenceImpl
    468     {
    469         int selectInvocations;
    470
    471         public MockJXPathContext(Object bean)
    472         {
    473             super(null, bean);
    474         }
    475
    476         /***
    477          * Dummy implementation of this method. If the passed in string is the
    478          * test key, the root node will be returned in the list. Otherwise the
    479          * return value is <b>null</b>.
    480          */
    481         public List selectNodes(String xpath)
    482         {
    483             selectInvocations++;
    484             if (TEST_KEY.equals(xpath))
    485             {
    486                 List result = new ArrayList(1);
    487                 result.add(ROOT);
    488                 return result;
    489             }
    490             else
    491             {
    492                 return null;
    493             }
    494         }
    495     }
    496
    497     /***
    498      * A mock implementation of the JXPathContextFactory class. This class is
    499      * used to inject the mock context, so that we can trace the invocations of
    500      * selectNodes().
    501      */
    502     public static class MockJXPathContextFactory extends JXPathContextFactory
    503     {
    504         /*** Stores the context instance. */
    505         static MockJXPathContext context;
    506
    507         public JXPathContext newContext(JXPathContext parentContext,
    508                 Object contextBean)
    509                 throws JXPathContextFactoryConfigurationError
    510         {
    511             context = new MockJXPathContext(contextBean);
    512             return context;
    513         }
    514
    515         /***
    516          * Returns the context created by the last newContext() call.
    517          *
    518          * @return the current context
    519          */
    520         public static MockJXPathContext getContext()
    521         {
    522             return context;
    523         }
    524     }
    525 }