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

net.sf.eBus.feed.pattern.EOrderedPatternFeed Maven / Gradle / Ivy

The newest version!
//
// Copyright 2018, 2020 Charles W. Rapp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package net.sf.eBus.feed.pattern;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.function.BiPredicate;
import net.sf.eBus.client.EClient;
import net.sf.eBus.client.ESubscribeFeed;
import net.sf.eBus.feed.pattern.EventPattern.MultiPatternComponent;
import net.sf.eBus.feed.pattern.EventPattern.PatternComponent;
import net.sf.eBus.feed.pattern.EventPattern.SinglePatternComponent;
import net.sf.eBus.messages.ENotificationMessage;


/**
 * Connects to one or more {@link ENotifyFeed notification feeds}
 * and searches those feeds for a user defined pattern. When the
 * pattern is found, a {@link MatchEvent} is raised containing
 * the matched events.
 * 

* An ordered pattern is an event regular expression. Events * must arrive in the pattern's order to match the pattern. * Event regular expression are simple, limited to single or * simple event groups combined with quantifiers. Named capturing * groups are also supported. Kleene closure (zero or more * quantifier) is not supported. *

* * @author Charles W. Rapp */ /* package */ final class EOrderedPatternFeed extends EPatternFeed { //--------------------------------------------------------------- // Enums. // /** * Delineates the transtion type generated for an FSM. */ private enum TransitionType { /** * Inner loop-back transition checking that maximum match * count not exceeded. */ MAXIMUM, /** * Transition from the current state to the next state * used for the next pattern component. */ MINIMUM } // end of TransitionType //--------------------------------------------------------------- // Member data. // //----------------------------------------------------------- // Constants. // /** * The state machine start state identifier is {@value}. */ public static final int START_STATE_ID = 0; /** * User-defined states have identifiers ≥ {@value}. */ public static final int INITIAL_USER_STATE_ID = (START_STATE_ID + 1); /** * The state machine final state identifier is {@value}. This * state is reached when a pattern is successfully matched. * This value is < zero because it is not an index into * the transitions table. */ public static final int FINAL_STATE_ID = -1; /** * Default empty transition array. Used when event is not * defined for the given state. */ private static final Transition[] EMPTY_TRANSITIONS = new Transition[0]; /** * Use this transition when a parameter is not defined for * the current state. This transition has a guard which * always fails for any notification, match frame pair. */ private static final Transition NO_TRANSITION = new Transition( (t, u) -> false, FINAL_STATE_ID, new String[0]); //----------------------------------------------------------- // Locals. // /** * State machine transitions. The indices are: *
    *
  1. * state identifier, *
  2. *
  3. * event identifier, *
  4. *
  5. * and individual transitions. *
  6. *
* The reason for multiple transition is that it is possible * for a single event to have multiple ways out of the * current state. Which is why this is a * non-deterministic finite state machine. */ private Transition[][][] mNdFSM; /** * In-progress event matching frames. Contains the collected * events for each match group. This list will be replaced * each time a new event is received. *

* Note: this data member may not be moved up to * super class because {@code MatchFrame} is a * {@code EOrderedPatternFeed} inner class. *

*/ private Queue mMatchFrames; //--------------------------------------------------------------- // Member methods. // //----------------------------------------------------------- // Constructors. // /** * Creates a new ordered pattern feed for the given publish * feed subject and subscriber client. * @param builder contains ordered pattern feed settings. */ private EOrderedPatternFeed(final Builder builder) { super (builder); mMatchFrames = new LinkedList<>(); // Convert pattern into a NDFSM. setStateMachine(createFSM(builder.mPattern)); // Create the subcribe feeds based on the pattern // parameters. setSubscribeFeeds(builder.mPattern); } // end of EOrderedPatternFeed(Builder) // // end of Constructors. //----------------------------------------------------------- //----------------------------------------------------------- // Abstract Method Overrides. // /** * Applies the given event to the regular expression's * non-deterministic finite state machine based on the * in progress match frames. A new, empty match frame set to * the start state is added to the match frame queue in case * event is the start of a new pattern. * @param event event to be matched to the FSM. * @param eventId the event identifier used to look up the * transitions for the given event. */ @Override protected void matchEvent(final ENotificationMessage event, final int eventId) { // 1. Start with a new match frame list. final Queue frames = new LinkedList<>(); MatchFrame currFrame; if (sLogger.isTraceEnabled()) { sLogger.trace("{}: received event ID {}:\n{}", mPubKey, eventId, event); } else { sLogger.debug("{}: received {} event.", mPubKey, event.key()); } // 2. Create a new match frame which begins life in // the start state. This is done in case the event // is the start of the pattern. mMatchFrames.add( new MatchFrame(mPatternName, mIsExclusive)); // 3. For each existing match frame ... while (!mMatchFrames.isEmpty()) { // 4 ... take the next frame and apply the event to // the frame state. currFrame = mMatchFrames.poll(); // Ignore defunct match frames. if (currFrame.isDefunct()) { // no-op. } // 5. Is this match frame's duration still within the // event pattern "until" duration? else if (mUntil.test(currFrame.allEvents(), event)) { // Yes. Continue processing the match frame. matchEvent(event, eventId, currFrame, frames); } } // 10. Replace the current match frame queue with the // newly generated frame queue. mMatchFrames = frames; } // end of matchEvent(ENotificationMessage, int) // // end of Abstract Method Overrides. //----------------------------------------------------------- /** * Returns a new {@code Builder} instance used to construct * an order pattern feed. * @return new order pattern feed {@code Builder} instance. */ public static Builder builder() { return (new Builder()); } // end of builder() @SuppressWarnings({"java:S3776"}) private void matchEvent(final ENotificationMessage event, final int eventId, final MatchFrame mf, final Queue frames) { final int stateId = mf.currentState(); final Transition[] transitions = mNdFSM[stateId][eventId]; final int numTransitions = transitions.length; MatchFrame frame = mf; int i; int nextStateId; int matchCount; MatchEvent me; // Add one to the match frame count prior to // executing the transition condition. Match count // checks are dependent on this. mf.incrementCount(); if (sLogger.isTraceEnabled()) { sLogger.trace( "{}: {} event, # transitions={}\n{}\nframe={}", mPatternName, event.key(), numTransitions, event, mf); } else { sLogger.debug("{}: {} event, # transitions={}", mPatternName, event.key(), numTransitions); } // 6. For each transition ... for (i = 0; i < numTransitions; ++i) { // 7. ... check if the event satisifies the // transition guard condition. if (transitions[i].matches(event, frame)) { // 8. Yes, the event matches the transition // guard condition. Create a new match // frame based on the current match frame // and add the event to the new frame. nextStateId = transitions[i].nextState(); // If the transition moved to a new state, then // set the match count to zero. if (nextStateId != stateId) { matchCount = 0; } // Otherwise, use the current match count. else { matchCount = mf.matchCount(); } frame = new MatchFrame(nextStateId, matchCount, mf); frame.addEvent(event, transitions[i].groups()); // Is this an exclusive pattern? if (mIsExclusive) { // Yes. Need to map the event to this match // frame. addMapping(event, frame); } sLogger.debug("{}: new match frame: {}", mPubKey, frame); // Is the pattern completely matched? if (nextStateId == FINAL_STATE_ID) { me = frame.generateMatch(mPubKey.subject()); // 9. Yes. Do the matched events satisfy the // condition? if (mCondition.test(me)) { // 10. Yes. Post the match event. EClient.dispatch( new NotifyTask( me, NO_CONDITION, this, mNotifyCallback), mEClient.target()); // Is this an exclusive event pattern? if (mIsExclusive) { // Yes. Throw away all in progress // match frames and start from // scratch with the next event. markDefunct(frame.allEvents()); } } } // No, but the pattern match is still in // progress. Add the new frame to the new frame // queue. else { frames.add(frame); } } // The event failed the transition guard // condition. The pattern match failed. } } // end of matchEvent() /** * Creates the subscription feeds based on the * {@link EventPattern#parameters() event pattern parameters}. * As a side effect sets {@link #mAllSubFeedsMask}. * @param pattern create subscription feeds for this pattern. */ private void setSubscribeFeeds(final EventPattern pattern) { final Collection feeds = (pattern.parameters()).values(); ESubscribeFeed subFeed; int feedId; for (EventPattern.FeedInfo info : feeds) { subFeed = (ESubscribeFeed.builder()) .target(this) .messageKey(info.messageKey()) .scope(info.scope()) .condition(info.condition()) .statusCallback(this::onFeedStateUpdate) .notifyCallback(this::onEvent) .build(); feedId = subFeed.feedId(); mSubFeeds.add(subFeed); mAllSubFeedsMask |= (1L << feedId); } } // end of setSubscribeFeeds(EventPattern) /** * Translates the event pattern into a non-deterministic * finite state machine. * @param pattern event pattern. * @param feed generating FSM for this feed. * @return finite state machine table. */ private static Transition[][][] createFSM(final EventPattern pattern) { final Iterator pcIt = (pattern.components()).iterator(); final Map>> fsm = new HashMap<>(); final int numParams = (pattern.parameters()).size(); PatternComponent pc; int currState = START_STATE_ID; // while (pcIt.hasNext()) { pc = pcIt.next(); currState = createTransitions( currState, pc, initializeState(currState, numParams, fsm), !pcIt.hasNext()); } return (generateFSM(fsm, numParams)); } // end of createFSM(EventPattern) private static int createTransitions(final int currentState, final PatternComponent pc, final Map> events, final boolean isFinal) { final int minMatchCount = pc.minimumMatchCount(); final int maxMatchCount = pc.maximumMatchCount(); int nextState; // There are two transitions generated for each // pattern component: // 1. Internal loopback which waits for the maximum match // count is reached. This is done only if the maximum // match count is > 1. // 2. Transition to the next state when the minimum match // count is reach. // Maximum inner loopback. if (maxMatchCount > 1) { createTransition(TransitionType.MAXIMUM, currentState, minMatchCount, maxMatchCount, events, pc); } // Create a transition from the current state to the // next state. If this the final pattern component, then // the final state is next. Otherwise, add one to the // current state to get the next state. nextState = (isFinal ? FINAL_STATE_ID : (currentState + 1)); // When the minimum match count is next, move to the next // state. createTransition(TransitionType.MINIMUM, nextState, minMatchCount, maxMatchCount, events, pc); return (nextState); } // end of createTransitions(PatternComponent, ...) private static void createTransition(final TransitionType tType, final int nextState, final int minMatchCount, final int maxMatchCount, final Map> events, final PatternComponent pc) { if (pc instanceof SinglePatternComponent) { createTransition(tType, nextState, minMatchCount, maxMatchCount, events, (SinglePatternComponent) pc); } else { createTransition(tType, nextState, minMatchCount, maxMatchCount, events, (MultiPatternComponent) pc); } } // end of createTransition(...) private static void createTransition(final TransitionType tType, final int nextState, final int minMatchCount, final int maxMatchCount, final Map> events, final MultiPatternComponent mpc) { final SinglePatternComponent[] subs = mpc.components(); final int numSubs = subs.length; int subIndex; for (subIndex = 0; subIndex < numSubs; ++subIndex) { createTransition(tType, nextState, minMatchCount, maxMatchCount, events, subs[subIndex]); } } // end of createTransition(...) private static void createTransition(final TransitionType tType, final int nextState, final int minMatchCount, final int maxMatchCount, final Map> events, final SinglePatternComponent spc) { final int transId = spc.transitionIdentifier(); final List transitions = events.get(transId); final Transition transition; switch (tType) { case MAXIMUM: transition = new Transition( (e, g) -> (componentTest(e, g, spc.condition()) && g.matchCount() < maxMatchCount), nextState, spc.groupNames()); break; default: transition = new Transition( (e, g) -> (componentTest(e, g, spc.condition()) && g.matchCount() >= minMatchCount), nextState, spc.groupNames()); break; } transitions.add(transition); } // end of createTransition(TransitionType, ...) private static Map> initializeState(final int stateId, final int numTransitions, final Map>> fsm) { int ti; List transitions; final Map> retval = new HashMap<>(numTransitions); fsm.put(stateId, retval); // Fill in the transition list with "no transition". // This list will be filled in with real transitions // for those transitions defined in the state. for (ti = 0; ti < numTransitions; ++ti) { transitions = new ArrayList<>(); retval.put(ti, transitions); transitions.add(NO_TRANSITION); } return (retval); } // end of initializeState(int, int, Map) /** * Returns a three dimensional array based on the given * state list. The * @param fsm translate this map into a transition table. * @param numEvents number of events used in the pattern. * @return */ private static Transition[][][] generateFSM(final Map>> fsm, final int numEvents) { int si; Map> events; int ei; final Transition[][][] retval = new Transition[fsm.size()][][]; for (Map.Entry>> state : fsm.entrySet()) { si = state.getKey(); events = state.getValue(); retval[si] = new Transition[numEvents][]; for (ei = 0; ei < numEvents; ++ei) { // Is this event defined for the given state? if (!events.containsKey(ei)) { // No, the event is not defined. Set the // transition array to the default empty // array. retval[si][ei] = EMPTY_TRANSITIONS; } // Yes, the event is defined. Convert the // transitions list to an array. else { retval[si][ei] = (events.get(ei)).toArray( EMPTY_TRANSITIONS); } } } return (retval); } // end of generateFSM(Map<>, int) /** * Sets the event pattern finite state machine. * @param ndFsm non-deterministic finite state machine. */ private void setStateMachine(final Transition[][][] ndFsm) { mNdFSM = ndFsm; } // end of setStateMachine(Transition[][][]) //--------------------------------------------------------------- // Inner classes. // /** * Builder class used to construct an ordered pattern feed. * Used to set event pattern, feed status, and notify * callbacks. * * @see EOrderedPatternFeed#builder() */ public static final class Builder extends PatternBuilder { //----------------------------------------------------------- // Member data. // //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * {@code EOrderPatternFeed} instance builder. */ private Builder() { super (EOrderedPatternFeed.class); } // end of Builder() // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Abstract Method Implementations. // /** * Returns {@code this} reference. * @return {@code this} reference. */ @Override protected Builder self() { return (this); } // end of self() /** * Returns a new ordered pattern feed instance based on * this builder's settings. * @return new ordered pattern feed. */ @Override protected EOrderedPatternFeed buildImpl() { return (new EOrderedPatternFeed(this)); } // end of buildImpl() // // end of Abstract Method Implementations. //------------------------------------------------------- } // end of class Builder /** * Defines a state machine transition. A transition is taken * if-and-only-if the guard condition is satisfied by the * transition event. If so, the transition moves to the * specified next state and stores the event in the specified * match groups. */ private static final class Transition { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * This condition is taken if-and-only-if this guard * condition returns true for the given notification * message. This data member is never {@code null} but * is set to a default predicate which always returns * {@code true}. */ private final BiPredicate mCondition; /** * Transition to this state. This value is always > * zero because zero is the start state and it is not * possible to transition to the start state. */ private final int mNextState; /** * When the transition is taken, add the matched * notification event to these match groups. The set * size is at least one because the event is always added * to the "all events" group. */ private final String[] mGroups; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a new transition for the given guard, state, * and group identifiers. * @param guard transition guard condition. * @param nextState transition to this state. * @param groups match group names. */ private Transition(final BiPredicate guard, final int nextState, final String[] groups) { mCondition = guard; mNextState = nextState; mGroups = groups; } // end of Transition(Predicate<>, int, String[]) // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Get Methods. // /** * Returns {@code true} if {@code event} passes the * transition guard condition. * @param event inbound event. * @return {@code true} if the transition is successful. */ public boolean matches(final ENotificationMessage event, final MatchFrame frame) { return (mCondition.test(event, frame)); } // end of matches(ENotificationMessage, MatchFrame) /** * Returns the transition to state. * @return state identifier. */ public int nextState() { return (mNextState); } // end of nextState() /** * Returns the match group name. * @return match group names. */ private String[] groups() { return (mGroups); } // end of groups() // // end of Get Methods. //------------------------------------------------------- } // end of Transition /** * A match frame tracks an in progress event */ private static final class MatchFrame extends AbstractMatchFrame { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * The match frame is in this FSM state. */ private final int mStateId; /** * Match groups which contain the matched notification * events for each group. */ private final Map> mGroups; /** * Number of times a particular component has matched. * Put another way, this is the number of times the * match frame has remained in the same state. * This value is used when a component has a min, max * match count. */ private int mMatchCount; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a new match frame for the start state with an * empty match groups map. * @param patternName name of pattern creating this * match frame. * @param isExclusive flag denoting whether this is an * exclusive pattern or not. */ private MatchFrame(final String patternName, final boolean isExclusive) { super (patternName, isExclusive); mStateId = START_STATE_ID; mMatchCount = 0; mGroups = new HashMap<>(); mGroups.put(EventPattern.ALL_EVENTS, mAllEvents); } // end of MatchFrame(String, boolean) /** * Creates a match frame which makes a deep copy of the * given frame's contents. * @param stateId the current FSM state. * @param matchCount number of times match frame has been * in {@code stateId}. * @param startTime initial event timestamp. * @param isExclusive flag denoting whether this is an * exclusive pattern or not. * @param frame copy the match groups to this newly * created frame. */ private MatchFrame(final int stateId, final int matchCount, final MatchFrame frame) { super (frame); mStateId = stateId; mMatchCount = matchCount; mGroups = new HashMap<>(); // Copy in the match fram match groups - except // the all events group. String key; for (Map.Entry> entry : (frame.mGroups).entrySet()) { key = entry.getKey(); if (!EventPattern.ALL_EVENTS.equals(key)) { mGroups.put( key, new ArrayList<>(entry.getValue())); } } mGroups.put(EventPattern.ALL_EVENTS, mAllEvents); } // end of MatchFrame(int, int, MatchFrame) // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Abstract Method Overrides. // /** * Returns a copy of the group map wherein both the map * and the group lists are read-only. * @return read-only copy of the group map. */ @Override protected Map> groupMap() { final Map> retval = new HashMap<>(mGroups.size()); mGroups.entrySet() .forEach( entry -> retval.put(entry.getKey(), Collections.unmodifiableList( entry.getValue()))); return (Collections.unmodifiableMap(retval)); } // end of groupMap() // // end of Abstract Method Overrides. //------------------------------------------------------- //------------------------------------------------------- // Object Method Overrides. // /** * Returns text containing the match count and state * identifier. * @return match frame text. */ @Override public String toString() { return ( String.format( "%s%n[match count=%d, state=%d]", super.toString(), mMatchCount, mStateId)); } // end of toString() // // end of Object Method Overrides. //------------------------------------------------------- //------------------------------------------------------- // Get Methods. // /** * Returns the match frame FSM state. * @return state identifier. */ public int currentState() { return (mStateId); } // end of currentState() /** * Returns the frame match count. * @return match count. */ public int matchCount() { return (mMatchCount); } // end of matchCount() /** * Generates a match event based on this match frames * capture groups. * @param subject notification message subject. * @return match event. */ public MatchEvent generateMatch(final String subject) { // Convert the match group lists into read-only lists. mGroups.keySet() .forEach( group -> { mGroups.put( group, Collections.unmodifiableList( mGroups.get(group))); }); return ((MatchEvent.builder()).subject(subject) .groups(mGroups) .userCache(userCache()) .build()); } // end of generateMatch(String) // // end of Get Methods. //------------------------------------------------------- //------------------------------------------------------- // Set Methods. // /** * Increments the match count by one. */ public void incrementCount() { ++mMatchCount; } // end of incrementCount() /** * Adds the event to the specified match groups. * @param event event to be added. * @param groupIds match group identifiers. */ public void addEvent(final ENotificationMessage event, final String[] groups) { final int numGroups = groups.length; int i; for (i = 0; i < numGroups; ++i) { (getGroup(groups[i])).add(event); } } // end of addEvent(ENotificationMessage, String[]) // // end of Set Methods. //------------------------------------------------------- /** * Returns the list associated with the given group name. * If no such list exists, then creates the list, puts * that list into the groups map, and return that value. * @param groupName event group name. * @return event group list. */ private List getGroup(final String groupName) { final List retval; if (mGroups.containsKey(groupName)) { retval = mGroups.get(groupName); } else { retval = new ArrayList<>(); mGroups.put(groupName, retval); } return (retval); } // end of getGroup(String) } // end of MatchFrame } // end of class EOrderedPatternFeed




© 2015 - 2025 Weber Informatics LLC | Privacy Policy