
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:
*
* -
* state identifier,
*
* -
* event identifier,
*
* -
* and individual transitions.
*
*
* 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