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     */
017    package org.apache.camel.util.toolbox;
018    
019    import java.util.Collection;
020    
021    import org.apache.camel.Exchange;
022    import org.apache.camel.Expression;
023    import org.apache.camel.Predicate;
024    import org.apache.camel.TypeConversionException;
025    import org.apache.camel.builder.ExpressionBuilder;
026    import org.apache.camel.processor.aggregate.AggregationStrategy;
027    import org.apache.camel.processor.aggregate.CompletionAwareAggregationStrategy;
028    import org.apache.camel.processor.aggregate.TimeoutAwareAggregationStrategy;
029    import org.apache.camel.util.ExchangeHelper;
030    import org.apache.camel.util.ObjectHelper;
031    import org.slf4j.Logger;
032    import 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     */
052    public 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 = (Collection<E>) 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 = (Collection<E>) 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 (Collection<E>) 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 (Collection<E>) 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 (Collection<E>) 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    }