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

software.amazon.event.ruler.Finder Maven / Gradle / Ivy

package software.amazon.event.ruler;

import javax.annotation.concurrent.ThreadSafe;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static software.amazon.event.ruler.SetOperations.intersection;

/*
 * Notes on the implementation:
 *
 * This is finite-automaton based. The state machine matches field names exactly using a 
 *  map in the NameState class.  The value matching uses the "ByteMachine" class which, as its name suggests,
 *  runs a different kind of state machine over the bytes in the String-valued fields.
 *
 * Trying to make moveTo() faster by keeping unused Step structs around turns out to make it
 *  run slower. Let Java manage its memory.
 *
 * The Step processing could be done in parallel rather than sequentially; the use of a Queue for
 *  Steps has this in mind.  This could be done by having multiple threads reading
 *  from the task queue.  But it might be more straightforward to run multiple Finders in parallel.
 *  In which case you might want to share the machine; perhaps either static or a singleton.
 *
 * In this class and the other internal classes, we use rule names of type Object while in GenericMachine the rule name
 *  is of generic type T. This is safe as we only operate rule names of the same type T provided by a user. We use a
 *  generic type for rule names in GenericMachine for convenience and to avoid explicit type casts.
 */

/**
 *  Uses a state machine created by software.amazon.event.ruler.Machine to process tokens
 *   representing key-value pairs in an event, and return any matching Rules.
 */
@ThreadSafe
class Finder {

    private static final Patterns ABSENCE_PATTERN = Patterns.absencePatterns();

    private Finder() { }

    /**
     * Return any rules that match the fields in the event.
     *
     * @param event the fields are those from the JSON expression of the event, sorted by key.
     * @param machine the compiled state machine
     * @param subRuleContextGenerator the sub-rule context generator
     * @return list of rule names that match. The list may be empty but never null.
     */
    static List rulesForEvent(final String[] event, final GenericMachine machine,
                                      final SubRuleContext.Generator subRuleContextGenerator) {
        return find(new Task(event, machine), subRuleContextGenerator);
    }

    /**
     * Return any rules that match the fields in the event.
     *
     * @param event the fields are those from the JSON expression of the event, sorted by key.
     * @param machine the compiled state machine
     * @param subRuleContextGenerator the sub-rule context generator
     * @return list of rule names that match. The list may be empty but never null.
     */
    static List rulesForEvent(final List event, final GenericMachine machine,
                                      final SubRuleContext.Generator subRuleContextGenerator) {
        return find(new Task(event, machine), subRuleContextGenerator);
    }

    private static List find(final Task task, final SubRuleContext.Generator subRuleContextGenerator) {

        // bootstrap the machine: Start state, first token
        NameState startState = task.startState();
        if (startState == null) {
            return Collections.emptyList();
        }
        moveFrom(null, startState, 0, task, subRuleContextGenerator);

        // each iteration removes a Step and adds zero or more new ones
        while (task.stepsRemain()) {
            tryStep(task, subRuleContextGenerator);
        }

        return task.getMatchedRules();
    }

    // Move from a state.  Give all the remaining tokens a chance to transition from it
    private static void moveFrom(final Set candidateSubRuleIdsForNextStep, final NameState nameState,
                                 final int tokenIndex, final Task task,
                                 final SubRuleContext.Generator subRuleContextGenerator) {
        /*
         * The Name Matchers look for an [ { exists: false } ] match. They
         * will match if a particular key is not present
         * in the event. Hence, if the name state has any matches configured
         * for the [ { exists: false } ] case, we need to evaluate these
         * matches regardless. The fields in the event can be completely
         * disconnected from the fields configured for [ { exists: false } ],
         * and it does not matter if the current field is used in machine.
         *
         * Another possibility is that there can be a final state configured for
         * [ { exists: false } ] match. This state needs to be evaluated for a match
         * even if we have matched all the keys in the event. This is needed because
         * the final state can still be evaluated to true if the particular event
         * does not have the key configured for [ { exists: false } ].
         */
        tryNameMatching(candidateSubRuleIdsForNextStep, nameState, task, tokenIndex, subRuleContextGenerator);

        // Add more steps using our new set of candidate sub-rules.
        for (int i = tokenIndex; i < task.event.length; i += 2) {
            if (task.isFieldUsed(task.event[i])) {
                task.addStep(new Step(i, nameState, candidateSubRuleIdsForNextStep));
            }
        }
    }

