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 * https://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.configuration2.tree;
18
19 import static org.mockito.ArgumentMatchers.any;
20 import static org.mockito.Mockito.mock;
21 import static org.mockito.Mockito.when;
22
23 import java.util.NoSuchElementException;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26
27 import org.apache.commons.lang3.StringUtils;
28
29 /**
30 * A helper class for tests related to hierarchies of {@code ImmutableNode} objects. This class provides functionality
31 * for creating test trees and accessing single nodes. It can be used by various test classes.
32 */
33 public final class NodeStructureHelper {
34
35 /** A pattern for parsing node keys with optional indices. */
36 private static final Pattern PAT_KEY_WITH_INDEX = Pattern.compile("(\\w+)\\((\\d+)\\)");
37
38 /** The character for splitting node path elements. */
39 private static final String PATH_SEPARATOR = "/";
40
41 /** An array with authors. */
42 private static final String[] AUTHORS = {"Shakespeare", "Homer", "Simmons"};
43
44 /** An array with the works of the test authors. */
45 private static final String[][] WORKS = {{"Troilus and Cressida", "The Tempest", "A Midsummer Night's Dream"}, {"Ilias"}, {"Ilium", "Hyperion"}};
46
47 /** An array with the personae in the works. */
48 private static final String[][][] PERSONAE = {{
49 // Works of Shakespeare
50 {"Troilus", "Cressidia", "Ajax", "Achilles"}, {"Prospero", "Ariel"}, {"Oberon", "Titania", "Puck"}},
51 {
52 // Works of Homer
53 {"Achilles", "Agamemnon", "Hektor"}},
54 {
55 // Works of Dan Simmons
56 {"Hockenberry", "Achilles"}, {"Shrike", "Moneta", "Consul", "Weintraub"}}};
57
58 /** An array with table names used for the TABLES tree. */
59 private static final String[] TABLES = {"users", "documents"};
60
61 /**
62 * An array with the names of columns to be used for the TABLES tree.
63 */
64 private static final String[][] FIELDS = {{"uid", "uname", "firstName", "lastName", "email"},
65 {"docid", "name", "creationDate", "authorID", "version", "length"}};
66
67 /** Constant for the author attribute. */
68 public static final String ATTR_AUTHOR = "author";
69
70 /** Constant for the original value element in the personae tree. */
71 public static final String ELEM_ORG_VALUE = "originalValue";
72
73 /** Constant for the tested attribute. */
74 public static final String ATTR_TESTED = "tested";
75
76 /** The root node of the authors tree. */
77 public static final ImmutableNode ROOT_AUTHORS_TREE = createAuthorsTree();
78
79 /** The root node of the personae tree. */
80 public static final ImmutableNode ROOT_PERSONAE_TREE = createPersonaeTree();
81
82 /** The root node of the TABLES tree. */
83 public static final ImmutableNode ROOT_TABLES_TREE = createTablesTree();
84
85 /**
86 * Appends a component to a node path. The component is added separated by a path separator.
87 *
88 * @param path the path
89 * @param component the component to be added
90 * @return the resulting path
91 */
92 public static String appendPath(final String path, final String component) {
93 final StringBuilder buf = new StringBuilder(StringUtils.length(path) + StringUtils.length(component) + 1);
94 buf.append(path).append(PATH_SEPARATOR).append(component);
95 return buf.toString();
96 }
97
98 /**
99 * Returns the name of the author at the given index.
100 *
101 * @param idx the index
102 * @return the name of this author
103 */
104 public static String author(final int idx) {
105 return AUTHORS[idx];
106 }
107
108 /**
109 * Returns the number of authors.
110 *
111 * @return the number of authors
112 */
113 public static int authorsLength() {
114 return AUTHORS.length;
115 }
116
117 /**
118 * Creates a tree with a root node whose children are the test authors. Each other has his works as child nodes. Each
119 * work has its personae as children.
120 *
121 * @return the root node of the authors tree
122 */
123 private static ImmutableNode createAuthorsTree() {
124 final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder(AUTHORS.length);
125 for (int author = 0; author < AUTHORS.length; author++) {
126 final ImmutableNode.Builder authorBuilder = new ImmutableNode.Builder();
127 authorBuilder.name(AUTHORS[author]);
128 for (int work = 0; work < WORKS[author].length; work++) {
129 final ImmutableNode.Builder workBuilder = new ImmutableNode.Builder();
130 workBuilder.name(WORKS[author][work]);
131 for (final String person : PERSONAE[author][work]) {
132 workBuilder.addChild(new ImmutableNode.Builder().name(person).create());
133 }
134 authorBuilder.addChild(workBuilder.create());
135 }
136 rootBuilder.addChild(authorBuilder.create());
137 }
138 return rootBuilder.name("authorTree").create();
139 }
140
141 /**
142 * Helper method for creating a field node with its children. Nodes of this type are used within the tables tree. They
143 * define a single column of a table.
144 *
145 * @param name the name of the field
146 * @return the field node
147 */
148 public static ImmutableNode createFieldNode(final String name) {
149 final ImmutableNode.Builder fldBuilder = new ImmutableNode.Builder(1);
150 fldBuilder.addChild(createNode("name", name));
151 return fldBuilder.name("field").create();
152 }
153
154 /**
155 * Helper method for creating an immutable node with a name and a value.
156 *
157 * @param name the node's name
158 * @param value the node's value
159 * @return the new node
160 */
161 public static ImmutableNode createNode(final String name, final Object value) {
162 return new ImmutableNode.Builder().name(name).value(value).create();
163 }
164
165 /**
166 * Creates a tree with a root node whose children are the test personae. Each node represents a person and has an
167 * attribute pointing to the author who invented this person. There is a single child node for the associated work which
168 * has again a child and an attribute.
169 *
170 * @return the root node of the personae tree
171 */
172 private static ImmutableNode createPersonaeTree() {
173 final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder();
174 for (int author = 0; author < AUTHORS.length; author++) {
175 for (int work = 0; work < WORKS[author].length; work++) {
176 for (final String person : PERSONAE[author][work]) {
177 final ImmutableNode orgValue = new ImmutableNode.Builder().name(ELEM_ORG_VALUE).value("yes").addAttribute(ATTR_TESTED, Boolean.FALSE)
178 .create();
179 final ImmutableNode workNode = new ImmutableNode.Builder(1).name(WORKS[author][work]).addChild(orgValue).create();
180 final ImmutableNode personNode = new ImmutableNode.Builder(1).name(person).addAttribute(ATTR_AUTHOR, AUTHORS[author]).addChild(workNode)
181 .create();
182 rootBuilder.addChild(personNode);
183 }
184 }
185 }
186 return rootBuilder.create();
187 }
188
189 /**
190 * Creates a mock for a resolver.
191 *
192 * @return the resolver mock
193 */
194 @SuppressWarnings("unchecked")
195 public static NodeKeyResolver<ImmutableNode> createResolverMock() {
196 return mock(NodeKeyResolver.class);
197 }
198
199 /**
200 * Creates a tree with database table data with the following structure:
201 *
202 * tables table name fields field name field name
203 *
204 * @return the resulting nodes structure
205 */
206 private static ImmutableNode createTablesTree() {
207 return createTablesTree(TABLES, FIELDS);
208 }
209
210 /**
211 * Creates as tree with database table data based on the passed in arrays of table names and fields for tables. Works
212 * like the method without parameters, but allows defining the data of the structure.
213 *
214 * @param tables an array with the names of the tables
215 * @param fields an array with the fields of the single tables
216 * @return the resulting nodes structure
217 */
218 public static ImmutableNode createTablesTree(final String[] tables, final String[][] fields) {
219 final ImmutableNode.Builder bldTables = new ImmutableNode.Builder(tables.length);
220 bldTables.name("tables");
221 for (int i = 0; i < tables.length; i++) {
222 final ImmutableNode.Builder bldTable = new ImmutableNode.Builder(2);
223 bldTable.addChild(createNode("name", tables[i]));
224 final ImmutableNode.Builder bldFields = new ImmutableNode.Builder(fields[i].length);
225 bldFields.name("fields");
226
227 for (int j = 0; j < fields[i].length; j++) {
228 bldFields.addChild(createFieldNode(fields[i][j]));
229 }
230 bldTable.addChild(bldFields.create());
231 bldTables.addChild(bldTable.name("table").create());
232 }
233 return bldTables.create();
234 }
235
236 /**
237 * Returns the name of the specified field in the tables tree.
238 *
239 * @param tabIdx the index of the table
240 * @param fldIdx the index of the field
241 * @return the name of this field
242 */
243 public static String field(final int tabIdx, final int fldIdx) {
244 return FIELDS[tabIdx][fldIdx];
245 }
246
247 /**
248 * Returns the number of fields in the test table with the given index.
249 *
250 * @param tabIdx the index of the table
251 * @return the number of fields in this table
252 */
253 public static int fieldsLength(final int tabIdx) {
254 return FIELDS[tabIdx].length;
255 }
256
257 /**
258 * Helper method for evaluating a single component of a node key.
259 *
260 * @param parent the current parent node
261 * @param components the array with the components of the node key
262 * @param currentIdx the index of the current path component
263 * @return the found target node
264 * @throws NoSuchElementException if the desired node cannot be found
265 */
266 private static ImmutableNode findNode(final ImmutableNode parent, final String[] components, final int currentIdx) {
267 if (currentIdx >= components.length) {
268 return parent;
269 }
270
271 final Matcher m = PAT_KEY_WITH_INDEX.matcher(components[currentIdx]);
272 final String childName;
273 final int childIndex;
274 if (m.matches()) {
275 childName = m.group(1);
276 childIndex = Integer.parseInt(m.group(2));
277 } else {
278 childName = components[currentIdx];
279 childIndex = 0;
280 }
281
282 int foundIdx = 0;
283 for (final ImmutableNode node : parent) {
284 if (childName.equals(node.getNodeName()) && foundIdx++ == childIndex) {
285 return findNode(node, components, currentIdx + 1);
286 }
287 }
288 throw new NoSuchElementException("Cannot resolve child " + components[currentIdx]);
289 }
290
291 /**
292 * Returns a clone of the array with the table fields. This is useful if a slightly different tree structure should be
293 * created.
294 *
295 * @return the cloned field names
296 */
297 public static String[][] getClonedFields() {
298 final String[][] fieldNamesNew = new String[FIELDS.length][];
299 for (int i = 0; i < FIELDS.length; i++) {
300 fieldNamesNew[i] = FIELDS[i].clone();
301 }
302 return fieldNamesNew;
303 }
304
305 /**
306 * Returns a clone of the array with the table names. This is useful if a slightly different tree structure should be
307 * created.
308 *
309 * @return the cloned table names
310 */
311 public static String[] getClonedTables() {
312 return TABLES.clone();
313 }
314
315 /**
316 * Evaluates the given key and finds the corresponding child node of the specified root. Keys have the form
317 * {@code path/to/node}. If there are multiple sibling nodes with the same name, a numerical index can be specified in
318 * parenthesis.
319 *
320 * @param root the root node
321 * @param key the key to the desired node
322 * @return the node with this key
323 * @throws NoSuchElementException if the key cannot be resolved
324 */
325 public static ImmutableNode nodeForKey(final ImmutableNode root, final String key) {
326 final String[] components = key.split(PATH_SEPARATOR);
327 return findNode(root, components, 0);
328 }
329
330 /**
331 * Evaluates the given key and finds the corresponding child node of the root node of the specified model. This is a
332 * convenience method that works like the method with the same name, but obtains the root node from the given model.
333 *
334 * @param model the node model
335 * @param key the key to the desired node
336 * @return the found target node
337 * @throws NoSuchElementException if the desired node cannot be found
338 */
339 public static ImmutableNode nodeForKey(final InMemoryNodeModel model, final String key) {
340 return nodeForKey(model.getRootNode(), key);
341 }
342
343 /**
344 * Evaluates the given key and finds the corresponding child node of the root node of the specified {@code NodeHandler}
345 * object. This is a convenience method that works like the method with the same name, but obtains the root node from
346 * the given handler object.
347 *
348 * @param handler the {@code NodeHandler} object
349 * @param key the key to the desired node
350 * @return the found target node
351 * @throws NoSuchElementException if the desired node cannot be found
352 */
353 public static ImmutableNode nodeForKey(final NodeHandler<ImmutableNode> handler, final String key) {
354 return nodeForKey(handler.getRootNode(), key);
355 }
356
357 /**
358 * Convenience method for creating a path for accessing a node based on the node names.
359 *
360 * @param path an array with the expected node names on the path
361 * @return the resulting path as string
362 */
363 public static String nodePath(final String... path) {
364 return StringUtils.join(path, PATH_SEPARATOR);
365 }
366
367 /**
368 * Convenience method for creating a node path with a special end node.
369 *
370 * @param endNode the name of the last path component
371 * @param path an array with the expected node names on the path
372 * @return the resulting path as string
373 */
374 public static String nodePathWithEndNode(final String endNode, final String... path) {
375 return nodePath(path) + PATH_SEPARATOR + endNode;
376 }
377
378 /**
379 * Returns the name of a persona.
380 *
381 * @param authorIdx the author index
382 * @param workIdx the index of the work
383 * @param personaIdx the index of the persona
384 * @return the name of this persona
385 */
386 public static String persona(final int authorIdx, final int workIdx, final int personaIdx) {
387 return PERSONAE[authorIdx][workIdx][personaIdx];
388 }
389
390 /**
391 * Returns the number of personae in the given work of the specified author.
392 *
393 * @param authorIdx the author index
394 * @param workIdx the index of the work
395 * @return the number of personae in this work
396 */
397 public static int personaeLength(final int authorIdx, final int workIdx) {
398 return PERSONAE[authorIdx][workIdx].length;
399 }
400
401 /**
402 * Prepares the passed in resolver mock to resolve add keys. They are interpreted on a default expression engine.
403 *
404 * @param resolver the {@code NodeKeyResolver} mock
405 */
406 public static void prepareResolveAddKeys(final NodeKeyResolver<ImmutableNode> resolver) {
407 when(resolver.resolveAddKey(any(), any(), any())).then(invocation -> {
408 final ImmutableNode root = invocation.getArgument(0, ImmutableNode.class);
409 final String key = invocation.getArgument(1, String.class);
410 final TreeData handler = invocation.getArgument(2, TreeData.class);
411 return DefaultExpressionEngine.INSTANCE.prepareAdd(root, key, handler);
412 });
413 }
414
415 /**
416 * Prepares a mock for a resolver to expect arbitrary resolve operations. These operations are implemented on top of a
417 * default expression engine.
418 *
419 * @param resolver the mock resolver
420 */
421 @SuppressWarnings("unchecked")
422 public static void prepareResolveKeyForQueries(final NodeKeyResolver<ImmutableNode> resolver) {
423 when(resolver.resolveKey(any(), any(), any())).thenAnswer(invocation -> {
424 final ImmutableNode root = invocation.getArgument(0, ImmutableNode.class);
425 final String key = invocation.getArgument(1, String.class);
426 final NodeHandler<ImmutableNode> handler = invocation.getArgument(2, NodeHandler.class);
427 return DefaultExpressionEngine.INSTANCE.query(root, key, handler);
428 });
429 }
430
431 /**
432 * Returns the name of the test table with the given index.
433 *
434 * @param idx the index of the table
435 * @return the name of the test table with this index
436 */
437 public static String table(final int idx) {
438 return TABLES[idx];
439 }
440
441 /**
442 * Returns the number of tables in the tables tree.
443 *
444 * @return the number of tables
445 */
446 public static int tablesLength() {
447 return TABLES.length;
448 }
449
450 /**
451 * Returns the work of an author with a given index.
452 *
453 * @param authorIdx the author index
454 * @param idx the index of the work
455 * @return the desired work
456 */
457 public static String work(final int authorIdx, final int idx) {
458 return WORKS[authorIdx][idx];
459 }
460
461 /**
462 * Returns the number of works for the author with the given index.
463 *
464 * @param authorIdx the author index
465 * @return the number of works of this author
466 */
467 public static int worksLength(final int authorIdx) {
468 return WORKS[authorIdx].length;
469 }
470
471 private NodeStructureHelper() {
472 // empty
473 }
474 }