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.processor;
018
019import org.apache.camel.AsyncCallback;
020import org.apache.camel.AsyncProcessor;
021import org.apache.camel.CamelContext;
022import org.apache.camel.CamelContextAware;
023import org.apache.camel.CamelExchangeException;
024import org.apache.camel.Endpoint;
025import org.apache.camel.Exchange;
026import org.apache.camel.ExchangePattern;
027import org.apache.camel.Expression;
028import org.apache.camel.Producer;
029import org.apache.camel.impl.DefaultExchange;
030import org.apache.camel.impl.EmptyProducerCache;
031import org.apache.camel.impl.ProducerCache;
032import org.apache.camel.processor.aggregate.AggregationStrategy;
033import org.apache.camel.spi.EndpointUtilizationStatistics;
034import org.apache.camel.spi.IdAware;
035import org.apache.camel.support.ServiceSupport;
036import org.apache.camel.util.AsyncProcessorConverterHelper;
037import org.apache.camel.util.AsyncProcessorHelper;
038import org.apache.camel.util.EventHelper;
039import org.apache.camel.util.ExchangeHelper;
040import org.apache.camel.util.ServiceHelper;
041import org.apache.camel.util.StopWatch;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044
045import static org.apache.camel.util.ExchangeHelper.copyResultsPreservePattern;
046
047/**
048 * A content enricher that enriches input data by first obtaining additional
049 * data from a <i>resource</i> represented by an endpoint <code>producer</code>
050 * and second by aggregating input data and additional data. Aggregation of
051 * input data and additional data is delegated to an {@link AggregationStrategy}
052 * object.
053 * <p/>
054 * Uses a {@link org.apache.camel.Producer} to obtain the additional data as opposed to {@link PollEnricher}
055 * that uses a {@link org.apache.camel.PollingConsumer}.
056 *
057 * @see PollEnricher
058 */
059public class Enricher extends ServiceSupport implements AsyncProcessor, IdAware, CamelContextAware {
060
061    private static final Logger LOG = LoggerFactory.getLogger(Enricher.class);
062    private CamelContext camelContext;
063    private String id;
064    private ProducerCache producerCache;
065    private final Expression expression;
066    private AggregationStrategy aggregationStrategy;
067    private boolean aggregateOnException;
068    private boolean shareUnitOfWork;
069    private int cacheSize;
070    private boolean ignoreInvalidEndpoint;
071
072    public Enricher(Expression expression) {
073        this.expression = expression;
074    }
075
076    public CamelContext getCamelContext() {
077        return camelContext;
078    }
079
080    public void setCamelContext(CamelContext camelContext) {
081        this.camelContext = camelContext;
082    }
083
084    public String getId() {
085        return id;
086    }
087
088    public void setId(String id) {
089        this.id = id;
090    }
091
092    public Expression getExpression() {
093        return expression;
094    }
095
096    public EndpointUtilizationStatistics getEndpointUtilizationStatistics() {
097        return producerCache.getEndpointUtilizationStatistics();
098    }
099
100    public void setAggregationStrategy(AggregationStrategy aggregationStrategy) {
101        this.aggregationStrategy = aggregationStrategy;
102    }
103
104    public AggregationStrategy getAggregationStrategy() {
105        return aggregationStrategy;
106    }
107
108    public boolean isAggregateOnException() {
109        return aggregateOnException;
110    }
111
112    public void setAggregateOnException(boolean aggregateOnException) {
113        this.aggregateOnException = aggregateOnException;
114    }
115
116    public boolean isShareUnitOfWork() {
117        return shareUnitOfWork;
118    }
119
120    public void setShareUnitOfWork(boolean shareUnitOfWork) {
121        this.shareUnitOfWork = shareUnitOfWork;
122    }
123
124    public int getCacheSize() {
125        return cacheSize;
126    }
127
128    public void setCacheSize(int cacheSize) {
129        this.cacheSize = cacheSize;
130    }
131
132    public boolean isIgnoreInvalidEndpoint() {
133        return ignoreInvalidEndpoint;
134    }
135
136    public void setIgnoreInvalidEndpoint(boolean ignoreInvalidEndpoint) {
137        this.ignoreInvalidEndpoint = ignoreInvalidEndpoint;
138    }
139
140    public void process(Exchange exchange) throws Exception {
141        AsyncProcessorHelper.process(this, exchange);
142    }
143
144    /**
145     * Enriches the input data (<code>exchange</code>) by first obtaining
146     * additional data from an endpoint represented by an endpoint
147     * <code>producer</code> and second by aggregating input data and additional
148     * data. Aggregation of input data and additional data is delegated to an
149     * {@link AggregationStrategy} object set at construction time. If the
150     * message exchange with the resource endpoint fails then no aggregation
151     * will be done and the failed exchange content is copied over to the
152     * original message exchange.
153     *
154     * @param exchange input data.
155     */
156    public boolean process(final Exchange exchange, final AsyncCallback callback) {
157        // which producer to use
158        final Producer producer;
159        final Endpoint endpoint;
160
161        // use dynamic endpoint so calculate the endpoint to use
162        Object recipient = null;
163        try {
164            recipient = expression.evaluate(exchange, Object.class);
165            endpoint = resolveEndpoint(exchange, recipient);
166            // acquire the consumer from the cache
167            producer = producerCache.acquireProducer(endpoint);
168        } catch (Throwable e) {
169            if (isIgnoreInvalidEndpoint()) {
170                if (LOG.isDebugEnabled()) {
171                    LOG.debug("Endpoint uri is invalid: " + recipient + ". This exception will be ignored.", e);
172                }
173            } else {
174                exchange.setException(e);
175            }
176            callback.done(true);
177            return true;
178        }
179
180        final Exchange resourceExchange = createResourceExchange(exchange, ExchangePattern.InOut);
181        final Endpoint destination = producer.getEndpoint();
182
183        StopWatch sw = null;
184        boolean sending = EventHelper.notifyExchangeSending(exchange.getContext(), resourceExchange, destination);
185        if (sending) {
186            sw = new StopWatch();
187        }
188        // record timing for sending the exchange using the producer
189        final StopWatch watch = sw;
190        AsyncProcessor ap = AsyncProcessorConverterHelper.convert(producer);
191        boolean sync = ap.process(resourceExchange, new AsyncCallback() {
192            public void done(boolean doneSync) {
193                // we only have to handle async completion of the routing slip
194                if (doneSync) {
195                    return;
196                }
197
198                // emit event that the exchange was sent to the endpoint
199                if (watch != null) {
200                    long timeTaken = watch.taken();
201                    EventHelper.notifyExchangeSent(resourceExchange.getContext(), resourceExchange, destination, timeTaken);
202                }
203                
204                if (!isAggregateOnException() && resourceExchange.isFailed()) {
205                    // copy resource exchange onto original exchange (preserving pattern)
206                    copyResultsPreservePattern(exchange, resourceExchange);
207                } else {
208                    prepareResult(exchange);
209                    try {
210                        // prepare the exchanges for aggregation
211                        ExchangeHelper.prepareAggregation(exchange, resourceExchange);
212
213                        Exchange aggregatedExchange = aggregationStrategy.aggregate(exchange, resourceExchange);
214                        if (aggregatedExchange != null) {
215                            // copy aggregation result onto original exchange (preserving pattern)
216                            copyResultsPreservePattern(exchange, aggregatedExchange);
217                        }
218                    } catch (Throwable e) {
219                        // if the aggregationStrategy threw an exception, set it on the original exchange
220                        exchange.setException(new CamelExchangeException("Error occurred during aggregation", exchange, e));
221                        callback.done(false);
222                        // we failed so break out now
223                        return;
224                    }
225                }
226
227                // set property with the uri of the endpoint enriched so we can use that for tracing etc
228                exchange.setProperty(Exchange.TO_ENDPOINT, producer.getEndpoint().getEndpointUri());
229
230                // return the producer back to the cache
231                try {
232                    producerCache.releaseProducer(endpoint, producer);
233                } catch (Exception e) {
234                    // ignore
235                }
236
237                callback.done(false);
238            }
239        });
240
241        if (!sync) {
242            LOG.trace("Processing exchangeId: {} is continued being processed asynchronously", exchange.getExchangeId());
243            // the remainder of the routing slip will be completed async
244            // so we break out now, then the callback will be invoked which then continue routing from where we left here
245            return false;
246        }
247
248        LOG.trace("Processing exchangeId: {} is continued being processed synchronously", exchange.getExchangeId());
249
250        if (watch != null) {
251            // emit event that the exchange was sent to the endpoint
252            long timeTaken = watch.taken();
253            EventHelper.notifyExchangeSent(resourceExchange.getContext(), resourceExchange, destination, timeTaken);
254        }
255        
256        if (!isAggregateOnException() && resourceExchange.isFailed()) {
257            // copy resource exchange onto original exchange (preserving pattern)
258            copyResultsPreservePattern(exchange, resourceExchange);
259        } else {
260            prepareResult(exchange);
261
262            try {
263                // prepare the exchanges for aggregation
264                ExchangeHelper.prepareAggregation(exchange, resourceExchange);
265
266                Exchange aggregatedExchange = aggregationStrategy.aggregate(exchange, resourceExchange);
267                if (aggregatedExchange != null) {
268                    // copy aggregation result onto original exchange (preserving pattern)
269                    copyResultsPreservePattern(exchange, aggregatedExchange);
270                }
271            } catch (Throwable e) {
272                // if the aggregationStrategy threw an exception, set it on the original exchange
273                exchange.setException(new CamelExchangeException("Error occurred during aggregation", exchange, e));
274                callback.done(true);
275                // we failed so break out now
276                return true;
277            }
278        }
279
280        // set property with the uri of the endpoint enriched so we can use that for tracing etc
281        exchange.setProperty(Exchange.TO_ENDPOINT, producer.getEndpoint().getEndpointUri());
282
283        // return the producer back to the cache
284        try {
285            producerCache.releaseProducer(endpoint, producer);
286        } catch (Exception e) {
287            // ignore
288        }
289
290        callback.done(true);
291        return true;
292    }
293
294    protected Endpoint resolveEndpoint(Exchange exchange, Object recipient) {
295        // trim strings as end users might have added spaces between separators
296        if (recipient instanceof String) {
297            recipient = ((String)recipient).trim();
298        }
299        return ExchangeHelper.resolveEndpoint(exchange, recipient);
300    }
301
302    /**
303     * Creates a new {@link DefaultExchange} instance from the given
304     * <code>exchange</code>. The resulting exchange's pattern is defined by
305     * <code>pattern</code>.
306     *
307     * @param source  exchange to copy from.
308     * @param pattern exchange pattern to set.
309     * @return created exchange.
310     */
311    protected Exchange createResourceExchange(Exchange source, ExchangePattern pattern) {
312        // copy exchange, and do not share the unit of work
313        Exchange target = ExchangeHelper.createCorrelatedCopy(source, false);
314        target.setPattern(pattern);
315
316        // if we share unit of work, we need to prepare the resource exchange
317        if (isShareUnitOfWork()) {
318            target.setProperty(Exchange.PARENT_UNIT_OF_WORK, source.getUnitOfWork());
319            // and then share the unit of work
320            target.setUnitOfWork(source.getUnitOfWork());
321        }
322        return target;
323    }
324
325    private static void prepareResult(Exchange exchange) {
326        if (exchange.getPattern().isOutCapable()) {
327            exchange.getOut().copyFrom(exchange.getIn());
328        }
329    }
330
331    private static AggregationStrategy defaultAggregationStrategy() {
332        return new CopyAggregationStrategy();
333    }
334
335    @Override
336    public String toString() {
337        return "Enrich[" + expression + "]";
338    }
339
340    protected void doStart() throws Exception {
341        if (aggregationStrategy == null) {
342            aggregationStrategy = defaultAggregationStrategy();
343        }
344        if (aggregationStrategy instanceof CamelContextAware) {
345            ((CamelContextAware) aggregationStrategy).setCamelContext(camelContext);
346        }
347
348        if (producerCache == null) {
349            if (cacheSize < 0) {
350                producerCache = new EmptyProducerCache(this, camelContext);
351                LOG.debug("Enricher {} is not using ProducerCache", this);
352            } else if (cacheSize == 0) {
353                producerCache = new ProducerCache(this, camelContext);
354                LOG.debug("Enricher {} using ProducerCache with default cache size", this);
355            } else {
356                producerCache = new ProducerCache(this, camelContext, cacheSize);
357                LOG.debug("Enricher {} using ProducerCache with cacheSize={}", this, cacheSize);
358            }
359        }
360
361        ServiceHelper.startServices(producerCache, aggregationStrategy);
362    }
363
364    protected void doStop() throws Exception {
365        ServiceHelper.stopServices(aggregationStrategy, producerCache);
366    }
367
368    private static class CopyAggregationStrategy implements AggregationStrategy {
369
370        public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
371            if (newExchange != null) {
372                copyResultsPreservePattern(oldExchange, newExchange);
373            }
374            return oldExchange;
375        }
376
377    }
378
379}