All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.exsoinn.util.epf.Context Maven / Gradle / Ivy

The newest version!
package com.exsoinn.util.epf;

import java.util.*;
import java.util.stream.Collectors;

/**
 * This API is meant to make it easier to search disparate data formats (e.g. XML, JSON), by acting as a wrapper
 * around those data structures to provide a consistent API to query the data, whih allows decoupling an application
 * from the data format that it acts on.. The data structures that it wraps may be hierarchical in nature, in which case recursion
 * can be applied. However, such implementation details are left up to implementing classes to handle according to the
 * characteristics of the data structure they intend to support.

 * The main main motivation of this API is to keep client code decoupled from the details of the underlying format of the data,
 * be it JSON, XML and the like. The only coupling/contract is done via input arguments to the various methods provided by this
 * API.
 * The only pain point is that the input parameters must be properly configured before this method gets invoked,
 * but that's a small price to pay in comparison to the maintainability and re-usability that is achieved via
 * the use of this API. The input parameters to search a {@code Context} object can be compared to the
 * XPath syntax, which is used to navigate
 * the elements and attribute of an XML document. Only difference here is that this API is not married to any specific
 * format.
 *
 * All the operations provided by this interface are read-only, which essentially renders the {@link Context} object
 * immutable after its creation.
 *
 * Created by QuijadaJ on 5/3/2017.
 */
public interface Context {
    String FOUND_ELEM_VAL_IS_REGEX = "foundElemValIsRegex";
    String PARTIAL_REGEX_MATCH = "partialRegexMatch";
    String IGNORE_INCOMPATIBLE_SEARCH_PATH_PROVIDED_ERROR = "ignoreIncompatibleSearchPathProvidedError";
    String IGNORE_INCOMPATIBLE_TARGET_ELEMENT_PROVIDED_ERROR = "ignoreIncompatibleTargetElementProvidedError";

