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.spring.spi;
018    
019    import java.util.concurrent.ScheduledExecutorService;
020    
021    import org.apache.camel.AsyncCallback;
022    import org.apache.camel.CamelContext;
023    import org.apache.camel.Exchange;
024    import org.apache.camel.LoggingLevel;
025    import org.apache.camel.Predicate;
026    import org.apache.camel.Processor;
027    import org.apache.camel.processor.RedeliveryErrorHandler;
028    import org.apache.camel.processor.RedeliveryPolicy;
029    import org.apache.camel.processor.exceptionpolicy.ExceptionPolicyStrategy;
030    import org.apache.camel.util.CamelLogger;
031    import org.apache.camel.util.ExchangeHelper;
032    import org.apache.camel.util.ObjectHelper;
033    import org.springframework.transaction.TransactionDefinition;
034    import org.springframework.transaction.TransactionStatus;
035    import org.springframework.transaction.support.TransactionCallbackWithoutResult;
036    import org.springframework.transaction.support.TransactionTemplate;
037    
038    /**
039     * The <a href="http://camel.apache.org/transactional-client.html">Transactional Client</a>
040     * EIP pattern.
041     *
042     * @version 
043     */
044    public class TransactionErrorHandler extends RedeliveryErrorHandler {
045    
046        private final TransactionTemplate transactionTemplate;
047        private final String transactionKey;
048        private final LoggingLevel rollbackLoggingLevel;
049    
050        /**
051         * Creates the transaction error handler.
052         *
053         * @param camelContext            the camel context
054         * @param output                  outer processor that should use this default error handler
055         * @param logger                  logger to use for logging failures and redelivery attempts
056         * @param redeliveryProcessor     an optional processor to run before redelivery attempt
057         * @param redeliveryPolicy        policy for redelivery
058         * @param exceptionPolicyStrategy strategy for onException handling
059         * @param transactionTemplate     the transaction template
060         * @param retryWhile              retry while
061         * @param executorService         the {@link java.util.concurrent.ScheduledExecutorService} to be used for redelivery thread pool. Can be <tt>null</tt>.
062         * @param rollbackLoggingLevel    logging level to use for logging transaction rollback occurred
063         */
064        public TransactionErrorHandler(CamelContext camelContext, Processor output, CamelLogger logger, 
065                Processor redeliveryProcessor, RedeliveryPolicy redeliveryPolicy, ExceptionPolicyStrategy exceptionPolicyStrategy,
066                TransactionTemplate transactionTemplate, Predicate retryWhile, ScheduledExecutorService executorService,
067                LoggingLevel rollbackLoggingLevel) {
068    
069            super(camelContext, output, logger, redeliveryProcessor, redeliveryPolicy, null, null, false, retryWhile, executorService);
070            setExceptionPolicy(exceptionPolicyStrategy);
071            this.transactionTemplate = transactionTemplate;
072            this.rollbackLoggingLevel = rollbackLoggingLevel;
073            this.transactionKey = ObjectHelper.getIdentityHashCode(transactionTemplate);
074        }
075    
076        public boolean supportTransacted() {
077            return true;
078        }
079    
080        @Override
081        public String toString() {
082            if (output == null) {
083                // if no output then don't do any description
084                return "";
085            }
086            return "TransactionErrorHandler:"
087                    + propagationBehaviorToString(transactionTemplate.getPropagationBehavior())
088                    + "[" + getOutput() + "]";
089        }
090    
091        @Override
092        public void process(Exchange exchange) throws Exception {
093            // we have to run this synchronously as Spring Transaction does *not* support
094            // using multiple threads to span a transaction
095            if (exchange.getUnitOfWork().isTransactedBy(transactionKey)) {
096                // already transacted by this transaction template
097                // so lets just let the error handler process it
098                processByErrorHandler(exchange);
099            } else {
100                // not yet wrapped in transaction so lets do that
101                // and then have it invoke the error handler from within that transaction
102                processInTransaction(exchange);
103            }
104        }
105    
106        @Override
107        public boolean process(Exchange exchange, AsyncCallback callback) {
108            // invoke ths synchronous method as Spring Transaction does *not* support
109            // using multiple threads to span a transaction
110            try {
111                process(exchange);
112            } catch (Throwable e) {
113                exchange.setException(e);
114            }
115    
116            // notify callback we are done synchronously
117            callback.done(true);
118            return true;
119        }
120    
121        protected void processInTransaction(final Exchange exchange) throws Exception {
122            // is the exchange redelivered, for example JMS brokers support such details
123            Boolean externalRedelivered = exchange.isExternalRedelivered();
124            final String redelivered = externalRedelivered != null ? externalRedelivered.toString() : "unknown";
125            final String ids = ExchangeHelper.logIds(exchange);
126    
127            try {
128                // mark the beginning of this transaction boundary
129                exchange.getUnitOfWork().beginTransactedBy(transactionKey);
130    
131                // do in transaction
132                logTransactionBegin(redelivered, ids);
133                doInTransactionTemplate(exchange);
134                logTransactionCommit(redelivered, ids);
135    
136            } catch (TransactionRollbackException e) {
137                // do not set as exception, as its just a dummy exception to force spring TX to rollback
138                logTransactionRollback(redelivered, ids, null, true);
139            } catch (Throwable e) {
140                exchange.setException(e);
141                logTransactionRollback(redelivered, ids, e, false);
142            } finally {
143                // mark the end of this transaction boundary
144                exchange.getUnitOfWork().endTransactedBy(transactionKey);
145            }
146    
147            // if it was a local rollback only then remove its marker so outer transaction wont see the marker
148            Boolean onlyLast = (Boolean) exchange.removeProperty(Exchange.ROLLBACK_ONLY_LAST);
149            if (onlyLast != null && onlyLast) {
150                // we only want this logged at debug level
151                if (log.isDebugEnabled()) {
152                    // log exception if there was a cause exception so we have the stack trace
153                    Exception cause = exchange.getException();
154                    if (cause != null) {
155                        log.debug("Transaction rollback (" + transactionKey + ") redelivered(" + redelivered + ") for "
156                            + ids + " due exchange was marked for rollbackOnlyLast and caught: ", cause);
157                    } else {
158                        log.debug("Transaction rollback ({}) redelivered({}) for {} "
159                                + "due exchange was marked for rollbackOnlyLast", new Object[]{transactionKey, redelivered, ids});
160                    }
161                }
162                // remove caused exception due we was marked as rollback only last
163                // so by removing the exception, any outer transaction will not be affected
164                exchange.setException(null);
165            }
166        }
167    
168        protected void doInTransactionTemplate(final Exchange exchange) {
169    
170            // spring transaction template is working best with rollback if you throw it a runtime exception
171            // otherwise it may not rollback messages send to JMS queues etc.
172    
173            transactionTemplate.execute(new TransactionCallbackWithoutResult() {
174                protected void doInTransactionWithoutResult(TransactionStatus status) {
175                    // wrapper exception to throw if the exchange failed
176                    // IMPORTANT: Must be a runtime exception to let Spring regard it as to do "rollback"
177                    RuntimeException rce;
178    
179                    // and now let process the exchange by the error handler
180                    processByErrorHandler(exchange);
181    
182                    // after handling and still an exception or marked as rollback only then rollback
183                    if (exchange.getException() != null || exchange.isRollbackOnly()) {
184    
185                        // wrap exception in transacted exception
186                        if (exchange.getException() != null) {
187                            rce = ObjectHelper.wrapRuntimeCamelException(exchange.getException());
188                        } else {
189                            // create dummy exception to force spring transaction manager to rollback
190                            rce = new TransactionRollbackException();
191                        }
192    
193                        if (!status.isRollbackOnly()) {
194                            status.setRollbackOnly();
195                        }
196    
197                        // throw runtime exception to force rollback (which works best to rollback with Spring transaction manager)
198                        throw rce;
199                    }
200                }
201            });
202        }
203    
204        /**
205         * Processes the {@link Exchange} using the error handler.
206         * <p/>
207         * This implementation will invoke ensure this occurs synchronously, that means if the async routing engine
208         * did kick in, then this implementation will wait for the task to complete before it continues.
209         *
210         * @param exchange the exchange
211         */
212        protected void processByErrorHandler(final Exchange exchange) {
213            // must invoke the async method with empty callback to have it invoke the
214            // super.processErrorHandler
215            // we are transacted so we have to route synchronously so don't worry about returned
216            // value from the process method
217            // and the camel routing engine will detect this is an transacted Exchange and route
218            // it fully synchronously so we don't have to wait here if we hit an async endpoint
219            // all that is taken care of in the camel-core
220            super.process(exchange, new AsyncCallback() {
221                public void done(boolean doneSync) {
222                    // noop
223                }
224            });
225        }
226    
227        /**
228         * Logs the transaction begin
229         */
230        private void logTransactionBegin(String redelivered, String ids) {
231            if (log.isDebugEnabled()) {
232                log.debug("Transaction begin ({}) redelivered({}) for {})", new Object[]{transactionKey, redelivered, ids});
233            }
234        }
235    
236        /**
237         * Logs the transaction commit
238         */
239        private void logTransactionCommit(String redelivered, String ids) {
240            if ("true".equals(redelivered)) {
241                // okay its a redelivered message so log at INFO level if rollbackLoggingLevel is INFO or higher
242                // this allows people to know that the redelivered message was committed this time
243                if (rollbackLoggingLevel == LoggingLevel.INFO || rollbackLoggingLevel == LoggingLevel.WARN || rollbackLoggingLevel == LoggingLevel.ERROR) {
244                    log.info("Transaction commit ({}) redelivered({}) for {})", new Object[]{transactionKey, redelivered, ids});
245                    // return after we have logged
246                    return;
247                }
248            }
249    
250            // log non redelivered by default at DEBUG level
251            log.debug("Transaction commit ({}) redelivered({}) for {})", new Object[]{transactionKey, redelivered, ids});
252        }
253    
254        /**
255         * Logs the transaction rollback.
256         */
257        private void logTransactionRollback(String redelivered, String ids, Throwable e, boolean rollbackOnly) {
258            if (rollbackLoggingLevel == LoggingLevel.OFF) {
259                return;
260            } else if (rollbackLoggingLevel == LoggingLevel.ERROR && log.isErrorEnabled()) {
261                if (rollbackOnly) {
262                    log.error("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly", new Object[]{transactionKey, redelivered, ids});
263                } else {
264                    log.error("Transaction rollback ({}) redelivered({}) for {} caught: {}", new Object[]{transactionKey, redelivered, ids, e.getMessage()});
265                }
266            } else if (rollbackLoggingLevel == LoggingLevel.WARN && log.isWarnEnabled()) {
267                if (rollbackOnly) {
268                    log.warn("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly", new Object[]{transactionKey, redelivered, ids});
269                } else {
270                    log.warn("Transaction rollback ({}) redelivered({}) for {} caught: {}", new Object[]{transactionKey, redelivered, ids, e.getMessage()});
271                }
272            } else if (rollbackLoggingLevel == LoggingLevel.INFO && log.isInfoEnabled()) {
273                if (rollbackOnly) {
274                    log.info("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly", new Object[]{transactionKey, redelivered, ids});
275                } else {
276                    log.info("Transaction rollback ({}) redelivered({}) for {} caught: {}", new Object[]{transactionKey, redelivered, ids, e.getMessage()});
277                }
278            } else if (rollbackLoggingLevel == LoggingLevel.DEBUG && log.isDebugEnabled()) {
279                if (rollbackOnly) {
280                    log.debug("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly", new Object[]{transactionKey, redelivered, ids});
281                } else {
282                    log.debug("Transaction rollback ({}) redelivered({}) for {} caught: {}", new Object[]{transactionKey, redelivered, ids, e.getMessage()});
283                }
284            } else if (rollbackLoggingLevel == LoggingLevel.TRACE && log.isTraceEnabled()) {
285                if (rollbackOnly) {
286                    log.trace("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly", new Object[]{transactionKey, redelivered, ids});
287                } else {
288                    log.trace("Transaction rollback ({}) redelivered({}) for {} caught: {}", new Object[]{transactionKey, redelivered, ids, e.getMessage()});
289                }
290            }
291        }
292    
293        private static String propagationBehaviorToString(int propagationBehavior) {
294            String rc;
295            switch (propagationBehavior) {
296            case TransactionDefinition.PROPAGATION_MANDATORY:
297                rc = "PROPAGATION_MANDATORY";
298                break;
299            case TransactionDefinition.PROPAGATION_NESTED:
300                rc = "PROPAGATION_NESTED";
301                break;
302            case TransactionDefinition.PROPAGATION_NEVER:
303                rc = "PROPAGATION_NEVER";
304                break;
305            case TransactionDefinition.PROPAGATION_NOT_SUPPORTED:
306                rc = "PROPAGATION_NOT_SUPPORTED";
307                break;
308            case TransactionDefinition.PROPAGATION_REQUIRED:
309                rc = "PROPAGATION_REQUIRED";
310                break;
311            case TransactionDefinition.PROPAGATION_REQUIRES_NEW:
312                rc = "PROPAGATION_REQUIRES_NEW";
313                break;
314            case TransactionDefinition.PROPAGATION_SUPPORTS:
315                rc = "PROPAGATION_SUPPORTS";
316                break;
317            default:
318                rc = "UNKNOWN";
319            }
320            return rc;
321        }
322    
323    }