/* * Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com * The software in this package is published under the terms of the CPAL v1.0 * license, a copy of which has been included with this distribution in the * LICENSE.txt file. */ package org.mule.runtime.core.routing; import static java.util.stream.Collectors.toList; import static org.mule.runtime.core.message.GroupCorrelation.NOT_SET; import static org.mule.runtime.core.util.StringUtils.DASH; import org.mule.runtime.api.exception.MuleException; import org.mule.runtime.api.message.Message; import org.mule.runtime.core.api.Event; import org.mule.runtime.core.api.MuleContext; import org.mule.runtime.core.api.MuleSession; import org.mule.runtime.core.api.config.MuleProperties; import org.mule.runtime.api.store.ObjectStoreException; import org.mule.runtime.core.api.store.PartitionableObjectStore; import org.mule.runtime.core.session.DefaultMuleSession; import org.mule.runtime.core.util.ClassUtils; import org.mule.runtime.core.util.store.DeserializationPostInitialisable; import java.io.Serializable; import java.util.Arrays; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Optional; import org.apache.commons.collections.IteratorUtils; /** * <code>EventGroup</code> is a holder over events grouped by a common group Id. This can be used by components such as routers to * managed related events. */ // @ThreadSafe public class EventGroup implements Comparable<EventGroup>, Serializable, DeserializationPostInitialisable { /** * Serial version */ private static final long serialVersionUID = 953739659615692697L; public static final Event[] EMPTY_EVENTS_ARRAY = new Event[0]; public static final String MULE_ARRIVAL_ORDER_PROPERTY = MuleProperties.PROPERTY_PREFIX + "ARRIVAL_ORDER"; private final Object groupId; private transient PartitionableObjectStore<Event> eventsObjectStore; private final String storePrefix; private final String eventsPartitionKey; private final long created; private final Integer expectedSize; transient private MuleContext muleContext; private int arrivalOrderCounter = 0; public static final String DEFAULT_STORE_PREFIX = "DEFAULT_STORE"; public EventGroup(Object groupId, MuleContext muleContext) { this(groupId, muleContext, Optional.empty(), DEFAULT_STORE_PREFIX); } public EventGroup(Object groupId, MuleContext muleContext, Optional<Integer> expectedSize, String storePrefix) { super(); this.created = System.currentTimeMillis(); this.muleContext = muleContext; this.storePrefix = storePrefix; this.eventsPartitionKey = storePrefix + ".eventGroups." + groupId; this.expectedSize = expectedSize.orElse(null); this.groupId = groupId; } /** * Compare this EventGroup to another one. If the receiver and the argument both have groupIds that are {@link Comparable}, they * are used for the comparison; otherwise - since the id can be any object - the group creation time stamp is used as fallback. * Older groups are considered "smaller". * * @see java.lang.Comparable#compareTo(java.lang.Object) */ @Override @SuppressWarnings("unchecked") public int compareTo(EventGroup other) { Object otherId = other.getGroupId(); if (groupId instanceof Comparable<?> && otherId instanceof Comparable<?>) { return ((Comparable<Object>) groupId).compareTo(otherId); } else { long diff = created - other.getCreated(); return (diff > 0 ? 1 : (diff < 0 ? -1 : 0)); } } /** * Compares two EventGroups for equality. EventGroups are considered equal if their groupIds (as returned by * {@link #getGroupId()}) are equal. * * @see java.lang.Object#equals(Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof EventGroup)) { return false; } final EventGroup other = (EventGroup) obj; if (groupId == null) { return (other.groupId == null); } return groupId.equals(other.groupId); } /** * The hashCode of an EventGroup is derived from the object returned by {@link #getGroupId()}. * * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return groupId.hashCode(); } /** * Returns an identifier for this EventGroup. It is recommended that this id is unique and {@link Comparable} e.g. a UUID. * * @return the id of this event group */ public Object getGroupId() { return groupId; } /** * Returns an iterator over a snapshot copy of this group's collected events sorted by their arrival time. If you need to * iterate over the group and e.g. remove select events, do so via {@link #removeEvent(Event)}. If you need to do so atomically * in order to prevent e.g. concurrent reception/aggregation of the group during iteration, wrap the iteration in a synchronized * block on the group instance. * * @return an iterator over collected {@link Event}s. * @throws ObjectStoreException */ public Iterator<Event> iterator() throws ObjectStoreException { return iterator(true); } /** * Returns an iterator over a snapshot copy of this group's collected events., optionally sorted by arrival order. If you need * to iterate over the group and e.g. remove select events, do so via {@link #removeEvent(Event)}. If you need to do so * atomically in order to prevent e.g. concurrent reception/aggregation of the group during iteration, wrap the iteration in a * synchronized block on the group instance. * * @return an iterator over collected {@link Event}s. * @throws ObjectStoreException */ @SuppressWarnings("unchecked") public Iterator<Event> iterator(boolean sortByArrival) throws ObjectStoreException { synchronized (this) { if (eventsObjectStore.allKeys(eventsPartitionKey).isEmpty()) { return IteratorUtils.emptyIterator(); } else { return IteratorUtils.arrayIterator(this.toArray(sortByArrival)); } } } /** * Returns a snapshot of collected events in this group sorted by their arrival time. * * @return an array of collected {@link Event}s. * @throws ObjectStoreException */ public Event[] toArray() throws ObjectStoreException { return toArray(true); } /** * Returns a snapshot of collected events in this group, optionally sorted by their arrival time. * * @return an array of collected {@link Event}s. * @throws ObjectStoreException */ public Event[] toArray(boolean sortByArrival) throws ObjectStoreException { synchronized (this) { if (eventsObjectStore.allKeys(eventsPartitionKey).isEmpty()) { return EMPTY_EVENTS_ARRAY; } List<Serializable> keys = eventsObjectStore.allKeys(eventsPartitionKey); Event[] eventArray = new Event[keys.size()]; for (int i = 0; i < keys.size(); i++) { eventArray[i] = eventsObjectStore.retrieve(keys.get(i), eventsPartitionKey); } if (sortByArrival) { Arrays.sort(eventArray, new ArrivalOrderEventComparator()); } return eventArray; } } /** * Add the given event to this group. * * @param event the event to add * @throws ObjectStoreException */ public void addEvent(Event event) throws ObjectStoreException { synchronized (this) { event = Event.builder(event).addVariable(MULE_ARRIVAL_ORDER_PROPERTY, ++arrivalOrderCounter).build(); // Using both event ID and CorrelationSequence since in certain instances // when an event is split up, the same event IDs are used. Serializable key = getEventKey(event); eventsObjectStore.store(key, event, eventsPartitionKey); } } private String getEventKey(Event event) { StringBuilder stringBuilder = new StringBuilder(); event.getGroupCorrelation().getSequence().ifPresent(v -> stringBuilder.append(v + DASH)); stringBuilder.append(event.hashCode()); stringBuilder.append(DASH); stringBuilder.append(event.getContext().getId()); return stringBuilder.toString(); } /** * Return the creation timestamp of the current group in milliseconds. * * @return the timestamp when this group was instantiated. */ public long getCreated() { return created; } /** * Returns the number of events collected so far. * * @return number of events in this group or 0 if the group is empty. */ public int size() { synchronized (this) { try { return eventsObjectStore.allKeys(eventsPartitionKey).size(); } catch (ObjectStoreException e) { // TODO Check if this is ok. return -1; } } } /** * Returns the number of events that this EventGroup is expecting before correlation can proceed. * * @return expected number of events or null if no expected size was specified. */ public Optional<Integer> expectedSize() { return Optional.ofNullable(expectedSize); } /** * Removes all events from this group. * * @throws ObjectStoreException */ public void clear() throws ObjectStoreException { synchronized (this) { eventsObjectStore.clear(eventsPartitionKey); eventsObjectStore.close(eventsPartitionKey); } } @Override public String toString() { StringBuilder buf = new StringBuilder(80); buf.append(ClassUtils.getSimpleName(this.getClass())); buf.append(" {"); buf.append("id=").append(groupId); buf.append(", expected size=").append(expectedSize().map(v -> v.toString()).orElse(NOT_SET)); try { synchronized (this) { int currentSize; currentSize = eventsObjectStore.allKeys(eventsPartitionKey).size(); buf.append(", current events=").append(currentSize); if (currentSize > 0) { buf.append(" ["); Iterator<Serializable> i = eventsObjectStore.allKeys(eventsPartitionKey).iterator(); while (i.hasNext()) { Serializable id = i.next(); buf.append(eventsObjectStore.retrieve(id, eventsPartitionKey).getCorrelationId()); if (i.hasNext()) { buf.append(", "); } } buf.append(']'); } } } catch (ObjectStoreException e) { buf.append("ObjectStoreException " + e + " caught:" + e.getMessage()); } buf.append('}'); return buf.toString(); } public Event getMessageCollectionEvent() { try { if (size() > 0) { Event[] muleEvents = toArray(true); Event lastEvent = muleEvents[muleEvents.length - 1]; List<Message> messageList = Arrays.stream(muleEvents).map(event -> event.getMessage()).collect(toList()); final Message.Builder builder = Message.builder().collectionPayload(messageList, Message.class); Event muleEvent = Event.builder(lastEvent).message(builder.build()).session(getMergedSession(muleEvents)).build(); return muleEvent; } else { return null; } } catch (ObjectStoreException e) { // Nothing to do... return null; } } protected MuleSession getMergedSession(Event[] events) throws ObjectStoreException { MuleSession session = new DefaultMuleSession(events[0].getSession()); for (int i = 1; i < events.length - 1; i++) { addAndOverrideSessionProperties(session, events[i]); } addAndOverrideSessionProperties(session, events[events.length - 1]); return session; } private void addAndOverrideSessionProperties(MuleSession session, Event event) { for (String name : event.getSession().getPropertyNamesAsSet()) { session.setProperty(name, event.getSession().getProperty(name)); } } public void initAfterDeserialisation(MuleContext context) throws MuleException { this.muleContext = context; } public void initEventsStore(PartitionableObjectStore<Event> events) throws ObjectStoreException { this.eventsObjectStore = events; events.open(eventsPartitionKey); } public boolean isInitialised() { return muleContext != null; } public final class ArrivalOrderEventComparator implements Comparator<Event> { @Override public int compare(Event event1, Event event2) { return getEventOrder(event1) - getEventOrder(event2); } private int getEventOrder(Event event) { Integer orderVariable = (Integer) event.getVariable(MULE_ARRIVAL_ORDER_PROPERTY).getValue(); return orderVariable != null ? orderVariable : -1; } } }