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 transient 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 }