/************************************************************************************** * Copyright (C) 2008 EsperTech, Inc. All rights reserved. * * http://esper.codehaus.org * * http://www.espertech.com * * ---------------------------------------------------------------------------------- * * The software in this package is published under the terms of the GPL license * * a copy of which has been included with this distribution in the license.txt file. * **************************************************************************************/ package com.espertech.esper.view.std; import com.espertech.esper.client.EPException; import com.espertech.esper.client.EventBean; import com.espertech.esper.client.EventType; import com.espertech.esper.collection.MultiKeyUntyped; import com.espertech.esper.collection.Pair; import com.espertech.esper.core.context.util.AgentInstanceViewFactoryChainContext; import com.espertech.esper.epl.expression.ExprEvaluator; import com.espertech.esper.epl.expression.ExprNode; import com.espertech.esper.epl.expression.ExprNodeUtility; import com.espertech.esper.event.EventBeanUtility; import com.espertech.esper.view.CloneableView; import com.espertech.esper.view.View; import com.espertech.esper.view.ViewSupport; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.util.*; /** * The group view splits the data in a stream to multiple subviews, based on a key index. * The key is one or more fields in the stream. Any view that follows the GROUP view will be executed * separately on each subview, one per unique key. * * The view takes a single parameter which is the field name returning the key value to group. * * This view can, for example, be used to calculate the average price per symbol for a list of symbols. * * The view treats its child views and their child views as prototypes. It dynamically instantiates copies * of each child view and their child views, and the child view's child views as so on. When there are * no more child views or the special merge view is encountered, it ends. The view installs a special merge * view unto each leaf child view that merges the value key that was grouped by back into the stream * using the group-by field name. */ public class GroupByViewImpl extends ViewSupport implements CloneableView, GroupByView { private final ExprNode[] criteriaExpressions; private final ExprEvaluator[] criteriaEvaluators; protected final AgentInstanceViewFactoryChainContext agentInstanceContext; private EventBean[] eventsPerStream = new EventBean[1]; protected String[] propertyNames; protected final Map<Object, Object> subViewsPerKey = new HashMap<Object, Object>(); private final HashMap<Object, Pair<Object, Object>> groupedEvents = new HashMap<Object, Pair<Object, Object>>(); /** * Constructor. * @param criteriaExpressions is the fields from which to pull the values to group by * @param agentInstanceContext contains required view services */ public GroupByViewImpl(AgentInstanceViewFactoryChainContext agentInstanceContext, ExprNode[] criteriaExpressions, ExprEvaluator[] criteriaEvaluators) { this.agentInstanceContext = agentInstanceContext; this.criteriaExpressions = criteriaExpressions; this.criteriaEvaluators = criteriaEvaluators; propertyNames = new String[criteriaExpressions.length]; for (int i = 0; i < criteriaExpressions.length; i++) { propertyNames[i] = criteriaExpressions[i].toExpressionString(); } } public View cloneView() { return new GroupByViewImpl(agentInstanceContext, criteriaExpressions, criteriaEvaluators); } /** * Returns the field name that provides the key valie by which to group by. * @return field name providing group-by key. */ public ExprNode[] getCriteriaExpressions() { return criteriaExpressions; } public final EventType getEventType() { // The schema is the parent view's schema return parent.getEventType(); } public final void update(EventBean[] newData, EventBean[] oldData) { // Algorithm for single new event if ((newData != null) && (oldData == null) && (newData.length == 1)) { EventBean theEvent = newData[0]; EventBean[] newDataToPost = new EventBean[] {theEvent}; Object groupByValuesKey = getGroupKey(theEvent); // Get child views that belong to this group-by value combination Object subViews = this.subViewsPerKey.get(groupByValuesKey); // If this is a new group-by value, the list of subviews is null and we need to make clone sub-views if (subViews == null) { subViews = makeSubViews(this, propertyNames, groupByValuesKey, agentInstanceContext); subViewsPerKey.put(groupByValuesKey, subViews); } updateChildViews(subViews, newDataToPost, null); } else { // Algorithm for dispatching multiple events if (newData != null) { for (EventBean newValue : newData) { handleEvent(newValue, true); } } if (oldData != null) { for (EventBean oldValue : oldData) { handleEvent(oldValue, false); } } // Update child views for (Map.Entry<Object, Pair<Object, Object>> entry : groupedEvents.entrySet()) { EventBean[] newEvents = convertToArray(entry.getValue().getFirst()); EventBean[] oldEvents = convertToArray(entry.getValue().getSecond()); updateChildViews(entry.getKey(), newEvents, oldEvents); } groupedEvents.clear(); } } public final Iterator<EventBean> iterator() { throw new UnsupportedOperationException("Cannot iterate over group view, this operation is not supported"); } public final String toString() { return this.getClass().getName() + " groupFieldNames=" + Arrays.toString(criteriaExpressions); } /** * Instantiate subviews for the given group view and the given key value to group-by. * Makes shallow copies of each child view and its subviews up to the merge point. * Sets up merge data views for merging the group-by key value back in. * @param groupView is the parent view for which to copy subviews for * @param groupByValues is the key values to group-by * @param agentInstanceContext is the view services that sub-views may need * @param propertyNames names of expressions or properties * @return a single view or a list of views that are copies of the original list, with copied children, with * data merge views added to the copied child leaf views. */ public static Object makeSubViews(GroupByView groupView, String[] propertyNames, Object groupByValues, AgentInstanceViewFactoryChainContext agentInstanceContext) { if (!groupView.hasViews()) { String message = "Unexpected empty list of child nodes for group view"; log.fatal(".copySubViews " + message); throw new EPException(message); } Object subviewHolder; if (groupView.getViews().size() == 1) { subviewHolder = copyChildView(groupView, propertyNames, groupByValues, agentInstanceContext, groupView.getViews().get(0)); } else { // For each child node ArrayList<View> subViewList = new ArrayList<View>(4); subviewHolder = subViewList; for (View originalChildView : groupView.getViews()) { View copyChildView = copyChildView(groupView, propertyNames, groupByValues, agentInstanceContext, originalChildView); subViewList.add(copyChildView); } } return subviewHolder; } protected static void updateChildViews(Object subViews, EventBean[] newData, EventBean[] oldData) { if (subViews instanceof List) { List<View> viewList = (List<View>) subViews; ViewSupport.updateChildren(viewList, newData, oldData); } else { ((View) subViews).update(newData, oldData); } } private void handleEvent(EventBean theEvent, boolean isNew) { Object groupByValuesKey = getGroupKey(theEvent); // Get child views that belong to this group-by value combination Object subViews = this.subViewsPerKey.get(groupByValuesKey); // If this is a new group-by value, the list of subviews is null and we need to make clone sub-views if (subViews == null) { subViews = makeSubViews(this, propertyNames, groupByValuesKey, agentInstanceContext); subViewsPerKey.put(groupByValuesKey, subViews); } // Construct a pair of lists to hold the events for the grouped value if not already there Pair<Object, Object> pair = groupedEvents.get(subViews); if (pair == null) { pair = new Pair<Object, Object>(null, null); groupedEvents.put(subViews, pair); } // Add event to a child view event list for later child update that includes new and old events if (isNew) { pair.setFirst(addUpgradeToDequeIfPopulated(pair.getFirst(), theEvent)); } else { pair.setSecond(addUpgradeToDequeIfPopulated(pair.getSecond(), theEvent)); } } private static View copyChildView(GroupByView groupView, String[] propertyNames, Object groupByValues, AgentInstanceViewFactoryChainContext agentInstanceContext, View originalChildView) { if (originalChildView instanceof MergeView) { String message = "Unexpected merge view as child of group-by view"; log.fatal(".copySubViews " + message); throw new EPException(message); } if (!(originalChildView instanceof CloneableView)) { throw new EPException("Unexpected error copying subview " + originalChildView.getClass().getName()); } CloneableView cloneableView = (CloneableView) originalChildView; // Copy child node View copyChildView = cloneableView.cloneView(); copyChildView.setParent(groupView); // Make the sub views for child copying from the original to the child copySubViews(groupView.getCriteriaExpressions(), propertyNames, groupByValues, originalChildView, copyChildView, agentInstanceContext); return copyChildView; } private static void copySubViews(ExprNode[] criteriaExpressions, String[] propertyNames, Object groupByValues, View originalView, View copyView, AgentInstanceViewFactoryChainContext agentInstanceContext) { for (View subView : originalView.getViews()) { // Determine if view is our merge view if (subView instanceof MergeViewMarker) { MergeViewMarker mergeView = (MergeViewMarker) subView; if (ExprNodeUtility.deepEquals(mergeView.getGroupFieldNames(), criteriaExpressions)) { // We found our merge view - install a new data merge view on top of it AddPropertyValueView mergeDataView = new AddPropertyValueView(agentInstanceContext, propertyNames, groupByValues, mergeView.getEventType()); // Add to the copied parent subview the view merge data view copyView.addView(mergeDataView); // Add to the new merge data view the actual single merge view instance that clients may attached to mergeDataView.addView(mergeView); // Add a parent view to the single merge view instance mergeView.addParentView(mergeDataView); continue; } } if (!(subView instanceof CloneableView)) { throw new EPException("Unexpected error copying subview"); } CloneableView cloneableView = (CloneableView) subView; View copiedChild = cloneableView.cloneView(); copyView.addView(copiedChild); // Make the sub views for child copySubViews(criteriaExpressions, propertyNames, groupByValues, subView, copiedChild, agentInstanceContext); } } private Object getGroupKey(EventBean theEvent) { eventsPerStream[0] = theEvent; if (criteriaEvaluators.length == 1) { return criteriaEvaluators[0].evaluate(eventsPerStream, true, agentInstanceContext); } Object[] values = new Object[criteriaEvaluators.length]; for (int i = 0; i < criteriaEvaluators.length; i++) { values[i] = criteriaEvaluators[i].evaluate(eventsPerStream, true, agentInstanceContext); } return new MultiKeyUntyped(values); } protected static Object addUpgradeToDequeIfPopulated(Object holder, EventBean theEvent) { if (holder == null) { return theEvent; } else if (holder instanceof Deque) { ArrayDeque<EventBean> deque = (ArrayDeque<EventBean>) holder; deque.add(theEvent); return deque; } else { ArrayDeque<EventBean> deque = new ArrayDeque<EventBean>(4); deque.add((EventBean)holder); deque.add(theEvent); return deque; } } protected static EventBean[] convertToArray(Object eventOrDeque) { if (eventOrDeque == null) { return null; } if (eventOrDeque instanceof EventBean) { return new EventBean[] {(EventBean) eventOrDeque}; } return EventBeanUtility.toArray((ArrayDeque<EventBean>) eventOrDeque); } private static final Log log = LogFactory.getLog(GroupByViewImpl.class); }