    private static void moveFromWithPriorCandidates(final Set candidateSubRuleIds,
                                                    final NameState fromState, final Patterns fromPattern,
                                                    final int tokenIndex, final Task task,
                                                    final SubRuleContext.Generator subRuleContextGenerator) {
        Set candidateSubRuleIdsForNextStep = calculateCandidateSubRuleIdsForNextStep(candidateSubRuleIds,
                fromState, fromPattern);

        // If there are no more candidate sub-rules, there is no need to proceed further.
        if (candidateSubRuleIdsForNextStep != null && !candidateSubRuleIdsForNextStep.isEmpty()) {
            moveFrom(candidateSubRuleIdsForNextStep, fromState, tokenIndex, task, subRuleContextGenerator);
        }

    }

    /**
     * Calculate the candidate sub-rule IDs for the next step.
     *
     * @param currentCandidateSubRuleIds The candidate sub-rule IDs for the current step. Use null to indicate that we
     *                                   are on first step and so there are not yet any candidate sub-rules.
     * @param fromState The NameState we are transitioning from.
     * @param fromPattern The pattern we used to transition from fromState.
     * @return The set of candidate sub-rule IDs for the next step. Null means there are no candidates and thus, there
     *         is no point to evaluating subsequent steps.
     */
    private static Set calculateCandidateSubRuleIdsForNextStep(final Set currentCandidateSubRuleIds,
                                                                       final NameState fromState,
                                                                       final Patterns fromPattern) {
        // These are all the sub-rules that use the matched pattern to transition to the next NameState. Note that they
        // are not all candidates as they may have required different values for previously evaluated fields.
        Set subRuleIds = fromState.getNonTerminalSubRuleIdsForPattern(fromPattern);

        // If no sub-rules used the matched pattern to transition to the next NameState, then there are no matches to be
        // found by going further.
        if (subRuleIds == null) {
            return null;
        }

        // If there are no candidate sub-rules, this means we are on the first NameState and must initialize the
        // candidate sub-rules to those that used the matched pattern to transition to the next NameState.
        if (currentCandidateSubRuleIds == null || currentCandidateSubRuleIds.isEmpty()) {
            return subRuleIds;
        }

        // There are candidate sub-rules, so retain only those that used the matched pattern to transition to the next
        // NameState.
        Set candidateSubRuleIdsForNextStep = new HashSet<>();
        intersection(subRuleIds, currentCandidateSubRuleIds, candidateSubRuleIdsForNextStep);
        return candidateSubRuleIdsForNextStep;
    }

    // remove a step from the work queue and see if there's a transition
    private static void tryStep(final Task task, final SubRuleContext.Generator subRuleContextGenerator) {
        final Step step = task.nextStep();

        tryValueMatching(task, step, subRuleContextGenerator);
    }

    private static void tryValueMatching(final Task task, final Step step,
                                         final SubRuleContext.Generator subRuleContextGenerator) {
        if (step.keyIndex >= task.event.length) {
            return;
        }

        String value = task.event[step.keyIndex];

        if (!task.isFieldUsed(value)) {
            return;
        }

        // if there are some possible value pattern matches for this key
        final ByteMachine valueMatcher = step.nameState.getTransitionOn(value);
        if (valueMatcher != null) {
            final int nextKeyIndex = step.keyIndex + 2;

            // loop through the value pattern matches
            for (NameStateWithPattern nextNameStateWithPattern : valueMatcher.transitionOn(task.event[step.keyIndex + 1])) {
                addNameState(step.candidateSubRuleIds, nextNameStateWithPattern.getNameState(),
                        nextNameStateWithPattern.getPattern(), task, nextKeyIndex, subRuleContextGenerator);
            }
        }
    }

    private static void tryNameMatching(final Set candidateSubRuleIds, final NameState nameState,
                                        final Task task, final int keyIndex,
                                        final SubRuleContext.Generator subRuleContextGenerator) {
        if (!nameState.hasKeyTransitions()) {
            return;
        }

        for (NameState nextNameState : nameState.getNameTransitions(task.event)) {
            if (nextNameState != null) {
                addNameState(candidateSubRuleIds, nextNameState, ABSENCE_PATTERN, task, keyIndex,
                        subRuleContextGenerator);
            }
        }
    }

    private static void addNameState(Set candidateSubRuleIds, NameState nameState, Patterns pattern, Task task,
                                     int nextKeyIndex, final SubRuleContext.Generator subRuleContextGenerator) {
        // one of the matches might imply a rule match
        task.collectRules(candidateSubRuleIds, nameState, pattern, subRuleContextGenerator);

        moveFromWithPriorCandidates(candidateSubRuleIds, nameState, pattern, nextKeyIndex, task,
                subRuleContextGenerator);
    }
}