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