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.impl;
018
019import java.util.ArrayDeque;
020import java.util.ArrayList;
021import java.util.Date;
022import java.util.Deque;
023import java.util.Iterator;
024import java.util.LinkedHashSet;
025import java.util.List;
026import java.util.NoSuchElementException;
027import java.util.Set;
028import java.util.function.Predicate;
029
030import org.apache.camel.AsyncCallback;
031import org.apache.camel.CamelContext;
032import org.apache.camel.CamelUnitOfWorkException;
033import org.apache.camel.Exchange;
034import org.apache.camel.Message;
035import org.apache.camel.Processor;
036import org.apache.camel.Route;
037import org.apache.camel.Service;
038import org.apache.camel.spi.RouteContext;
039import org.apache.camel.spi.SubUnitOfWork;
040import org.apache.camel.spi.SubUnitOfWorkCallback;
041import org.apache.camel.spi.Synchronization;
042import org.apache.camel.spi.SynchronizationVetoable;
043import org.apache.camel.spi.TracedRouteNodes;
044import org.apache.camel.spi.UnitOfWork;
045import org.apache.camel.util.EventHelper;
046import org.apache.camel.util.UnitOfWorkHelper;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050/**
051 * The default implementation of {@link org.apache.camel.spi.UnitOfWork}
052 */
053public class DefaultUnitOfWork implements UnitOfWork, Service {
054    private static final Logger LOG = LoggerFactory.getLogger(DefaultUnitOfWork.class);
055
056    // TODO: This implementation seems to have transformed itself into a to broad concern
057    // where unit of work is doing a bit more work than the transactional aspect that ties
058    // to its name. Maybe this implementation should be named ExchangeContext and we can
059    // introduce a simpler UnitOfWork concept. This would also allow us to refactor the
060    // SubUnitOfWork into a general parent/child unit of work concept. However this
061    // requires API changes and thus is best kept for Camel 3.0
062
063    private UnitOfWork parent;
064    private String id;
065    private CamelContext context;
066    private List<Synchronization> synchronizations;
067    private Message originalInMessage;
068    private TracedRouteNodes tracedRouteNodes;
069    private Set<Object> transactedBy;
070    private final Deque<RouteContext> routeContextStack = new ArrayDeque<>();
071    private Deque<DefaultSubUnitOfWork> subUnitOfWorks;
072    private final transient Logger log;
073    
074    public DefaultUnitOfWork(Exchange exchange) {
075        this(exchange, LOG);
076    }
077
078    protected DefaultUnitOfWork(Exchange exchange, Logger logger) {
079        log = logger;
080        if (log.isTraceEnabled()) {
081            log.trace("UnitOfWork created for ExchangeId: {} with {}", exchange.getExchangeId(), exchange);
082        }
083
084        context = exchange.getContext();
085
086        // only use tracer if explicit enabled
087        if (context.isTracing() != null && context.isTracing()) {
088            // backwards compatible
089            tracedRouteNodes = new DefaultTracedRouteNodes();
090        }
091
092        if (context.isAllowUseOriginalMessage()) {
093            // special for JmsMessage as it can cause it to loose headers later.
094            if (exchange.getIn().getClass().getName().equals("org.apache.camel.component.jms.JmsMessage")) {
095                this.originalInMessage = new DefaultMessage(context);
096                this.originalInMessage.setBody(exchange.getIn().getBody());
097                this.originalInMessage.getHeaders().putAll(exchange.getIn().getHeaders());
098            } else {
099                this.originalInMessage = exchange.getIn().copy();
100            }
101            // must preserve exchange on the original in message
102            if (this.originalInMessage instanceof MessageSupport) {
103                ((MessageSupport) this.originalInMessage).setExchange(exchange);
104            }
105        }
106
107        // mark the creation time when this Exchange was created
108        if (exchange.getProperty(Exchange.CREATED_TIMESTAMP) == null) {
109            exchange.setProperty(Exchange.CREATED_TIMESTAMP, new Date());
110        }
111
112        // inject breadcrumb header if enabled
113        if (exchange.getContext().isUseBreadcrumb()) {
114            // create or use existing breadcrumb
115            String breadcrumbId = exchange.getIn().getHeader(Exchange.BREADCRUMB_ID, String.class);
116            if (breadcrumbId == null) {
117                // no existing breadcrumb, so create a new one based on the exchange id
118                breadcrumbId = exchange.getExchangeId();
119                exchange.getIn().setHeader(Exchange.BREADCRUMB_ID, breadcrumbId);
120            }
121        }
122        
123        // setup whether the exchange is externally redelivered or not (if not initialized before)
124        // store as property so we know that the origin exchange was redelivered
125        if (exchange.getProperty(Exchange.EXTERNAL_REDELIVERED) == null) {
126            Boolean redelivered = exchange.isExternalRedelivered();
127            if (redelivered == null) {
128                // not from a transactional resource so mark it as false by default
129                redelivered = false;
130            }
131            exchange.setProperty(Exchange.EXTERNAL_REDELIVERED, redelivered);
132        }
133
134        // fire event
135        try {
136            EventHelper.notifyExchangeCreated(exchange.getContext(), exchange);
137        } catch (Throwable e) {
138            // must catch exceptions to ensure the exchange is not failing due to notification event failed
139            log.warn("Exception occurred during event notification. This exception will be ignored.", e);
140        }
141
142        // register to inflight registry
143        if (exchange.getContext() != null) {
144            exchange.getContext().getInflightRepository().add(exchange);
145        }
146    }
147
148    UnitOfWork newInstance(Exchange exchange) {
149        return new DefaultUnitOfWork(exchange);
150    }
151
152    @Override
153    public void setParentUnitOfWork(UnitOfWork parentUnitOfWork) {
154        this.parent = parentUnitOfWork;
155    }
156
157    public UnitOfWork createChildUnitOfWork(Exchange childExchange) {
158        // create a new child unit of work, and mark me as its parent
159        UnitOfWork answer = newInstance(childExchange);
160        answer.setParentUnitOfWork(this);
161        return answer;
162    }
163
164    public void start() throws Exception {
165        id = null;
166    }
167
168    public void stop() throws Exception {
169        // need to clean up when we are stopping to not leak memory
170        if (synchronizations != null) {
171            synchronizations.clear();
172        }
173        if (tracedRouteNodes != null) {
174            tracedRouteNodes.clear();
175        }
176        if (transactedBy != null) {
177            transactedBy.clear();
178        }
179        routeContextStack.clear();
180        if (subUnitOfWorks != null) {
181            subUnitOfWorks.clear();
182        }
183        originalInMessage = null;
184        parent = null;
185        id = null;
186    }
187
188    public synchronized void addSynchronization(Synchronization synchronization) {
189        if (synchronizations == null) {
190            synchronizations = new ArrayList<>();
191        }
192        log.trace("Adding synchronization {}", synchronization);
193        synchronizations.add(synchronization);
194    }
195
196    public synchronized void removeSynchronization(Synchronization synchronization) {
197        if (synchronizations != null) {
198            synchronizations.remove(synchronization);
199        }
200    }
201
202    public synchronized boolean containsSynchronization(Synchronization synchronization) {
203        return synchronizations != null && synchronizations.contains(synchronization);
204    }
205
206    public void handoverSynchronization(Exchange target) {
207        handoverSynchronization(target, null);
208    }
209
210    @Override
211    public void handoverSynchronization(Exchange target, Predicate<Synchronization> filter) {
212        if (synchronizations == null || synchronizations.isEmpty()) {
213            return;
214        }
215
216        Iterator<Synchronization> it = synchronizations.iterator();
217        while (it.hasNext()) {
218            Synchronization synchronization = it.next();
219
220            boolean handover = true;
221            if (synchronization instanceof SynchronizationVetoable) {
222                SynchronizationVetoable veto = (SynchronizationVetoable) synchronization;
223                handover = veto.allowHandover();
224            }
225
226            if (handover && (filter == null || filter.test(synchronization))) {
227                log.trace("Handover synchronization {} to: {}", synchronization, target);
228                target.addOnCompletion(synchronization);
229                // remove it if its handed over
230                it.remove();
231            } else {
232                log.trace("Handover not allow for synchronization {}", synchronization);
233            }
234        }
235    }
236
237    public void done(Exchange exchange) {
238        log.trace("UnitOfWork done for ExchangeId: {} with {}", exchange.getExchangeId(), exchange);
239
240        boolean failed = exchange.isFailed();
241
242        // at first done the synchronizations
243        UnitOfWorkHelper.doneSynchronizations(exchange, synchronizations, log);
244
245        // notify uow callback if in use
246        try {
247            SubUnitOfWorkCallback uowCallback = getSubUnitOfWorkCallback();
248            if (uowCallback != null) {
249                uowCallback.onDone(exchange);
250            }
251        } catch (Throwable e) {
252            // must catch exceptions to ensure synchronizations is also invoked
253            log.warn("Exception occurred during savepoint onDone. This exception will be ignored.", e);
254        }
255
256        // unregister from inflight registry, before signalling we are done
257        if (exchange.getContext() != null) {
258            exchange.getContext().getInflightRepository().remove(exchange);
259        }
260
261        // then fire event to signal the exchange is done
262        try {
263            if (failed) {
264                EventHelper.notifyExchangeFailed(exchange.getContext(), exchange);
265            } else {
266                EventHelper.notifyExchangeDone(exchange.getContext(), exchange);
267            }
268        } catch (Throwable e) {
269            // must catch exceptions to ensure synchronizations is also invoked
270            log.warn("Exception occurred during event notification. This exception will be ignored.", e);
271        }
272    }
273
274    @Override
275    public void beforeRoute(Exchange exchange, Route route) {
276        if (log.isTraceEnabled()) {
277            log.trace("UnitOfWork beforeRoute: {} for ExchangeId: {} with {}", new Object[]{route.getId(), exchange.getExchangeId(), exchange});
278        }
279        UnitOfWorkHelper.beforeRouteSynchronizations(route, exchange, synchronizations, log);
280    }
281
282    @Override
283    public void afterRoute(Exchange exchange, Route route) {
284        if (log.isTraceEnabled()) {
285            log.trace("UnitOfWork afterRoute: {} for ExchangeId: {} with {}", new Object[]{route.getId(), exchange.getExchangeId(), exchange});
286        }
287        UnitOfWorkHelper.afterRouteSynchronizations(route, exchange, synchronizations, log);
288    }
289
290    public String getId() {
291        if (id == null) {
292            id = context.getUuidGenerator().generateUuid();
293        }
294        return id;
295    }
296
297    public Message getOriginalInMessage() {
298        if (originalInMessage == null && !context.isAllowUseOriginalMessage()) {
299            throw new IllegalStateException("AllowUseOriginalMessage is disabled. Cannot access the original message.");
300        }
301        return originalInMessage;
302    }
303
304    public TracedRouteNodes getTracedRouteNodes() {
305        return tracedRouteNodes;
306    }
307
308    public boolean isTransacted() {
309        return transactedBy != null && !transactedBy.isEmpty();
310    }
311
312    public boolean isTransactedBy(Object key) {
313        return getTransactedBy().contains(key);
314    }
315
316    public void beginTransactedBy(Object key) {
317        getTransactedBy().add(key);
318    }
319
320    public void endTransactedBy(Object key) {
321        getTransactedBy().remove(key);
322    }
323
324    public RouteContext getRouteContext() {
325        return routeContextStack.peek();
326    }
327
328    public void pushRouteContext(RouteContext routeContext) {
329        routeContextStack.push(routeContext);
330    }
331
332    public RouteContext popRouteContext() {
333        try {
334            return routeContextStack.pop();
335        } catch (NoSuchElementException e) {
336            // ignore and return null
337        }
338        return null;
339    }
340
341    public AsyncCallback beforeProcess(Processor processor, Exchange exchange, AsyncCallback callback) {
342        // no wrapping needed
343        return callback;
344    }
345
346    public void afterProcess(Processor processor, Exchange exchange, AsyncCallback callback, boolean doneSync) {
347    }
348
349    @Override
350    public void beginSubUnitOfWork(Exchange exchange) {
351        if (log.isTraceEnabled()) {
352            log.trace("beginSubUnitOfWork exchangeId: {}", exchange.getExchangeId());
353        }
354
355        if (subUnitOfWorks == null) {
356            subUnitOfWorks = new ArrayDeque<>();
357        }
358        subUnitOfWorks.push(new DefaultSubUnitOfWork());
359    }
360
361    @Override
362    public void endSubUnitOfWork(Exchange exchange) {
363        if (log.isTraceEnabled()) {
364            log.trace("endSubUnitOfWork exchangeId: {}", exchange.getExchangeId());
365        }
366
367        if (subUnitOfWorks == null || subUnitOfWorks.isEmpty()) {
368            return;
369        }
370
371        // pop last sub unit of work as its now ended
372        SubUnitOfWork subUoW = null;
373        try {
374            subUoW = subUnitOfWorks.pop();
375        } catch (NoSuchElementException e) {
376            // ignore
377        }
378        if (subUoW != null && subUoW.isFailed()) {
379            // the sub unit of work failed so set an exception containing all the caused exceptions
380            // and mark the exchange for rollback only
381
382            // if there are multiple exceptions then wrap those into another exception with them all
383            Exception cause;
384            List<Exception> list = subUoW.getExceptions();
385            if (list != null) {
386                if (list.size() == 1) {
387                    cause = list.get(0);
388                } else {
389                    cause = new CamelUnitOfWorkException(exchange, list);
390                }
391                exchange.setException(cause);
392            }
393            // mark it as rollback and that the unit of work is exhausted. This ensures that we do not try
394            // to redeliver this exception (again)
395            exchange.setProperty(Exchange.ROLLBACK_ONLY, true);
396            exchange.setProperty(Exchange.UNIT_OF_WORK_EXHAUSTED, true);
397            // and remove any indications of error handled which will prevent this exception to be noticed
398            // by the error handler which we want to react with the result of the sub unit of work
399            exchange.setProperty(Exchange.ERRORHANDLER_HANDLED, null);
400            exchange.setProperty(Exchange.FAILURE_HANDLED, null);
401            if (log.isTraceEnabled()) {
402                log.trace("endSubUnitOfWork exchangeId: {} with {} caused exceptions.", exchange.getExchangeId(), list != null ? list.size() : 0);
403            }
404        }
405    }
406
407    @Override
408    public SubUnitOfWorkCallback getSubUnitOfWorkCallback() {
409        // if there is a parent-child relationship between unit of works
410        // then we should use the callback strategies from the parent
411        if (parent != null) {
412            return parent.getSubUnitOfWorkCallback();
413        }
414
415        return subUnitOfWorks != null ? subUnitOfWorks.peek() : null;
416    }
417
418    private Set<Object> getTransactedBy() {
419        if (transactedBy == null) {
420            transactedBy = new LinkedHashSet<>();
421        }
422        return transactedBy;
423    }
424
425    @Override
426    public String toString() {
427        return "DefaultUnitOfWork";
428    }
429}