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