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}