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