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 java.util.Stack;
020
021import org.apache.camel.AsyncCallback;
022import org.apache.camel.Exchange;
023import org.apache.camel.Processor;
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027import static org.apache.camel.builder.ExpressionBuilder.routeIdExpression;
028
029/**
030 * An {@link org.apache.camel.processor.ErrorHandler} used as a safe fallback when
031 * processing by other error handlers such as the {@link org.apache.camel.model.OnExceptionDefinition}.
032 * <p/>
033 * This error handler is used as a fail-safe to ensure that error handling does not run in endless recursive looping
034 * which potentially can happen if a new exception is thrown while error handling a previous exception which then
035 * cause new error handling to process and this then keep on failing with new exceptions in an endless loop.
036 *
037 * @version
038 */
039public class FatalFallbackErrorHandler extends DelegateAsyncProcessor implements ErrorHandler {
040
041    private static final Logger LOG = LoggerFactory.getLogger(FatalFallbackErrorHandler.class);
042
043    private boolean deadLetterChannel;
044
045    public FatalFallbackErrorHandler(Processor processor) {
046        this(processor, false);
047    }
048
049    public FatalFallbackErrorHandler(Processor processor, boolean isDeadLetterChannel) {
050        super(processor);
051        this.deadLetterChannel = isDeadLetterChannel;
052    }
053
054    @Override
055    @SuppressWarnings("unchecked")
056    public boolean process(final Exchange exchange, final AsyncCallback callback) {
057        // get the current route id we use
058        final String id = routeIdExpression().evaluate(exchange, String.class);
059
060        // prevent endless looping if we end up coming back to ourself
061        Stack<String> fatals = exchange.getProperty(Exchange.FATAL_FALLBACK_ERROR_HANDLER, null, Stack.class);
062        if (fatals == null) {
063            fatals = new Stack<>();
064            exchange.setProperty(Exchange.FATAL_FALLBACK_ERROR_HANDLER, fatals);
065        }
066        if (fatals.search(id) > -1) {
067            LOG.warn("Circular error-handler detected at route: {} - breaking out processing Exchange: {}", id, exchange);
068            // mark this exchange as already been error handler handled (just by having this property)
069            // the false value mean the caught exception will be kept on the exchange, causing the
070            // exception to be propagated back to the caller, and to break out routing
071            exchange.setProperty(Exchange.ERRORHANDLER_HANDLED, false);
072            exchange.setProperty(Exchange.ERRORHANDLER_CIRCUIT_DETECTED, true);
073            callback.done(true);
074            return true;
075        }
076
077        // okay we run under this fatal error handler now
078        fatals.push(id);
079
080        // support the asynchronous routing engine
081        boolean sync = processor.process(exchange, new AsyncCallback() {
082            public void done(boolean doneSync) {
083                try {
084                    if (exchange.getException() != null) {
085                        // an exception occurred during processing onException
086
087                        // log detailed error message with as much detail as possible
088                        Throwable previous = exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Throwable.class);
089
090                        // check if previous and this exception are set as the same exception
091                        // which happens when using global scoped onException and you call a direct route that causes the 2nd exception
092                        // then we need to find the original previous exception as the suppressed exception
093                        if (previous != null && previous == exchange.getException()) {
094                            previous = null;
095                            // maybe previous was suppressed?
096                            if (exchange.getException().getSuppressed().length > 0) {
097                                previous = exchange.getException().getSuppressed()[0];
098                            }
099                        }
100
101                        String msg = "Exception occurred while trying to handle previously thrown exception on exchangeId: "
102                            + exchange.getExchangeId() + " using: [" + processor + "].";
103                        if (previous != null) {
104                            msg += " The previous and the new exception will be logged in the following.";
105                            log(msg);
106                            log("\\--> Previous exception on exchangeId: " + exchange.getExchangeId(), previous);
107                            log("\\--> New exception on exchangeId: " + exchange.getExchangeId(), exchange.getException());
108                        } else {
109                            log(msg);
110                            log("\\--> New exception on exchangeId: " + exchange.getExchangeId(), exchange.getException());
111                        }
112
113                        // add previous as suppressed to exception if not already there
114                        if (previous != null) {
115                            Throwable[] suppressed = exchange.getException().getSuppressed();
116                            boolean found = false;
117                            for (Throwable t : suppressed) {
118                                if (t == previous) {
119                                    found = true;
120                                }
121                            }
122                            if (!found) {
123                                exchange.getException().addSuppressed(previous);
124                            }
125                        }
126
127                        // we can propagated that exception to the caught property on the exchange
128                        // which will shadow any previously caught exception and cause this new exception
129                        // to be visible in the error handler
130                        exchange.setProperty(Exchange.EXCEPTION_CAUGHT, exchange.getException());
131
132                        if (deadLetterChannel) {
133                            // special for dead letter channel as we want to let it determine what to do, depending how
134                            // it has been configured
135                            exchange.removeProperty(Exchange.ERRORHANDLER_HANDLED);
136                        } else {
137                            // mark this exchange as already been error handler handled (just by having this property)
138                            // the false value mean the caught exception will be kept on the exchange, causing the
139                            // exception to be propagated back to the caller, and to break out routing
140                            exchange.setProperty(Exchange.ERRORHANDLER_HANDLED, false);
141                        }
142                    }
143                } finally {
144                    // no longer running under this fatal fallback error handler
145                    Stack<String> fatals = exchange.getProperty(Exchange.FATAL_FALLBACK_ERROR_HANDLER, null, Stack.class);
146                    if (fatals != null) {
147                        fatals.remove(id);
148                    }
149                    callback.done(doneSync);
150                }
151            }
152        });
153
154        return sync;
155    }
156
157    private void log(String message) {
158        log(message, null);
159    }
160
161    private void log(String message, Throwable t) {
162        // when using dead letter channel we only want to log at WARN level
163        if (deadLetterChannel) {
164            if (t != null) {
165                LOG.warn(message, t);
166            } else {
167                LOG.warn(message);
168            }
169        } else {
170            if (t != null) {
171                LOG.error(message, t);
172            } else {
173                LOG.error(message);
174            }
175        }
176    }
177
178    @Override
179    public String toString() {
180        return "FatalFallbackErrorHandler[" + processor + "]";
181    }
182}