View Javadoc
1   package com.exsoinn.util.epf;
2   
3   import java.util.*;
4   import java.util.stream.Collectors;
5   
6   /**
7    * This API is meant to make it easier to search disparate data formats (e.g. XML, JSON), by acting as a wrapper
8    * around those data structures to provide a consistent API to query the data, whih allows decoupling an application
9    * from the data format that it acts on.. The data structures that it wraps may be hierarchical in nature, in which case recursion
10   * can be applied. However, such implementation details are left up to implementing classes to handle according to the
11   * characteristics of the data structure they intend to support.
12  
13   * The main main motivation of this API is to keep client code decoupled from the details of the underlying format of the data,
14   * be it JSON, XML and the like. The only coupling/contract is done via input arguments to the various methods provided by this
15   * API.
16   * The only pain point is that the input parameters must be properly configured before this method gets invoked,
17   * but that's a small price to pay in comparison to the maintainability and re-usability that is achieved via
18   * the use of this API. The input parameters to search a {@code Context} object can be compared to the
19   * <a href="https://www.w3schools.com/xml/xml_xpath.asp">XPath</a> syntax, which is used to navigate
20   * the elements and attribute of an XML document. Only difference here is that this API is not married to any specific
21   * format.
22   *
23   * All the operations provided by this interface are read-only, which essentially renders the {@link Context} object
24   * immutable after its creation.
25   *
26   * Created by QuijadaJ on 5/3/2017.
27   */
28  public interface Context {
29      String FOUND_ELEM_VAL_IS_REGEX = "foundElemValIsRegex";
30      String PARTIAL_REGEX_MATCH = "partialRegexMatch";
31      String IGNORE_INCOMPATIBLE_SEARCH_PATH_PROVIDED_ERROR = "ignoreIncompatibleSearchPathProvidedError";
32      String IGNORE_INCOMPATIBLE_TARGET_ELEMENT_PROVIDED_ERROR = "ignoreIncompatibleTargetElementProvidedError";
33  
34      /**
35       * Represents the entry point to begin searching the underlying data structure. The search works by specifying a path
36       * to the node of interest, represented by a {@link SearchPath}.
37       * The {@link Context} object off of which this method can be invoked was previously obtained via a call to factory
38       * method {@link ContextFactory#obtainContext(Object)}.
39       * To further refine the results use the pFilter and pTargetElements arguments.
40       * TODO: Add more examples of usage
41       * @param pSearchPath - The path that in the underlying data structure that this method has been instructed to find. For more
42       *                    information on how to build a {@code SearchPath} object to then pass it here, read the documentation
43       *                    of {@link SearchPath#valueOf(String)}.
44       *                    Basically the last node of the <code>SearchPath</code> is what gets returned to the caller. This can be
45       *                    a primitive, and array (of primitives, or other complex structures, or a combination thereof), or a complex
46       *                    data structure. The type of the found element determines how the pFilter and pTargetElements
47       *                    arguments behave. Read respective description of these arguments for more details.
48       *                    If an array element will be
49       *                    encountered somewhere in the search path, then the corresponding node should contain square
50       *                    brackets like this: someElemName[0]. If an array element is encountered yet the path
51       *                    did not tell this method to expect an array at this point (by appending "[N]" to the
52       *                    path node), IllegalArgumentException is thrown, *unless* the array element happens to be the
53       *                    last node of the search path.
54       * @param pFilter - Use this argument to further refine the search rules. Build a filter by specifying a semi-colon
55       *                separated list of name/value pairs separated by equals sign, and pass that string to
56       *                factory method {@link Filter#valueOf(String)}, like this:
57       *
58       *                <code>Filter.valueOf("name1=val1;name2=val2")</code>
59       *
60       *                The keys specified should correspond to keys found in the last node of the <code>pSearchPath</code>.
61       *                This assumes that developer is intimately familiar with the data structure he's working with, and that
62       *                he'll know exactly what result <code>pSearchPath</code> will yield.
63       *
64       *                How <code>pFilter</code> gets applied depends on the type of data found in the last node of the
65       *                <code>pSearchPath</code>. In all cases, the members of the search results are checked to see if
66       *                a corresponding key is found in the <code>Filter</code> specified, and if so that member value
67       *                is compared against the filter value.
68       *                The filter values can have wildcards too. There can be a wildcard at either the beginning, the end
69       *                or both.
70       *                Below lists the possible types of data that can be found at end of search path, and how the filter
71       *                gets applied in each case:
72       *
73       *                primitive: simply compare the corresponding filter value against the primitive, and if there's a match
74       *                  that primitive will be included in the search result
75       *                array: Primitive entries are handled the same way single primitive results are found. Complex
76       *                  structures in the array have their members checked against the filter the same way
77       *                  single complex structures are handled as described below.
78       *                complex structure: Each member of the complex structure is checked to see if there's a filter
79       *                  value provided. If there's a match, then this complex structure will be included in the results.
80       *
81       *                A <code>Filter</code> key can be a search path also, in which case the value for the filter
82       *                search path is found relative to the last node of the search path passed to this method. An error
83       *                is thrown if the filter search path is not found, otherwise the node will be included in the
84       *                search results if the filter value matches the value found in the underlying <code>Context</code>.
85       *                An example of a filter that has a search path as key is:
86       *
87       *                SearchPath: top_node.inner_node.member2
88       *                Filter: key=sub_member5.key=1234
89       *                Context (assuming underlying data is in JSON format):
90      {
91      "top_node":{
92      "inner_node":{
93      "member1":1110,
94      "member2":[
95      {
96      "sub_member_1":1110,
97      "sub_member_2":15199,
98      "sub_member_3":13135,
99      "sub_member_4":7184441216,
100     "sub_member5": {
101     "key": "abcd"
102     }
103     },
104     {
105     "sub_member_1":1110,
106     "sub_member_2":15199,
107     "sub_member_3":24099,
108     "sub_member_4":7184441216,
109     "sub_member5": {
110     "key": "1234"
111     }
112     }
113     ],
114     "member3":1073,
115     "member4":7184441216,
116     "member5":19555
117     }
118     }
119     }
120 
121      *                  Based on the filter, the second entry of the "member2" array could be selected.
122      *
123      *                  TODO: Filter key values also support passing in a comma separated list of values
124      *
125      *
126      *
127      * @param pTargetElements - Use it to further refine the search results by specifying which elements to return in the search
128      *                        results when the last node of the pSearchPath yields a complex data structure, and you're only
129      *                        interested in a sub-set of the members of the found complex data structure.
130      * @param pExtraParams - Implementing classes can use this {@link Map} to provide arbitrary list of name/value
131      *                     pairs to provide features/behavior that this interface does not already plan for.
132      *                     TODO: Clearly document *all* keys supported; they're defined as constants at top of this class
133      * @return - A {@link SearchResult} which is nothing more than a {@link Map} that maps keys to {@link Context}
134      *   objects. Each {@link Context} object in the {@link SearchResult} can be a primitive, or an array, or
135      *   another complex object. Refer to the other methods of this class for the available operations.
136      * @throws IllegalArgumentException - Thrown if parameter <code>pSearchPath</code> is determined to be invalid
137      *   for whatever reason.
138      */
139     SearchResult findElement(SearchPath pSearchPath,
140                              Filter pFilter,
141                              TargetElements pTargetElements,
142                              Map<String, String> pExtraParams) throws IllegalArgumentException;
143 
144 
145     /**
146      * Works similar to {@link Context#findElement(SearchPath, Filter, TargetElements, Map)}, except that the first three
147      * arguments are replaced with a {@link SelectionCriteria} element.
148      * @param pSelectCriteria - pSelectCriteria
149      * @param pExtraParams - pExtraParams
150      * @return - TODO
151      * @throws IllegalArgumentException - TODO
152      */
153     SearchResult findElement(SelectionCriteria pSelectCriteria,
154                              Map<String, String> pExtraParams) throws IllegalArgumentException;
155     /**
156      * Implementing classes use this method to tell if underlying data is a primitive (I.e. long, int, double,
157      * {@link String}, etc...
158      * @return - TODO
159      */
160     boolean isPrimitive();
161 
162 
163     /**
164      * Implementing classes use this method to tell if underlying data is complex (I.e. not a primitive).
165      * @return - TODO
166      */
167     boolean isRecursible();
168 
169     /**
170      * Implementing classes use this method to tell if underlying data is a type of array or list-like.
171      * @return - TODO
172      */
173     boolean isArray();
174 
175     /**
176      * Meant for use only {@link #isArray()} is true, will return a {@link List} of the underlying array-like
177      * structure.
178      * @return - TODO
179      * @throws IllegalStateException - TODO
180      */
181     List<Context> asArray() throws IllegalStateException;
182 
183 
184     /**
185      * If the underlying data is array-like, will retrieve entry at index <code>pIdx</code>
186      * @param pIdx - pIdx
187      * @return - TODO
188      * @throws IllegalStateException - TODO
189      */
190     Context entryFromArray(int pIdx) throws IllegalStateException;
191 
192     /**
193      * If the underlying data provides implementation aside from the toString() to return the data as a string,
194      * then this method is expected to wrap such an implmentation.
195      * @return - TODO
196      */
197     String stringRepresentation();
198 
199 
200     /**
201      * To be used only when the underlying data is complex, returns true if underlying data contains the
202      * <code>pElemName</code> given
203      * @param pElemName - pElemName
204      * @return - TODO
205      * @throws IllegalStateException - TODO
206      */
207     boolean containsElement(String pElemName) throws IllegalStateException;
208 
209 
210 
211     /**
212      * To be used only when the underlying data is complex, returns a {@link Set} of {@link Map.Entry}s
213      * to represent the name/value pairs contained int he complex data structure.
214      * @return - TODO
215      * @throws IllegalStateException - TODO
216      */
217     Set<Map.Entry<String, Context>> entrySet() throws IllegalStateException;
218 
219 
220     /**
221      * To be used only when the underlying data is complex, returns the value of the specified <code>pMemberName</code>
222      * @param pMemberName - pMemberName
223      * @return - TODO
224      * @throws IllegalStateException - TODO
225      */
226     Context memberValue(String pMemberName) throws IllegalStateException;
227 
228 
229     /**
230      * When the {@code Context} is an array, checks if the value is contained in it.
231      *
232      * @param pVal - pVal
233      * @return - TODO
234      * @throws IllegalStateException - TODO
235      */
236     boolean arrayContains(String pVal) throws IllegalStateException;
237 
238     /**
239      * Gives the starting search path of this {@link Context}. This works only when the data is recursible
240      * ({@link Context#isRecursible()} yields <code>true</code>), and there's only one outer most element, else
241      * this method returns null. This was added mainly as a convenience so that calling code does not need to be
242      * calculating this value over and over again. There's no reason the {@link Context} object itself can't provide
243      * this information.
244      * @return - The starting {@link SearchPath} if there's just a single outermost element, <code>null</code>
245      *   otherwise.
246      */
247     SearchPath startSearchPath();
248 
249 
250     /**
251      * This method is applicable for {@link Context}'s that return <code>true</code> when {@link Context#isRecursible()}
252      * gets invoked.
253      *
254      * @return - A list of what the top level field names are for this recursible {@link Context}. If this
255      * {@link Context} is not recursible, then <code>null</code> is returned.
256      */
257     List<String> topLevelElementNames();
258 
259 
260     /**
261      * Utility method which attempts to transform passed in argument to a {@link List}. Argument must be a {@code String}
262      * object or a sub-type, and must be a comma-separated list of values, or a value that
263      * {@link ContextFactory#obtainContext(Object)} can transform to an array of primitives (for example a string
264      * like '[a,b,c,d]' is recognized as JSON, hence can be transformed to Context by {@link ContextFactory#obtainContext(Object)}
265      * because JSON is one the formats recognized by that factory).
266      *
267      * @param pArg - What will get transformed to a <code>List</code> of <code>String</code>'s, if possible.
268      * @param <T> - T
269      * @return - The {@code List} of {@code String}'s produced, if possible, <code>null</code> otherwise
270      */
271     static <T extends String> List<T> transformArgumentToListObject(T pArg) {
272         boolean errorParsingCtx = true;
273         try {
274             Context ctx;
275             if (null != (ctx = ContextFactory.obtainContext(pArg)) && ctx.isArray()) {
276                 List<Context> ctxAry = ctx.asArray();
277                 // All the Context objects in the List must be of type primitive.
278                 if (ctxAry.parallelStream().filter(e -> !e.isPrimitive()).findAny().isPresent()) {
279                     throw new IllegalArgumentException("The produced list did not contain all primitive values, please check: "
280                             + pArg);
281                 }
282                 errorParsingCtx = false;
283                 return ctxAry.stream().map(e -> e.stringRepresentation()).
284                         collect(Collectors.toCollection(() -> new ArrayList()));
285             }
286         } catch (Exception ignore) {
287             // Ignore
288         } finally {
289             if (errorParsingCtx) {
290                 /**
291                  * One last ditch attempt at converting argument to a list
292                  */
293                 if (pArg.indexOf(",") > 0) {
294                     return Arrays.stream(pArg.split(",")).collect(Collectors.toCollection(() -> new ArrayList()));
295                 }
296             }
297         }
298         return null;
299     }
300 }