    /**
     * Represents the entry point to begin searching the underlying data structure. The search works by specifying a path
     * to the node of interest, represented by a {@link SearchPath}.
     * The {@link Context} object off of which this method can be invoked was previously obtained via a call to factory
     * method {@link ContextFactory#obtainContext(Object)}.
     * To further refine the results use the pFilter and pTargetElements arguments.
     * TODO: Add more examples of usage
     * @param pSearchPath - The path that in the underlying data structure that this method has been instructed to find. For more
     *                    information on how to build a {@code SearchPath} object to then pass it here, read the documentation
     *                    of {@link SearchPath#valueOf(String)}.
     *                    Basically the last node of the SearchPath is what gets returned to the caller. This can be
     *                    a primitive, and array (of primitives, or other complex structures, or a combination thereof), or a complex
     *                    data structure. The type of the found element determines how the pFilter and pTargetElements
     *                    arguments behave. Read respective description of these arguments for more details.
     *                    If an array element will be
     *                    encountered somewhere in the search path, then the corresponding node should contain square
     *                    brackets like this: someElemName[0]. If an array element is encountered yet the path
     *                    did not tell this method to expect an array at this point (by appending "[N]" to the
     *                    path node), IllegalArgumentException is thrown, *unless* the array element happens to be the
     *                    last node of the search path.
     * @param pFilter - Use this argument to further refine the search rules. Build a filter by specifying a semi-colon
     *                separated list of name/value pairs separated by equals sign, and pass that string to
     *                factory method {@link Filter#valueOf(String)}, like this:
     *
     *                Filter.valueOf("name1=val1;name2=val2")
     *
     *                The keys specified should correspond to keys found in the last node of the pSearchPath.
     *                This assumes that developer is intimately familiar with the data structure he's working with, and that
     *                he'll know exactly what result pSearchPath will yield.
     *
     *                How pFilter gets applied depends on the type of data found in the last node of the
     *                pSearchPath. In all cases, the members of the search results are checked to see if
     *                a corresponding key is found in the Filter specified, and if so that member value
     *                is compared against the filter value.
     *                The filter values can have wildcards too. There can be a wildcard at either the beginning, the end
     *                or both.
     *                Below lists the possible types of data that can be found at end of search path, and how the filter
     *                gets applied in each case:
     *
     *                primitive: simply compare the corresponding filter value against the primitive, and if there's a match
     *                  that primitive will be included in the search result
     *                array: Primitive entries are handled the same way single primitive results are found. Complex
     *                  structures in the array have their members checked against the filter the same way
     *                  single complex structures are handled as described below.
     *                complex structure: Each member of the complex structure is checked to see if there's a filter
     *                  value provided. If there's a match, then this complex structure will be included in the results.
     *
     *                A Filter key can be a search path also, in which case the value for the filter
     *                search path is found relative to the last node of the search path passed to this method. An error
     *                is thrown if the filter search path is not found, otherwise the node will be included in the
     *                search results if the filter value matches the value found in the underlying Context.
     *                An example of a filter that has a search path as key is:
     *
     *                SearchPath: top_node.inner_node.member2
     *                Filter: key=sub_member5.key=1234
     *                Context (assuming underlying data is in JSON format):
    {
    "top_node":{
    "inner_node":{
    "member1":1110,
    "member2":[
    {
    "sub_member_1":1110,
    "sub_member_2":15199,
    "sub_member_3":13135,
    "sub_member_4":7184441216,
    "sub_member5": {
    "key": "abcd"
    }
    },
    {
    "sub_member_1":1110,
    "sub_member_2":15199,
    "sub_member_3":24099,
    "sub_member_4":7184441216,
    "sub_member5": {
    "key": "1234"
    }
    }
    ],
    "member3":1073,
    "member4":7184441216,
    "member5":19555
    }
    }
    }

     *                  Based on the filter, the second entry of the "member2" array could be selected.
     *
     *                  TODO: Filter key values also support passing in a comma separated list of values
     *
     *
     *
     * @param pTargetElements - Use it to further refine the search results by specifying which elements to return in the search
     *                        results when the last node of the pSearchPath yields a complex data structure, and you're only
     *                        interested in a sub-set of the members of the found complex data structure.
     * @param pExtraParams - Implementing classes can use this {@link Map} to provide arbitrary list of name/value
     *                     pairs to provide features/behavior that this interface does not already plan for.
     *                     TODO: Clearly document *all* keys supported; they're defined as constants at top of this class
     * @return - A {@link SearchResult} which is nothing more than a {@link Map} that maps keys to {@link Context}
     *   objects. Each {@link Context} object in the {@link SearchResult} can be a primitive, or an array, or
     *   another complex object. Refer to the other methods of this class for the available operations.
     * @throws IllegalArgumentException - Thrown if parameter pSearchPath is determined to be invalid
     *   for whatever reason.
     */
    SearchResult findElement(SearchPath pSearchPath,
                             Filter pFilter,
                             TargetElements pTargetElements,
                             Map pExtraParams) throws IllegalArgumentException;


    /**
     * Works similar to {@link Context#findElement(SearchPath, Filter, TargetElements, Map)}, except that the first three
     * arguments are replaced with a {@link SelectionCriteria} element.
     * @param pSelectCriteria - pSelectCriteria
     * @param pExtraParams - pExtraParams
     * @return - TODO
     * @throws IllegalArgumentException - TODO
     */
    SearchResult findElement(SelectionCriteria pSelectCriteria,
                             Map pExtraParams) throws IllegalArgumentException;
    /**
     * Implementing classes use this method to tell if underlying data is a primitive (I.e. long, int, double,
     * {@link String}, etc...
     * @return - TODO
     */
    boolean isPrimitive();


    /**
     * Implementing classes use this method to tell if underlying data is complex (I.e. not a primitive).
     * @return - TODO
     */
    boolean isRecursible();

    /**
     * Implementing classes use this method to tell if underlying data is a type of array or list-like.
     * @return - TODO
     */
    boolean isArray();

    /**
     * Meant for use only {@link #isArray()} is true, will return a {@link List} of the underlying array-like
     * structure.
     * @return - TODO
     * @throws IllegalStateException - TODO
     */
    List asArray() throws IllegalStateException;


