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