/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.camel.util.toolbox;
import java.util.Collection;
import org.apache.camel.Exchange;
import org.apache.camel.Expression;
import org.apache.camel.Predicate;
import org.apache.camel.TypeConversionException;
import org.apache.camel.builder.ExpressionBuilder;
import org.apache.camel.processor.aggregate.AggregationStrategy;
import org.apache.camel.processor.aggregate.CompletionAwareAggregationStrategy;
import org.apache.camel.processor.aggregate.TimeoutAwareAggregationStrategy;
import org.apache.camel.util.ExchangeHelper;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The Flexible Aggregation Strategy is a highly customizable, fluently configurable aggregation strategy. It allows you to quickly
* allows you to quickly whip up an {@link AggregationStrategy} that is capable of performing the most typical aggregation duties,
* with zero Java code.
* <p/>
* It can perform the following logic:
* <ul>
* <li>Filtering results based on a defined {@link Predicate} written in any language, such as XPath, OGNL, Simple, Javascript, etc.</li>
* <li>Picking specific data elements for aggregation.</li>
* <li>Accumulating results in any designated {@link Collection} type, e.g. in a HashSet, LinkedList, ArrayList, etc.</li>
* <li>Storing the output in a specific place in the Exchange: a property, a header or in the body.</li>
* </ul>
*
* It also includes the ability to specify both aggregation batch completion actions and timeout actions, in an abbreviated manner.
* <p/>
* This Aggregation Strategy is suitable for usage in aggregate, split, multicast, enrich and recipient list EIPs.
*
*/
public class FlexibleAggregationStrategy<E extends Object> implements AggregationStrategy,
CompletionAwareAggregationStrategy, TimeoutAwareAggregationStrategy {
private static final Logger LOG = LoggerFactory.getLogger(FlexibleAggregationStrategy.class);
private Expression pickExpression = ExpressionBuilder.bodyExpression();
private Predicate conditionPredicate;
@SuppressWarnings("rawtypes")
private Class<? extends Collection> collectionType;
@SuppressWarnings("unchecked")
private Class<E> castAs = (Class<E>) Object.class;
private boolean storeNulls;
private boolean ignoreInvalidCasts; // = false
private FlexibleAggregationStrategyInjector injector = new BodyInjector(castAs);
private TimeoutAwareMixin timeoutMixin;
private CompletionAwareMixin completionMixin;
/**
* Initializes a new instance with {@link Object} as the {@link FlexibleAggregationStrategy#castAs} type.
*/
public FlexibleAggregationStrategy() {
}
/**
* Initializes a new instance with the specified type as the {@link FlexibleAggregationStrategy#castAs} type.
* @param type The castAs type.
*/
public FlexibleAggregationStrategy(Class<E> type) {
this.castAs = type;
}
/**
* Set an expression to extract the element to be aggregated from the incoming {@link Exchange}.
* All results are cast to the {@link FlexibleAggregationStrategy#castAs} type (or the type specified in the constructor).
* <p/>
* By default, it picks the full IN message body of the incoming exchange.
* @param expression The picking expression.
* @return This instance.
*/
public FlexibleAggregationStrategy<E> pick(Expression expression) {
this.pickExpression = expression;
return this;
}
/**
* Set a filter condition such as only results satisfying it will be aggregated.
* By default, all picked values will be processed.
* @param predicate The condition.
* @return This instance.
*/
public FlexibleAggregationStrategy<E> condition(Predicate predicate) {
this.conditionPredicate = predicate;
return this;
}
/**
* Accumulate the result of the <i>pick expression</i> in a collection of the designated type.
* No <tt>null</tt>s will stored unless the {@link FlexibleAggregationStrategy#storeNulls()} option is enabled.
* @param collectionType The type of the Collection to aggregate into.
* @return This instance.
*/
@SuppressWarnings("rawtypes")
public FlexibleAggregationStrategy<E> accumulateInCollection(Class<? extends Collection> collectionType) {
this.collectionType = collectionType;
return this;
}
/**
* Store the result of this Aggregation Strategy (whether an atomic element or a Collection) in a property with
* the designated name.
* @param propertyName The property name.
* @return This instance.
*/
public FlexibleAggregationStrategy<E> storeInProperty(String propertyName) {
this.injector = new PropertyInjector(castAs, propertyName);
return this;
}
/**
* Store the result of this Aggregation Strategy (whether an atomic element or a Collection) in an IN message header with
* the designated name.
* @param headerName The header name.
* @return This instance.
*/
public FlexibleAggregationStrategy<E> storeInHeader(String headerName) {
this.injector = new HeaderInjector(castAs, headerName);
return this;
}
/**
* Store the result of this Aggregation Strategy (whether an atomic element or a Collection) in the body of the IN message.
* @return This instance.
*/
public FlexibleAggregationStrategy<E> storeInBody() {
this.injector = new BodyInjector(castAs);
return this;
}
/**
* Cast the result of the <i>pick expression</i> to this type.
* @param castAs Type for the cast.
* @return This instance.
*/
public FlexibleAggregationStrategy<E> castAs(Class<E> castAs) {
this.castAs = castAs;
injector.setType(castAs);
return this;
}
/**
* Enables storing null values in the resulting collection.
* By default, this aggregation strategy will drop null values.
* @return This instance.
*/
public FlexibleAggregationStrategy<E> storeNulls() {
this.storeNulls = true;
return this;
}
/**
* Ignores invalid casts instead of throwing an exception if the <i>pick expression</i> result cannot be casted to the
* specified type.
* By default, this aggregation strategy will throw an exception if an invalid cast occurs.
* @return This instance.
*/
public FlexibleAggregationStrategy<E> ignoreInvalidCasts() {
this.ignoreInvalidCasts = true;
return this;
}
/**
* Plugs in logic to execute when a timeout occurs.
* @param timeoutMixin
* @return This instance.
*/
public FlexibleAggregationStrategy<E> timeoutAware(TimeoutAwareMixin timeoutMixin) {
this.timeoutMixin = timeoutMixin;
return this;
}
/**
* Plugs in logic to execute when an aggregation batch completes.
* @param completionMixin
* @return This instance.
*/
public FlexibleAggregationStrategy<E> completionAware(CompletionAwareMixin completionMixin) {
this.completionMixin = completionMixin;
return this;
}
@Override
public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
Exchange exchange = oldExchange;
if (exchange == null) {
exchange = ExchangeHelper.createCorrelatedCopy(newExchange, true);
injector.prepareAggregationExchange(exchange);
}
// 1. Apply the condition and reject the aggregation if unmatched
if (conditionPredicate != null && !conditionPredicate.matches(newExchange)) {
LOG.trace("Dropped exchange {} from aggregation as predicate {} was not matched", newExchange, conditionPredicate);
return exchange;
}
// 2. Pick the appropriate element of the incoming message, casting it to the specified class
// If null, act accordingly based on storeNulls
E picked = null;
try {
picked = pickExpression.evaluate(newExchange, castAs);
} catch (TypeConversionException exception) {
if (!ignoreInvalidCasts) {
throw exception;
}
}
if (picked == null && !storeNulls) {
LOG.trace("Dropped exchange {} from aggregation as pick expression returned null and storing nulls is not enabled", newExchange);
return exchange;
}
if (collectionType == null) {
injectAsRawValue(exchange, picked);
} else {
injectAsCollection(exchange, picked, collectionType);
}
return exchange;
}
@Override
public void timeout(Exchange oldExchange, int index, int total, long timeout) {
if (timeoutMixin == null) {
return;
}
timeoutMixin.timeout(oldExchange, index, total, timeout);
}
@Override
public void onCompletion(Exchange exchange) {
if (completionMixin == null) {
return;
}
completionMixin.onCompletion(exchange);
}
private void injectAsRawValue(Exchange oldExchange, E picked) {
injector.setValue(oldExchange, picked);
}
private void injectAsCollection(Exchange oldExchange, E picked, Class<? extends Collection> collectionType) {
Collection<E> col = injector.getValueAsCollection(oldExchange, collectionType);
col = safeInsertIntoCollection(oldExchange, col, picked);
injector.setValueAsCollection(oldExchange, col);
}
@SuppressWarnings("unchecked")
private Collection<E> safeInsertIntoCollection(Exchange oldExchange, Collection<E> oldValue, E toInsert) {
Collection<E> collection = null;
try {
if (oldValue == null || oldExchange.getProperty(Exchange.AGGREGATED_COLLECTION_GUARD, Boolean.class) == null) {
try {
collection = collectionType.newInstance();
} catch (Exception e) {
LOG.warn("Could not instantiate collection of type {}. Aborting aggregation.", collectionType);
throw ObjectHelper.wrapCamelExecutionException(oldExchange, e);
}
oldExchange.setProperty(Exchange.AGGREGATED_COLLECTION_GUARD, Boolean.FALSE);
} else {
collection = collectionType.cast(oldValue);
}
if (collection != null) {
collection.add(toInsert);
}
} catch (ClassCastException exception) {
if (!ignoreInvalidCasts) {
throw exception;
}
}
return collection;
}
public interface TimeoutAwareMixin {
void timeout(Exchange exchange, int index, int total, long timeout);
}
public interface CompletionAwareMixin {
void onCompletion(Exchange exchange);
}
private abstract class FlexibleAggregationStrategyInjector {
protected Class<E> type;
FlexibleAggregationStrategyInjector(Class<E> type) {
this.type = type;
}
public void setType(Class<E> type) {
this.type = type;
}
public abstract void prepareAggregationExchange(Exchange exchange);
public abstract E getValue(Exchange exchange);
public abstract void setValue(Exchange exchange, E obj);
public abstract Collection<E> getValueAsCollection(Exchange exchange, Class<? extends Collection> type);
public abstract void setValueAsCollection(Exchange exchange, Collection<E> obj);
}
private class PropertyInjector extends FlexibleAggregationStrategyInjector {
private String propertyName;
PropertyInjector(Class<E> type, String propertyName) {
super(type);
this.propertyName = propertyName;
}
@Override
public void prepareAggregationExchange(Exchange exchange) {
exchange.removeProperty(propertyName);
}
@Override
public E getValue(Exchange exchange) {
return exchange.getProperty(propertyName, type);
}
@Override
public void setValue(Exchange exchange, E obj) {
exchange.setProperty(propertyName, obj);
}
@Override @SuppressWarnings("unchecked")
public Collection<E> getValueAsCollection(Exchange exchange, Class<? extends Collection> type) {
Object value = exchange.getProperty(propertyName);
if (value == null) {
// empty so create a new collection to host this
return exchange.getContext().getInjector().newInstance(type);
} else {
return exchange.getProperty(propertyName, type);
}
}
@Override
public void setValueAsCollection(Exchange exchange, Collection<E> obj) {
exchange.setProperty(propertyName, obj);
}
}
private class HeaderInjector extends FlexibleAggregationStrategyInjector {
private String headerName;
HeaderInjector(Class<E> type, String headerName) {
super(type);
this.headerName = headerName;
}
@Override
public void prepareAggregationExchange(Exchange exchange) {
exchange.getIn().removeHeader(headerName);
}
@Override
public E getValue(Exchange exchange) {
return exchange.getIn().getHeader(headerName, type);
}
@Override
public void setValue(Exchange exchange, E obj) {
exchange.getIn().setHeader(headerName, obj);
}
@Override @SuppressWarnings("unchecked")
public Collection<E> getValueAsCollection(Exchange exchange, Class<? extends Collection> type) {
Object value = exchange.getIn().getHeader(headerName);
if (value == null) {
// empty so create a new collection to host this
return exchange.getContext().getInjector().newInstance(type);
} else {
return exchange.getIn().getHeader(headerName, type);
}
}
@Override
public void setValueAsCollection(Exchange exchange, Collection<E> obj) {
exchange.getIn().setHeader(headerName, obj);
}
}
private class BodyInjector extends FlexibleAggregationStrategyInjector {
BodyInjector(Class<E> type) {
super(type);
}
@Override
public void prepareAggregationExchange(Exchange exchange) {
exchange.getIn().setBody(null);
}
@Override
public E getValue(Exchange exchange) {
return exchange.getIn().getBody(type);
}
@Override
public void setValue(Exchange exchange, E obj) {
exchange.getIn().setBody(obj);
}
@Override @SuppressWarnings("unchecked")
public Collection<E> getValueAsCollection(Exchange exchange, Class<? extends Collection> type) {
Object value = exchange.getIn().getBody();
if (value == null) {
// empty so create a new collection to host this
return exchange.getContext().getInjector().newInstance(type);
} else {
return exchange.getIn().getBody(type);
}
}
@Override
public void setValueAsCollection(Exchange exchange, Collection<E> obj) {
exchange.getIn().setBody(obj);
}
}
}