/* *************************************************************************************** * Copyright (C) 2006 EsperTech, Inc. All rights reserved. * * http://www.espertech.com/esper * * 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.core.ExprEvaluator; import com.espertech.esper.epl.expression.core.ExprNode; import com.espertech.esper.epl.expression.core.ExprNodeUtility; import com.espertech.esper.event.EventBeanUtility; import com.espertech.esper.metrics.instrumentation.InstrumentationHelper; import com.espertech.esper.view.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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. * <p> * The view takes a single parameter which is the field name returning the key value to group. * <p> * This view can, for example, be used to calculate the average price per symbol for a list of symbols. * <p> * 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 { public final static String VIEWNAME = "Group-By"; 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 * @param criteriaEvaluators evaluators */ 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] = ExprNodeUtility.toExpressionStringMinPrecedenceSafe(criteriaExpressions[i]); } } 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) { if (InstrumentationHelper.ENABLED) { InstrumentationHelper.get().qViewProcessIRStream(this, "Grouped", newData, 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(); } if (InstrumentationHelper.ENABLED) { InstrumentationHelper.get().aViewProcessIRStream(); } } 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.error(".copySubViews " + message); throw new EPException(message); } Object subviewHolder; if (groupView.getViews().length == 1) { subviewHolder = copyChildView(groupView, propertyNames, groupByValues, agentInstanceContext, groupView.getViews()[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; } public void visitViewContainer(ViewDataVisitorContained viewDataVisitor) { viewDataVisitor.visitPrimary(VIEWNAME, subViewsPerKey.size()); for (Map.Entry<Object, Object> entry : subViewsPerKey.entrySet()) { GroupByViewImpl.visitView(viewDataVisitor, entry.getKey(), entry.getValue()); } } public static void visitView(ViewDataVisitorContained viewDataVisitor, Object groupkey, Object subviewHolder) { if (subviewHolder == null) { return; } if (subviewHolder instanceof View) { viewDataVisitor.visitContained(groupkey, (View) subviewHolder); return; } if (subviewHolder instanceof Collection) { Collection<View> deque = (Collection<View>) subviewHolder; for (View view : deque) { viewDataVisitor.visitContained(groupkey, view); return; } } } @Override public boolean removeView(View view) { if (!(view instanceof GroupableView)) { super.removeView(view); } boolean removed = super.removeView(view); if (!removed) { return false; } if (!hasViews()) { subViewsPerKey.clear(); return true; } GroupableView removedView = (GroupableView) view; Deque<Object> removedKeys = null; for (Map.Entry<Object, Object> entry : subViewsPerKey.entrySet()) { Object value = entry.getValue(); if (value instanceof View) { GroupableView subview = (GroupableView) value; if (compareViews(subview, removedView)) { if (removedKeys == null) { removedKeys = new ArrayDeque<Object>(); } removedKeys.add(entry.getKey()); } } else if (value instanceof List) { List<View> subviews = (List<View>) value; for (int i = 0; i < subviews.size(); i++) { GroupableView subview = (GroupableView) subviews.get(i); if (compareViews(subview, removedView)) { subviews.remove(i); if (subviews.isEmpty()) { if (removedKeys == null) { removedKeys = new ArrayDeque<Object>(); } removedKeys.add(entry.getKey()); } break; } } } } if (removedKeys != null) { for (Object key : removedKeys) { subViewsPerKey.remove(key); } } return true; } private boolean compareViews(GroupableView subview, GroupableView removed) { return subview.getViewFactory() == removed.getViewFactory(); } 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.error(".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, false)) { if (mergeView.getEventType() != copyView.getEventType()) { // We found our merge view - install a new data merge view on top of it AddPropertyValueOptionalView addPropertyView = new AddPropertyValueOptionalView(agentInstanceContext, propertyNames, groupByValues, mergeView.getEventType()); // Add to the copied parent subview the view merge data view copyView.addView(addPropertyView); // Add to the new merge data view the actual single merge view instance that clients may attached to addPropertyView.addView(mergeView); // Add a parent view to the single merge view instance mergeView.addParentView(addPropertyView); } else { // Add to the copied parent subview the view merge data view copyView.addView(mergeView); // Add a parent view to the single merge view instance mergeView.addParentView(copyView); } 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) { Deque<EventBean> deque = (Deque<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 Logger log = LoggerFactory.getLogger(GroupByViewImpl.class); }