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}