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 }