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