001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.util.toolbox;
018
019import java.util.Collection;
020
021import org.apache.camel.Exchange;
022import org.apache.camel.Expression;
023import org.apache.camel.Predicate;
024import org.apache.camel.TypeConversionException;
025import org.apache.camel.builder.ExpressionBuilder;
026import org.apache.camel.processor.aggregate.AggregationStrategy;
027import org.apache.camel.processor.aggregate.CompletionAwareAggregationStrategy;
028import org.apache.camel.processor.aggregate.TimeoutAwareAggregationStrategy;
029import org.apache.camel.util.ExchangeHelper;
030import org.apache.camel.util.ObjectHelper;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034/**
035 * The Flexible Aggregation Strategy is a highly customizable, fluently configurable aggregation strategy. It allows you to quickly 
036 * allows you to quickly whip up an {@link AggregationStrategy} that is capable of performing the most typical aggregation duties, 
037 * with zero Java code. 
038 * <p/>
039 * It can perform the following logic:
040 * <ul>
041 *   <li>Filtering results based on a defined {@link Predicate} written in any language, such as XPath, OGNL, Simple, Javascript, etc.</li>
042 *   <li>Picking specific data elements for aggregation.</li>
043 *   <li>Accumulating results in any designated {@link Collection} type, e.g. in a HashSet, LinkedList, ArrayList, etc.</li>
044 *   <li>Storing the output in a specific place in the Exchange: a property, a header or in the body.</li>
045 * </ul>
046 * 
047 * It also includes the ability to specify both aggregation batch completion actions and timeout actions, in an abbreviated manner.
048 * <p/>
049 * This Aggregation Strategy is suitable for usage in aggregate, split, multicast, enrich and recipient list EIPs.
050 * 
051 */
052public class FlexibleAggregationStrategy<E extends Object> implements AggregationStrategy, 
053        CompletionAwareAggregationStrategy, TimeoutAwareAggregationStrategy {
054
055    private static final Logger LOG = LoggerFactory.getLogger(FlexibleAggregationStrategy.class);
056    private static final String COLLECTION_AGGR_GUARD_PROPERTY = "CamelFlexAggrStrCollectionGuard";
057
058    private Expression pickExpression = ExpressionBuilder.bodyExpression();
059    private Predicate conditionPredicate;
060    @SuppressWarnings("rawtypes")
061    private Class<? extends Collection> collectionType;
062    @SuppressWarnings("unchecked")
063    private Class<E> castAs = (Class<E>) Object.class;
064    private boolean storeNulls;
065    private boolean ignoreInvalidCasts; // = false
066    private FlexibleAggregationStrategyInjector injector = new BodyInjector(castAs);
067    private TimeoutAwareMixin timeoutMixin;
068    private CompletionAwareMixin completionMixin;
069
070    /**
071     * Initializes a new instance with {@link Object} as the {@link FlexibleAggregationStrategy#castAs} type.
072     */
073    public FlexibleAggregationStrategy() {
074    }
075    
076    /**
077     * Initializes a new instance with the specified type as the {@link FlexibleAggregationStrategy#castAs} type.
078     * @param type The castAs type.
079     */
080    public FlexibleAggregationStrategy(Class<E> type) {
081        this.castAs = type;
082    }
083    
084    /**
085     * Set an expression to extract the element to be aggregated from the incoming {@link Exchange}.
086     * All results are cast to the {@link FlexibleAggregationStrategy#castAs} type (or the type specified in the constructor).
087     * <p/>
088     * By default, it picks the full IN message body of the incoming exchange. 
089     * @param expression The picking expression.
090     * @return This instance.
091     */
092    public FlexibleAggregationStrategy<E> pick(Expression expression) {
093        this.pickExpression = expression;
094        return this;
095    }
096
097    /**
098     * Set a filter condition such as only results satisfying it will be aggregated. 
099     * By default, all picked values will be processed.
100     * @param predicate The condition.
101     * @return This instance.
102     */
103    public FlexibleAggregationStrategy<E> condition(Predicate predicate) {
104        this.conditionPredicate = predicate;
105        return this;
106    }
107
108    /**
109     * Accumulate the result of the <i>pick expression</i> in a collection of the designated type. 
110     * No <tt>null</tt>s will stored unless the {@link FlexibleAggregationStrategy#storeNulls()} option is enabled.
111     * @param collectionType The type of the Collection to aggregate into.
112     * @return This instance.
113     */
114    @SuppressWarnings("rawtypes")
115    public FlexibleAggregationStrategy<E> accumulateInCollection(Class<? extends Collection> collectionType) {
116        this.collectionType = collectionType;
117        return this;
118    }
119
120    /**
121     * Store the result of this Aggregation Strategy (whether an atomic element or a Collection) in a property with
122     * the designated name.
123     * @param propertyName The property name.
124     * @return This instance.
125     */
126    public FlexibleAggregationStrategy<E> storeInProperty(String propertyName) {
127        this.injector = new PropertyInjector(castAs, propertyName);
128        return this;
129    }
130
131    /**
132     * Store the result of this Aggregation Strategy (whether an atomic element or a Collection) in an IN message header with
133     * the designated name.
134     * @param headerName The header name.
135     * @return This instance.
136     */
137    public FlexibleAggregationStrategy<E> storeInHeader(String headerName) {
138        this.injector = new HeaderInjector(castAs, headerName);
139        return this;
140    }
141
142    /**
143     * Store the result of this Aggregation Strategy (whether an atomic element or a Collection) in the body of the IN message.
144     * @return This instance.
145     */
146    public FlexibleAggregationStrategy<E> storeInBody() {
147        this.injector = new BodyInjector(castAs);
148        return this;
149    }
150
151    /**
152     * Cast the result of the <i>pick expression</i> to this type.
153     * @param castAs Type for the cast.
154     * @return This instance.
155     */
156    public FlexibleAggregationStrategy<E> castAs(Class<E> castAs) {
157        this.castAs = castAs;
158        injector.setType(castAs);
159        return this;
160    }
161
162    /**
163     * Enables storing null values in the resulting collection.
164     * By default, this aggregation strategy will drop null values.
165     * @return This instance.
166     */
167    public FlexibleAggregationStrategy<E> storeNulls() {
168        this.storeNulls = true;
169        return this;
170    }
171    
172    /**
173     * Ignores invalid casts instead of throwing an exception if the <i>pick expression</i> result cannot be casted to the 
174     * specified type.
175     * By default, this aggregation strategy will throw an exception if an invalid cast occurs.
176     * @return This instance.
177     */
178    public FlexibleAggregationStrategy<E> ignoreInvalidCasts() {
179        this.ignoreInvalidCasts = true;
180        return this;
181    }
182    
183    /**
184     * Plugs in logic to execute when a timeout occurs.
185     * @param timeoutMixin
186     * @return This instance.
187     */
188    public FlexibleAggregationStrategy<E> timeoutAware(TimeoutAwareMixin timeoutMixin) {
189        this.timeoutMixin = timeoutMixin;
190        return this;
191    }
192
193    /**
194     * Plugs in logic to execute when an aggregation batch completes.
195     * @param completionMixin
196     * @return This instance.
197     */
198    public FlexibleAggregationStrategy<E> completionAware(CompletionAwareMixin completionMixin) {
199        this.completionMixin = completionMixin;
200        return this;
201    }
202    
203    @Override
204    public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
205        Exchange exchange = oldExchange;
206        if (exchange == null) {
207            exchange = ExchangeHelper.createCorrelatedCopy(newExchange, true);
208            injector.prepareAggregationExchange(exchange);
209        }
210
211        // 1. Apply the condition and reject the aggregation if unmatched
212        if (conditionPredicate != null && !conditionPredicate.matches(newExchange)) {
213            LOG.trace("Dropped exchange {} from aggregation as predicate {} was not matched", newExchange, conditionPredicate);
214            return exchange;
215        }
216
217        // 2. Pick the appropriate element of the incoming message, casting it to the specified class
218        //    If null, act accordingly based on storeNulls
219        E picked = null;
220        try {
221            picked = pickExpression.evaluate(newExchange, castAs);
222        } catch (TypeConversionException exception) {
223            if (!ignoreInvalidCasts) {
224                throw exception;
225            }
226        }
227        
228        if (picked == null && !storeNulls) {
229            LOG.trace("Dropped exchange {} from aggregation as pick expression returned null and storing nulls is not enabled", newExchange);
230            return exchange;
231        }
232
233        if (collectionType == null) {
234            injectAsRawValue(exchange, picked);
235        } else {
236            injectAsCollection(exchange, picked);
237        }
238
239        return exchange;
240    }
241    
242
243    @Override
244    public void timeout(Exchange oldExchange, int index, int total, long timeout) {
245        if (timeoutMixin == null) {
246            return;
247        }
248        timeoutMixin.timeout(oldExchange, index, total, timeout);
249    }
250
251    @Override
252    public void onCompletion(Exchange exchange) {
253        if (completionMixin == null) {
254            return;
255        }
256        completionMixin.onCompletion(exchange);
257    }
258
259    private void injectAsRawValue(Exchange oldExchange, E picked) {
260        injector.setValue(oldExchange, picked);
261    }
262
263    private void injectAsCollection(Exchange oldExchange, E picked) {
264        Collection<E> col = injector.getValueAsCollection(oldExchange);
265        col = safeInsertIntoCollection(oldExchange, col, picked);
266        injector.setValueAsCollection(oldExchange, col);
267    }
268
269    @SuppressWarnings("unchecked")
270    private Collection<E> safeInsertIntoCollection(Exchange oldExchange, Collection<E> oldValue, E toInsert) {
271        Collection<E> collection = null;
272        try {
273            if (oldValue == null || oldExchange.getProperty(COLLECTION_AGGR_GUARD_PROPERTY, Boolean.class) == null) {
274                try {
275                    collection = collectionType.newInstance();
276                } catch (Exception e) {
277                    LOG.warn("Could not instantiate collection of type {}. Aborting aggregation.", collectionType);
278                    throw ObjectHelper.wrapCamelExecutionException(oldExchange, e);
279                }
280                oldExchange.setProperty(COLLECTION_AGGR_GUARD_PROPERTY, Boolean.FALSE);
281            } else {
282                collection = collectionType.cast(oldValue);
283            }
284            
285            if (collection != null) {
286                collection.add(toInsert);
287            }
288            
289        } catch (ClassCastException exception) {
290            if (!ignoreInvalidCasts) {
291                throw exception;
292            }
293        }
294        return collection;
295    }
296    
297    public interface TimeoutAwareMixin {
298        void timeout(Exchange exchange, int index, int total, long timeout);
299    }
300    
301    public interface CompletionAwareMixin {
302        void onCompletion(Exchange exchange);
303    }
304    
305    private abstract class FlexibleAggregationStrategyInjector {
306        protected Class<E> type;
307        
308        public FlexibleAggregationStrategyInjector(Class<E> type) {
309            this.type = type;
310        }
311        
312        public void setType(Class<E> type) {
313            this.type = type;
314        }
315        
316        public abstract void prepareAggregationExchange(Exchange exchange);
317        public abstract E getValue(Exchange exchange);
318        public abstract void setValue(Exchange exchange, E obj);
319        public abstract Collection<E> getValueAsCollection(Exchange exchange);
320        public abstract void setValueAsCollection(Exchange exchange, Collection<E> obj);
321    }
322    
323    private class PropertyInjector extends FlexibleAggregationStrategyInjector {
324        private String propertyName;
325        
326        public PropertyInjector(Class<E> type, String propertyName) {
327            super(type);
328            this.propertyName = propertyName;
329        }
330        
331        @Override
332        public void prepareAggregationExchange(Exchange exchange) {
333            exchange.removeProperty(propertyName);
334        }
335        
336        @Override
337        public E getValue(Exchange exchange) {
338            return exchange.getProperty(propertyName, type);
339        }
340
341        @Override
342        public void setValue(Exchange exchange, E obj) {
343            exchange.setProperty(propertyName, obj);
344        }
345
346        @Override @SuppressWarnings("unchecked")
347        public Collection<E> getValueAsCollection(Exchange exchange) {
348            return exchange.getProperty(propertyName, Collection.class);
349        }
350
351        @Override
352        public void setValueAsCollection(Exchange exchange, Collection<E> obj) {
353            exchange.setProperty(propertyName, obj);
354        }
355
356    }
357    
358    private class HeaderInjector extends FlexibleAggregationStrategyInjector {
359        private String headerName;
360        
361        public HeaderInjector(Class<E> type, String headerName) {
362            super(type);
363            this.headerName = headerName;
364        }
365        
366        @Override
367        public void prepareAggregationExchange(Exchange exchange) {
368            exchange.getIn().removeHeader(headerName);
369        }
370        
371        @Override
372        public E getValue(Exchange exchange) {
373            return exchange.getIn().getHeader(headerName, type);
374        }
375
376        @Override
377        public void setValue(Exchange exchange, E obj) {
378            exchange.getIn().setHeader(headerName, obj);
379        }
380
381        @Override @SuppressWarnings("unchecked")
382        public Collection<E> getValueAsCollection(Exchange exchange) {
383            return exchange.getIn().getHeader(headerName, Collection.class);
384        }
385        
386        @Override
387        public void setValueAsCollection(Exchange exchange, Collection<E> obj) {
388            exchange.getIn().setHeader(headerName, obj);
389        }
390    }
391    
392    private class BodyInjector extends FlexibleAggregationStrategyInjector {
393        public BodyInjector(Class<E> type) {
394            super(type);
395        }
396
397        @Override
398        public void prepareAggregationExchange(Exchange exchange) {
399            exchange.getIn().setBody(null);
400        }
401        
402        @Override
403        public E getValue(Exchange exchange) {
404            return exchange.getIn().getBody(type);
405        }
406
407        @Override
408        public void setValue(Exchange exchange, E obj) {
409            exchange.getIn().setBody(obj);
410        }
411
412        @Override @SuppressWarnings("unchecked")
413        public Collection<E> getValueAsCollection(Exchange exchange) {
414            return exchange.getIn().getBody(Collection.class);
415        }
416        
417        @Override
418        public void setValueAsCollection(Exchange exchange, Collection<E> obj) {
419            exchange.getIn().setBody(obj);
420        }
421    }
422    
423}