    /**
     * If the underlying data is array-like, will retrieve entry at index pIdx
     * @param pIdx - pIdx
     * @return - TODO
     * @throws IllegalStateException - TODO
     */
    Context entryFromArray(int pIdx) throws IllegalStateException;

    /**
     * If the underlying data provides implementation aside from the toString() to return the data as a string,
     * then this method is expected to wrap such an implmentation.
     * @return - TODO
     */
    String stringRepresentation();


    /**
     * To be used only when the underlying data is complex, returns true if underlying data contains the
     * pElemName given
     * @param pElemName - pElemName
     * @return - TODO
     * @throws IllegalStateException - TODO
     */
    boolean containsElement(String pElemName) throws IllegalStateException;



    /**
     * To be used only when the underlying data is complex, returns a {@link Set} of {@link Map.Entry}s
     * to represent the name/value pairs contained int he complex data structure.
     * @return - TODO
     * @throws IllegalStateException - TODO
     */
    Set> entrySet() throws IllegalStateException;


    /**
     * To be used only when the underlying data is complex, returns the value of the specified pMemberName
     * @param pMemberName - pMemberName
     * @return - TODO
     * @throws IllegalStateException - TODO
     */
    Context memberValue(String pMemberName) throws IllegalStateException;


    /**
     * When the {@code Context} is an array, checks if the value is contained in it.
     *
     * @param pVal - pVal
     * @return - TODO
     * @throws IllegalStateException - TODO
     */
    boolean arrayContains(String pVal) throws IllegalStateException;

    /**
     * Gives the starting search path of this {@link Context}. This works only when the data is recursible
     * ({@link Context#isRecursible()} yields true), and there's only one outer most element, else
     * this method returns null. This was added mainly as a convenience so that calling code does not need to be
     * calculating this value over and over again. There's no reason the {@link Context} object itself can't provide
     * this information.
     * @return - The starting {@link SearchPath} if there's just a single outermost element, null
     *   otherwise.
     */
    SearchPath startSearchPath();


    /**
     * This method is applicable for {@link Context}'s that return true when {@link Context#isRecursible()}
     * gets invoked.
     *
     * @return - A list of what the top level field names are for this recursible {@link Context}. If this
     * {@link Context} is not recursible, then null is returned.
     */
    List topLevelElementNames();


    /**
     * Utility method which attempts to transform passed in argument to a {@link List}. Argument must be a {@code String}
     * object or a sub-type, and must be a comma-separated list of values, or a value that
     * {@link ContextFactory#obtainContext(Object)} can transform to an array of primitives (for example a string
     * like '[a,b,c,d]' is recognized as JSON, hence can be transformed to Context by {@link ContextFactory#obtainContext(Object)}
     * because JSON is one the formats recognized by that factory).
     *
     * @param pArg - What will get transformed to a List of String's, if possible.
     * @param  - T
     * @return - The {@code List} of {@code String}'s produced, if possible, null otherwise
     */
    static  List transformArgumentToListObject(T pArg) {
        boolean errorParsingCtx = true;
        try {
            Context ctx;
            if (null != (ctx = ContextFactory.obtainContext(pArg)) && ctx.isArray()) {
                List ctxAry = ctx.asArray();
                // All the Context objects in the List must be of type primitive.
                if (ctxAry.parallelStream().filter(e -> !e.isPrimitive()).findAny().isPresent()) {
                    throw new IllegalArgumentException("The produced list did not contain all primitive values, please check: "
                            + pArg);
                }
                errorParsingCtx = false;
                return (List) ctxAry.stream().map(e -> e.stringRepresentation()).
                        collect(Collectors.toCollection(() -> new ArrayList<>()));
            }
        } catch (Exception ignore) {
            // Ignore
        } finally {
            if (errorParsingCtx) {
                /**
                 * One last ditch attempt at converting argument to a list
                 */
                if (pArg.indexOf(",") > 0) {
                    return (List) Arrays.stream(pArg.split(",")).collect(Collectors.toCollection(() -> new ArrayList<>()));
                }
            }
        }
        return null;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy