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.tree.xpath; 018 019 import static org.junit.Assert.assertEquals; 020 import static org.junit.Assert.assertNotNull; 021 import static org.junit.Assert.assertSame; 022 import static org.junit.Assert.assertTrue; 023 024 import java.util.ArrayList; 025 import java.util.Iterator; 026 import java.util.List; 027 028 import org.apache.commons.configuration.tree.ConfigurationNode; 029 import org.apache.commons.configuration.tree.DefaultConfigurationNode; 030 import org.apache.commons.configuration.tree.NodeAddData; 031 import org.apache.commons.jxpath.JXPathContext; 032 import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl; 033 import org.apache.commons.jxpath.ri.model.NodePointerFactory; 034 import org.junit.Before; 035 import org.junit.Test; 036 037 /** 038 * Test class for XPathExpressionEngine. 039 * 040 * @author <a 041 * href="http://commons.apache.org/configuration/team-list.html">Commons 042 * Configuration team</a> 043 * @version $Id: TestXPathExpressionEngine.java 1226111 2011-12-31 15:44:50Z oheger $ 044 */ 045 public class TestXPathExpressionEngine 046 { 047 /** Constant for the test root node. */ 048 static final ConfigurationNode ROOT = new DefaultConfigurationNode( 049 "testRoot"); 050 051 /** Constant for the valid test key. */ 052 static final String TEST_KEY = "TESTKEY"; 053 054 /** The expression engine to be tested. */ 055 XPathExpressionEngine engine; 056 057 @Before 058 public void setUp() throws Exception 059 { 060 engine = new MockJXPathContextExpressionEngine(); 061 } 062 063 /** 064 * Tests the query() method with a normal expression. 065 */ 066 @Test 067 public void testQueryExpression() 068 { 069 List<ConfigurationNode> nodes = engine.query(ROOT, TEST_KEY); 070 assertEquals("Incorrect number of results", 1, nodes.size()); 071 assertSame("Wrong result node", ROOT, nodes.get(0)); 072 checkSelectCalls(1); 073 } 074 075 /** 076 * Tests a query that has no results. This should return an empty list. 077 */ 078 @Test 079 public void testQueryWithoutResult() 080 { 081 List<ConfigurationNode> nodes = engine.query(ROOT, "a non existing key"); 082 assertTrue("Result list is not empty", nodes.isEmpty()); 083 checkSelectCalls(1); 084 } 085 086 /** 087 * Tests a query with an empty key. This should directly return the root 088 * node without invoking the JXPathContext. 089 */ 090 @Test 091 public void testQueryWithEmptyKey() 092 { 093 checkEmptyKey(""); 094 } 095 096 /** 097 * Tests a query with a null key. Same as an empty key. 098 */ 099 @Test 100 public void testQueryWithNullKey() 101 { 102 checkEmptyKey(null); 103 } 104 105 /** 106 * Helper method for testing undefined keys. 107 * 108 * @param key the key 109 */ 110 private void checkEmptyKey(String key) 111 { 112 List<ConfigurationNode> nodes = engine.query(ROOT, key); 113 assertEquals("Incorrect number of results", 1, nodes.size()); 114 assertSame("Wrong result node", ROOT, nodes.get(0)); 115 checkSelectCalls(0); 116 } 117 118 /** 119 * Tests if the used JXPathContext is correctly initialized. 120 */ 121 @Test 122 public void testCreateContext() 123 { 124 JXPathContext ctx = new XPathExpressionEngine().createContext(ROOT, 125 TEST_KEY); 126 assertNotNull("Context is null", ctx); 127 assertTrue("Lenient mode is not set", ctx.isLenient()); 128 assertSame("Incorrect context bean set", ROOT, ctx.getContextBean()); 129 130 NodePointerFactory[] factories = JXPathContextReferenceImpl 131 .getNodePointerFactories(); 132 boolean found = false; 133 for (int i = 0; i < factories.length; i++) 134 { 135 if (factories[i] instanceof ConfigurationNodePointerFactory) 136 { 137 found = true; 138 } 139 } 140 assertTrue("No configuration pointer factory found", found); 141 } 142 143 /** 144 * Tests a normal call of nodeKey(). 145 */ 146 @Test 147 public void testNodeKeyNormal() 148 { 149 assertEquals("Wrong node key", "parent/child", engine.nodeKey( 150 new DefaultConfigurationNode("child"), "parent")); 151 } 152 153 /** 154 * Tests nodeKey() for an attribute node. 155 */ 156 @Test 157 public void testNodeKeyAttribute() 158 { 159 ConfigurationNode node = new DefaultConfigurationNode("attr"); 160 node.setAttribute(true); 161 assertEquals("Wrong attribute key", "node/@attr", engine.nodeKey(node, 162 "node")); 163 } 164 165 /** 166 * Tests nodeKey() for the root node. 167 */ 168 @Test 169 public void testNodeKeyForRootNode() 170 { 171 assertEquals("Wrong key for root node", "", engine.nodeKey(ROOT, null)); 172 assertEquals("Null name not detected", "test", engine.nodeKey( 173 new DefaultConfigurationNode(), "test")); 174 } 175 176 /** 177 * Tests node key() for direct children of the root node. 178 */ 179 @Test 180 public void testNodeKeyForRootChild() 181 { 182 ConfigurationNode node = new DefaultConfigurationNode("child"); 183 assertEquals("Wrong key for root child node", "child", engine.nodeKey( 184 node, "")); 185 node.setAttribute(true); 186 assertEquals("Wrong key for root attribute", "@child", engine.nodeKey( 187 node, "")); 188 } 189 190 /** 191 * Tests adding a single child node. 192 */ 193 @Test 194 public void testPrepareAddNode() 195 { 196 NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY + " newNode"); 197 checkAddPath(data, new String[] 198 { "newNode" }, false); 199 checkSelectCalls(1); 200 } 201 202 /** 203 * Tests adding a new attribute node. 204 */ 205 @Test 206 public void testPrepareAddAttribute() 207 { 208 NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY + "\t@newAttr"); 209 checkAddPath(data, new String[] 210 { "newAttr" }, true); 211 checkSelectCalls(1); 212 } 213 214 /** 215 * Tests adding a complete path. 216 */ 217 @Test 218 public void testPrepareAddPath() 219 { 220 NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY 221 + " \t a/full/path/node"); 222 checkAddPath(data, new String[] 223 { "a", "full", "path", "node" }, false); 224 checkSelectCalls(1); 225 } 226 227 /** 228 * Tests adding a complete path whose final node is an attribute. 229 */ 230 @Test 231 public void testPrepareAddAttributePath() 232 { 233 NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY 234 + " a/full/path@attr"); 235 checkAddPath(data, new String[] 236 { "a", "full", "path", "attr" }, true); 237 checkSelectCalls(1); 238 } 239 240 /** 241 * Tests adding a new node to the root. 242 */ 243 @Test 244 public void testPrepareAddRootChild() 245 { 246 NodeAddData data = engine.prepareAdd(ROOT, " newNode"); 247 checkAddPath(data, new String[] 248 { "newNode" }, false); 249 checkSelectCalls(0); 250 } 251 252 /** 253 * Tests adding a new attribute to the root. 254 */ 255 @Test 256 public void testPrepareAddRootAttribute() 257 { 258 NodeAddData data = engine.prepareAdd(ROOT, " @attr"); 259 checkAddPath(data, new String[] 260 { "attr" }, true); 261 checkSelectCalls(0); 262 } 263 264 /** 265 * Tests an add operation with a query that does not return a single node. 266 */ 267 @Test(expected = IllegalArgumentException.class) 268 public void testPrepareAddInvalidParent() 269 { 270 engine.prepareAdd(ROOT, "invalidKey newNode"); 271 } 272 273 /** 274 * Tests an add operation with an empty path for the new node. 275 */ 276 @Test(expected = IllegalArgumentException.class) 277 public void testPrepareAddEmptyPath() 278 { 279 engine.prepareAdd(ROOT, TEST_KEY + " "); 280 } 281 282 /** 283 * Tests an add operation where the key is null. 284 */ 285 @Test(expected = IllegalArgumentException.class) 286 public void testPrepareAddNullKey() 287 { 288 engine.prepareAdd(ROOT, null); 289 } 290 291 /** 292 * Tests an add operation where the key is null. 293 */ 294 @Test(expected = IllegalArgumentException.class) 295 public void testPrepareAddEmptyKey() 296 { 297 engine.prepareAdd(ROOT, ""); 298 } 299 300 /** 301 * Tests an add operation with an invalid path. 302 */ 303 @Test(expected = IllegalArgumentException.class) 304 public void testPrepareAddInvalidPath() 305 { 306 engine.prepareAdd(ROOT, TEST_KEY + " an/invalid//path"); 307 } 308 309 /** 310 * Tests an add operation with an invalid path: the path contains an 311 * attribute in the middle part. 312 */ 313 @Test(expected = IllegalArgumentException.class) 314 public void testPrepareAddInvalidAttributePath() 315 { 316 engine.prepareAdd(ROOT, TEST_KEY + " a/path/with@an/attribute"); 317 } 318 319 /** 320 * Tests an add operation with an invalid path: the path contains an 321 * attribute after a slash. 322 */ 323 @Test(expected = IllegalArgumentException.class) 324 public void testPrepareAddInvalidAttributePath2() 325 { 326 engine.prepareAdd(ROOT, TEST_KEY + " a/path/with/@attribute"); 327 } 328 329 /** 330 * Tests an add operation with an invalid path that starts with a slash. 331 */ 332 @Test(expected = IllegalArgumentException.class) 333 public void testPrepareAddInvalidPathWithSlash() 334 { 335 engine.prepareAdd(ROOT, TEST_KEY + " /a/path/node"); 336 } 337 338 /** 339 * Tests an add operation with an invalid path that contains multiple 340 * attribute components. 341 */ 342 @Test(expected = IllegalArgumentException.class) 343 public void testPrepareAddInvalidPathMultipleAttributes() 344 { 345 engine.prepareAdd(ROOT, TEST_KEY + " an@attribute@path"); 346 } 347 348 /** 349 * Helper method for testing the path nodes in the given add data object. 350 * 351 * @param data the data object to check 352 * @param expected an array with the expected path elements 353 * @param attr a flag if the new node is an attribute 354 */ 355 private void checkAddPath(NodeAddData data, String[] expected, boolean attr) 356 { 357 assertSame("Wrong parent node", ROOT, data.getParent()); 358 List<String> path = data.getPathNodes(); 359 assertEquals("Incorrect number of path nodes", expected.length - 1, 360 path.size()); 361 Iterator<String> it = path.iterator(); 362 for (int idx = 0; idx < expected.length - 1; idx++) 363 { 364 assertEquals("Wrong node at position " + idx, expected[idx], it 365 .next()); 366 } 367 assertEquals("Wrong name of new node", expected[expected.length - 1], 368 data.getNewNodeName()); 369 assertEquals("Incorrect attribute flag", attr, data.isAttribute()); 370 } 371 372 /** 373 * Checks if the JXPath context's selectNodes() method was called as often 374 * as expected. 375 * 376 * @param expected the number of expected calls 377 */ 378 protected void checkSelectCalls(int expected) 379 { 380 MockJXPathContext ctx = ((MockJXPathContextExpressionEngine) engine).getContext(); 381 int calls = (ctx == null) ? 0 : ctx.selectInvocations; 382 assertEquals("Incorrect number of select calls", expected, calls); 383 } 384 385 /** 386 * A mock implementation of the JXPathContext class. This implementation 387 * will overwrite the <code>selectNodes()</code> method that is used by 388 * <code>XPathExpressionEngine</code> to count the invocations of this 389 * method. 390 */ 391 static class MockJXPathContext extends JXPathContextReferenceImpl 392 { 393 int selectInvocations; 394 395 public MockJXPathContext(Object bean) 396 { 397 super(null, bean); 398 } 399 400 /** 401 * Dummy implementation of this method. If the passed in string is the 402 * test key, the root node will be returned in the list. Otherwise the 403 * return value is <b>null</b>. 404 */ 405 @Override 406 public List<?> selectNodes(String xpath) 407 { 408 selectInvocations++; 409 if (TEST_KEY.equals(xpath)) 410 { 411 List<ConfigurationNode> result = new ArrayList<ConfigurationNode>(1); 412 result.add(ROOT); 413 return result; 414 } 415 else 416 { 417 return null; 418 } 419 } 420 } 421 422 /** 423 * A special implementation of XPathExpressionEngine that overrides 424 * createContext() to return a mock context object. 425 */ 426 static class MockJXPathContextExpressionEngine extends 427 XPathExpressionEngine 428 { 429 /** Stores the context instance. */ 430 private MockJXPathContext context; 431 432 @Override 433 protected JXPathContext createContext(ConfigurationNode root, String key) 434 { 435 context = new MockJXPathContext(root); 436 return context; 437 } 438 439 /** 440 * Returns the context created by the last newContext() call. 441 * 442 * @return the current context 443 */ 444 public MockJXPathContext getContext() 445 { 446 return context; 447 } 448 } 449 }