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 }