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 }