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}