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.processor.exceptionpolicy;
018    
019    import java.util.Iterator;
020    import java.util.LinkedHashMap;
021    import java.util.Map;
022    import java.util.Set;
023    import java.util.TreeMap;
024    
025    import org.apache.camel.Exchange;
026    import org.apache.camel.model.OnExceptionDefinition;
027    import org.apache.camel.model.ProcessorDefinitionHelper;
028    import org.apache.camel.model.RouteDefinition;
029    import org.apache.camel.util.ObjectHelper;
030    import org.slf4j.Logger;
031    import org.slf4j.LoggerFactory;
032    
033    /**
034     * The default strategy used in Camel to resolve the {@link org.apache.camel.model.OnExceptionDefinition} that should
035     * handle the thrown exception.
036     * <p/>
037     * <b>Selection strategy:</b>
038     * <br/>This strategy applies the following rules:
039     * <ul>
040     * <li>Will walk the exception hierarchy from bottom upwards till the thrown exception, meaning that the most outer caused
041     * by is selected first, ending with the thrown exception itself. The method {@link #createExceptionIterator(Throwable)}
042     * provides the Iterator used for the walking.</li>
043     * <li>The exception type must be configured with an Exception that is an instance of the thrown exception, this
044     * is tested using the {@link #filter(org.apache.camel.model.OnExceptionDefinition, Class, Throwable)} method.
045     * By default the filter uses <tt>instanceof</tt> test.</li>
046     * <li>If the exception type has <b>exactly</b> the thrown exception then its selected as its an exact match</li>
047     * <li>Otherwise the type that has an exception that is the closets super of the thrown exception is selected
048     * (recurring up the exception hierarchy)</li>
049     * </ul>
050     * <p/>
051     * <b>Fine grained matching:</b>
052     * <br/> If the {@link OnExceptionDefinition} has a when defined with an expression the type is also matches against
053     * the current exchange using the {@link #matchesWhen(org.apache.camel.model.OnExceptionDefinition, org.apache.camel.Exchange)}
054     * method. This can be used to for more fine grained matching, so you can e.g. define multiple sets of
055     * exception types with the same exception class(es) but have a predicate attached to select which to select at runtime.
056     */
057    public class DefaultExceptionPolicyStrategy implements ExceptionPolicyStrategy {
058    
059        private static final Logger LOG = LoggerFactory.getLogger(DefaultExceptionPolicyStrategy.class);
060    
061        public OnExceptionDefinition getExceptionPolicy(Map<ExceptionPolicyKey, OnExceptionDefinition> exceptionPolicies,
062                                                        Exchange exchange, Throwable exception) {
063    
064            Map<Integer, OnExceptionDefinition> candidates = new TreeMap<Integer, OnExceptionDefinition>();
065            Map<ExceptionPolicyKey, OnExceptionDefinition> routeScoped = new LinkedHashMap<ExceptionPolicyKey, OnExceptionDefinition>();
066            Map<ExceptionPolicyKey, OnExceptionDefinition> contextScoped = new LinkedHashMap<ExceptionPolicyKey, OnExceptionDefinition>();
067    
068            // split policies into route and context scoped
069            initRouteAndContextScopedExceptionPolicies(exceptionPolicies, routeScoped, contextScoped);
070    
071            // at first check route scoped as we prefer them over context scoped
072            // recursive up the tree using the iterator
073            boolean exactMatch = false;
074            Iterator<Throwable> it = createExceptionIterator(exception);
075            while (!exactMatch && it.hasNext()) {
076                // we should stop looking if we have found an exact match
077                exactMatch = findMatchedExceptionPolicy(routeScoped, exchange, it.next(), candidates);
078            }
079    
080            // fallback to check context scoped (only do this if there was no exact match)
081            it = createExceptionIterator(exception);
082            while (!exactMatch && it.hasNext()) {
083                // we should stop looking if we have found an exact match
084                exactMatch = findMatchedExceptionPolicy(contextScoped, exchange, it.next(), candidates);
085            }
086    
087            // now go through the candidates and find the best
088            LOG.trace("Found {} candidates", candidates.size());
089    
090            if (candidates.isEmpty()) {
091                // no type found
092                return null;
093            } else {
094                // return the first in the map as its sorted and we checked route scoped first, which we prefer
095                return candidates.values().iterator().next();
096            }
097        }
098    
099        private void initRouteAndContextScopedExceptionPolicies(Map<ExceptionPolicyKey, OnExceptionDefinition> exceptionPolicies,
100                                                                Map<ExceptionPolicyKey, OnExceptionDefinition> routeScoped,
101                                                                Map<ExceptionPolicyKey, OnExceptionDefinition> contextScoped) {
102    
103            // loop through all the entries and split into route and context scoped
104            Set<Map.Entry<ExceptionPolicyKey, OnExceptionDefinition>> entries = exceptionPolicies.entrySet();
105            for (Map.Entry<ExceptionPolicyKey, OnExceptionDefinition> entry : entries) {
106                if (entry.getKey().getRouteId() != null) {
107                    routeScoped.put(entry.getKey(), entry.getValue());
108                } else {
109                    contextScoped.put(entry.getKey(), entry.getValue());
110                }
111            }
112        }
113    
114    
115        private boolean findMatchedExceptionPolicy(Map<ExceptionPolicyKey, OnExceptionDefinition> exceptionPolicies,
116                                                   Exchange exchange, Throwable exception,
117                                                   Map<Integer, OnExceptionDefinition> candidates) {
118            if (LOG.isTraceEnabled()) {
119                LOG.trace("Finding best suited exception policy for thrown exception {}", exception.getClass().getName());
120            }
121    
122            // the goal is to find the exception with the same/closet inheritance level as the target exception being thrown
123            int targetLevel = getInheritanceLevel(exception.getClass());
124            // candidate is the best candidate found so far to return
125            OnExceptionDefinition candidate = null;
126            // difference in inheritance level between the current candidate and the thrown exception (target level)
127            int candidateDiff = Integer.MAX_VALUE;
128    
129            // loop through all the entries and find the best candidates to use
130            Set<Map.Entry<ExceptionPolicyKey, OnExceptionDefinition>> entries = exceptionPolicies.entrySet();
131            for (Map.Entry<ExceptionPolicyKey, OnExceptionDefinition> entry : entries) {
132                Class<?> clazz = entry.getKey().getExceptionClass();
133                OnExceptionDefinition type = entry.getValue();
134    
135                // if OnException is route scoped then the current route (Exchange) must match
136                // so we will not pick an OnException from another route
137                if (exchange != null && exchange.getUnitOfWork() != null && type.isRouteScoped()) {
138                    RouteDefinition route = exchange.getUnitOfWork().getRouteContext() != null ? exchange.getUnitOfWork().getRouteContext().getRoute() : null;
139                    RouteDefinition typeRoute = ProcessorDefinitionHelper.getRoute(type);
140                    if (route != null && typeRoute != null && route != typeRoute) {
141                        if (LOG.isTraceEnabled()) {
142                            LOG.trace("The type is scoped for route: {} however Exchange is at route: {}", typeRoute.getId(), route.getId());
143                        }
144                        continue;
145                    }
146                }
147    
148                if (filter(type, clazz, exception)) {
149    
150                    // must match
151                    if (!matchesWhen(type, exchange)) {
152                        LOG.trace("The type did not match when: {}", type);
153                        continue;
154                    }
155    
156                    // exact match then break
157                    if (clazz.equals(exception.getClass())) {
158                        candidate = type;
159                        candidateDiff = 0;
160                        break;
161                    }
162    
163                    // not an exact match so find the best candidate
164                    int level = getInheritanceLevel(clazz);
165                    int diff = targetLevel - level;
166    
167                    if (diff < candidateDiff) {
168                        // replace with a much better candidate
169                        candidate = type;
170                        candidateDiff = diff;
171                    }
172                }
173            }
174    
175            if (candidate != null) {
176                if (!candidates.containsKey(candidateDiff)) {
177                    // only add as candidate if we do not already have it registered with that level
178                    LOG.trace("Adding {} as candidate at level {}", candidate, candidateDiff);
179                    candidates.put(candidateDiff, candidate);
180                } else {
181                    // we have an existing candidate already which we should prefer to use
182                    // for example we check route scope before context scope (preferring route scopes)
183                    if (LOG.isTraceEnabled()) {
184                        LOG.trace("Existing candidate {} takes precedence over{} at level {}",
185                                new Object[]{candidates.get(candidateDiff), candidate, candidateDiff});
186                    }
187                }
188            }
189    
190            // if we found a exact match then we should stop continue looking
191            boolean exactMatch = candidateDiff == 0;
192            if (LOG.isTraceEnabled() && exactMatch) {
193                LOG.trace("Exact match found for candidate: {}", candidate);
194            }
195            return exactMatch;
196        }
197    
198        /**
199         * Strategy to filter the given type exception class with the thrown exception
200         *
201         * @param type           the exception type
202         * @param exceptionClass the current exception class for testing
203         * @param exception      the thrown exception
204         * @return <tt>true</tt> if the to current exception class is a candidate, <tt>false</tt> to skip it.
205         */
206        protected boolean filter(OnExceptionDefinition type, Class<?> exceptionClass, Throwable exception) {
207            // must be instance of check to ensure that the exceptionClass is one type of the thrown exception
208            return exceptionClass.isInstance(exception);
209        }
210    
211        /**
212         * Strategy method for matching the exception type with the current exchange.
213         * <p/>
214         * This default implementation will match as:
215         * <ul>
216         * <li>Always true if no when predicate on the exception type
217         * <li>Otherwise the when predicate is matches against the current exchange
218         * </ul>
219         *
220         * @param definition     the exception definition
221         * @param exchange the current {@link Exchange}
222         * @return <tt>true</tt> if matched, <tt>false</tt> otherwise.
223         */
224        protected boolean matchesWhen(OnExceptionDefinition definition, Exchange exchange) {
225            if (definition.getOnWhen() == null || definition.getOnWhen().getExpression() == null) {
226                // if no predicate then it's always a match
227                return true;
228            }
229            return definition.getOnWhen().getExpression().matches(exchange);
230        }
231    
232        /**
233         * Strategy method creating the iterator to walk the exception in the order Camel should use
234         * for find the {@link OnExceptionDefinition} should be used.
235         * <p/>
236         * The default iterator will walk from the bottom upwards
237         * (the last caused by going upwards to the exception)
238         *
239         * @param exception  the exception
240         * @return the iterator
241         */
242        protected Iterator<Throwable> createExceptionIterator(Throwable exception) {
243            return ObjectHelper.createExceptionIterator(exception);
244        }
245    
246        private static int getInheritanceLevel(Class<?> clazz) {
247            if (clazz == null || "java.lang.Object".equals(clazz.getName())) {
248                return 0;
249            }
250            return 1 + getInheritanceLevel(clazz.getSuperclass());
251        }
252    }