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 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 }