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