(){
66 | @Override
67 | protected Context initialValue() {
68 | return new Context();
69 | }
70 | };
71 | }
72 |
73 | /**
74 | * Create a new ErrorHandler with the given one as parent.
75 | *
76 | * @param parentErrorHandler the parent @{link ErrorHandler}
77 | */
78 | private ErrorHandler(ErrorHandler parentErrorHandler) {
79 | this();
80 | this.parentErrorHandler = parentErrorHandler;
81 | }
82 |
83 | /**
84 | * Create a new @{link ErrorHandler}, isolated from the default one.
85 | *
86 | * In other words, designed to handle all errors by itself without delegating
87 | * to the default error handler.
88 | *
89 | * @return returns a new {@code ErrorHandler} instance
90 | */
91 | public static ErrorHandler createIsolated() {
92 | return new ErrorHandler();
93 | }
94 |
95 | /**
96 | * Create a new @{link ErrorHandler}, that delegates to the default one.
97 | *
98 | * Any default actions, are always executed after the ones registered on this one.
99 | *
100 | * @return returns a new {@code ErrorHandler} instance
101 | */
102 | public static ErrorHandler create() {
103 | return new ErrorHandler(defaultErrorHandler());
104 | }
105 |
106 | /**
107 | * Get the default @{link ErrorHandler}, a singleton object
108 | * to which all other instances by default delegate to.
109 | *
110 | * @return the default @{link ErrorHandler} instance
111 | */
112 | public static synchronized ErrorHandler defaultErrorHandler() {
113 | if (defaultInstance == null) {
114 | defaultInstance = new ErrorHandler();
115 | }
116 | return defaultInstance;
117 | }
118 |
119 | /**
120 | * Register {@code action} to be executed by {@link #handle(Throwable)},
121 | * if the thrown error matches the {@code matcher}.
122 | *
123 | * @param matcher a matcher to match the thrown error
124 | * @param action the associated action
125 | * @return the current {@code ErrorHandler} instance - to use in command chains
126 | */
127 | public ErrorHandler on(Matcher matcher, Action action) {
128 | if (matcher == null) {
129 | throw new IllegalArgumentException("matcher cannot be null");
130 | }
131 | assertNotNullAction(action);
132 | this.actions.add(ActionEntry.from(matcher, action));
133 | return this;
134 | }
135 |
136 | /**
137 | * Register {@code action} to be executed by {@link #handle(Throwable)},
138 | * if the thrown error is an instance of {@code exceptionClass}.
139 | *
140 | * @param exceptionClass the class of the error
141 | * @param action the associated action
142 | * @return the current {@code ErrorHandler} instance - to use in command chains
143 | */
144 | public ErrorHandler on(Class extends Exception> exceptionClass, Action action) {
145 | if (exceptionClass == null) {
146 | throw new IllegalArgumentException("exceptionClass cannot be null");
147 | }
148 | assertNotNullAction(action);
149 | actions.add(ActionEntry.from(new ExceptionMatcher(exceptionClass), action));
150 | return this;
151 | }
152 |
153 | /**
154 | * Register {@code action} to be executed by {@link #handle(Throwable)},
155 | * if the thrown error is bound (associated) to {@code errorCode}.
156 | *
157 | * See {@link #bindClass(Class, MatcherFactory)} and {@link #bind(Object, MatcherFactory)}
158 | * on how to associate arbitrary error codes with actual Throwables via {@link Matcher}.
159 | *
160 | * @param the error code type
161 | * @param errorCode the error code
162 | * @param action the associated action
163 | * @return the current {@code ErrorHandler} instance - to use in command chains
164 | */
165 | public ErrorHandler on(T errorCode, Action action) {
166 | if (errorCode == null) {
167 | throw new IllegalArgumentException("errorCode cannot be null");
168 | }
169 |
170 | MatcherFactory super T> matcherFactory = getMatcherFactoryForErrorCode(errorCode);
171 | if (matcherFactory == null) {
172 | throw new UnknownErrorCodeException(errorCode);
173 | }
174 |
175 | actions.add(ActionEntry.from(matcherFactory.build(errorCode), action));
176 | return this;
177 | }
178 |
179 | /**
180 | * Register {@code action} to be executed in case no other conditional
181 | * action gets executed.
182 | *
183 | * @param action the action
184 | * @return the current {@code ErrorHandler} instance - to use in command chains
185 | */
186 | public ErrorHandler otherwise(Action action) {
187 | assertNotNullAction(action);
188 | otherwiseActions.add(action);
189 | return this;
190 | }
191 |
192 | /**
193 | * Register {@code action} to be executed on all errors.
194 | *
195 | * @param action the action
196 | * @return the current {@code ErrorHandler} instance - to use in command chains
197 | */
198 | public ErrorHandler always(Action action) {
199 | assertNotNullAction(action);
200 | alwaysActions.add(action);
201 | return this;
202 | }
203 |
204 | /**
205 | * Skip all following actions registered via an {@code on} method
206 | * @return the current {@code ErrorHandler} instance - to use in command chains
207 | */
208 | public ErrorHandler skipFollowing() {
209 | if (localContext != null) {
210 | localContext.get().skipFollowing = true;
211 | }
212 | return this;
213 | }
214 |
215 | /**
216 | * Skip all actions registered via {@link #always(Action)}
217 | * @return the current {@code ErrorHandler} instance - to use in command chains
218 | */
219 | public ErrorHandler skipAlways() {
220 | if (localContext != null) {
221 | localContext.get().skipAlways = true;
222 | }
223 | return this;
224 | }
225 |
226 | /**
227 | * Skip the default matching actions if any
228 | * @return the current {@code ErrorHandler} instance - to use in command chains
229 | */
230 | public ErrorHandler skipDefaults() {
231 | if (localContext != null) {
232 | localContext.get().skipDefaults = true;
233 | }
234 | return this;
235 | }
236 |
237 | protected void handle(Throwable error, ThreadLocal context) {
238 | if (error == null)
239 | throw new IllegalArgumentException("error to be checked can not be null");
240 |
241 | localContext = context;
242 |
243 | Context ctx = localContext.get();
244 |
245 | for (ActionEntry actionEntry : actions) {
246 | if (ctx.skipFollowing) break;
247 | if (actionEntry.matcher.matches(error)) {
248 | actionEntry.action.execute(error, this);
249 | ctx.handled = true;
250 | }
251 | }
252 |
253 | if (!ctx.handled && !otherwiseActions.isEmpty()) {
254 | for (Action action : otherwiseActions) {
255 | action.execute(error, this);
256 | ctx.handled = true;
257 | }
258 | }
259 |
260 | if (!ctx.skipAlways) {
261 | for (Action action : alwaysActions) {
262 | action.execute(error, this);
263 | ctx.handled = true;
264 | }
265 | }
266 |
267 | if (parentErrorHandler != null && !ctx.skipDefaults) {
268 | parentErrorHandler.handle(error, localContext);
269 | }
270 | }
271 |
272 | /**
273 | * Run a custom code block and assign current ErrorHandler instance
274 | * to handle a possible exception throw in 'catch'.
275 | *
276 | * @param blockExecutor functional interface containing Exception prone code
277 | */
278 | public void run(BlockExecutor blockExecutor) {
279 | try {
280 | blockExecutor.invoke();
281 | } catch (Exception exception) {
282 | handle(exception, localContext);
283 | }
284 | }
285 |
286 | /**
287 | * Handle {@code error} by executing all matching actions.
288 | *
289 | * @param error the error as a {@link Throwable}
290 | */
291 | public void handle(Throwable error) {
292 | this.handle(error, localContext);
293 | }
294 |
295 | /**
296 | * Bind an {@code errorCode} to a {@code Matcher}, using a {@code MatcherFactory}.
297 | *
298 | *
299 | * For example, when we need to catch a network timeout it's better to just write "timeout"
300 | * instead of a train-wreck expression. So we need to bind this "timeout" error code to an actual
301 | * condition that will check the actual error when it occurs to see if its a network timeout or not.
302 | *
303 | *
304 | *
305 | * {@code
306 | * ErrorHandler
307 | * .defaultErrorHandler()
308 | * .bind("timeout", errorCode -> throwable -> {
309 | * return (throwable instanceof SocketTimeoutException) && throwable.getMessage().contains("Read timed out");
310 | * });
311 | *
312 | * // ...
313 | *
314 | * ErrorHandler
315 | * .build()
316 | * .on("timeout", (throwable, handler) -> {
317 | * showOfflineScreen();
318 | * })
319 | * }
320 | *
321 | *
322 | *
323 | * @param the error code type
324 | * @param errorCode the errorCode value, can use a primitive for clarity and let it be autoboxed
325 | * @param matcherFactory a factory that given an error code, provides a matcher to match the error against it
326 | * @return the current {@code ErrorHandler} instance - to use in command chains
327 | */
328 | public ErrorHandler bind(T errorCode, MatcherFactory super T> matcherFactory) {
329 | errorCodeMap.put(new ErrorCodeIdentifier<>(errorCode), matcherFactory);
330 | return this;
331 | }
332 |
333 | /**
334 | * Bind an {@code errorCode} Class
to a {@code Matcher}, using a {@code MatcherFactory}.
335 | *
336 | *
337 | * For example, when we prefer using plain integers to refer to HTTP errors instead of
338 | * checking the HTTPException status code every time.
339 | *
340 | *
341 | *
342 | * {@code
343 | * ErrorHandler
344 | * .defaultErrorHandler()
345 | * .bindClass(Integer.class, errorCode -> throwable -> {
346 | * return throwable instanceof HTTPException && ((HTTPException)throwable).getStatusCode() == errorCode;
347 | * });
348 | *
349 | * // ...
350 | *
351 | * ErrorHandler
352 | * .build()
353 | * .on(404, (throwable, handler) -> {
354 | * showResourceNotFoundError();
355 | * })
356 | * .on(500, (throwable, handler) -> {
357 | * showServerError();
358 | * })
359 | * }
360 | *
361 | *
362 | * @param the error code type
363 | * @param errorCodeClass the errorCode class
364 | * @param matcherFactory a factory that given an error code, provides a matcher to match the error against it
365 | * @return the current {@code ErrorHandler} instance - to use in command chains
366 | */
367 | public ErrorHandler bindClass(Class errorCodeClass, MatcherFactory super T> matcherFactory) {
368 | errorCodeMap.put(new ErrorCodeIdentifier<>(errorCodeClass), matcherFactory);
369 | return this;
370 | }
371 |
372 | @SuppressWarnings("unchecked")
373 | protected MatcherFactory super T> getMatcherFactoryForErrorCode(T errorCode) {
374 | MatcherFactory matcherFactory;
375 | matcherFactory = errorCodeMap.get(new ErrorCodeIdentifier<>(errorCode));
376 |
377 | if (matcherFactory != null) {
378 | return matcherFactory;
379 | }
380 |
381 | matcherFactory = errorCodeMap.get(new ErrorCodeIdentifier(errorCode.getClass()));
382 |
383 | if (matcherFactory != null) {
384 | return matcherFactory;
385 | }
386 |
387 | if (parentErrorHandler != null) {
388 | return parentErrorHandler.getMatcherFactoryForErrorCode(errorCode);
389 | }
390 |
391 | return null;
392 | }
393 |
394 | /**
395 | * Clear ErrorHandler instance from all its registered Actions and Matchers.
396 | */
397 | public void clear() {
398 | actions.clear();
399 | errorCodeMap.clear();
400 | otherwiseActions.clear();
401 | alwaysActions.clear();
402 | if (localContext != null) {
403 | localContext.get().clear();
404 | }
405 | }
406 |
407 | /**
408 | * Throws if {@code action} is null
409 | *
410 | * @param action the action to assert against
411 | */
412 | private void assertNotNullAction(Action action) {
413 | if (action == null) {
414 | throw new IllegalArgumentException("action cannot be null");
415 | }
416 | }
417 |
418 | private static class Context {
419 | private HashMap keys = new HashMap<>();
420 |
421 | boolean handled;
422 | boolean skipDefaults = false;
423 | boolean skipFollowing = false;
424 | boolean skipAlways = false;
425 |
426 | public Object get(Object key) {
427 | return keys.get(key);
428 | }
429 |
430 | public Object put(String key, Object value) {
431 | return keys.put(key, value);
432 | }
433 |
434 | public Object remove(Object key) {
435 | return keys.remove(key);
436 | }
437 |
438 | void clear() {
439 | keys.clear();
440 | skipDefaults = false;
441 | skipFollowing = false;
442 | skipAlways = false;
443 | }
444 | }
445 |
446 | /**
447 | * Used to identify an error code either by its "literal" value
448 | * or by its Class.
449 | *
450 | * When using custom objects as error codes,
451 | * make sure you implement {@link Object#equals(Object)} to allow ErrorHandler
452 | * perform equality comparisons between instances.
453 | */
454 | private static final class ErrorCodeIdentifier {
455 | private T errorCode;
456 | private Class errorCodeClass;
457 |
458 | ErrorCodeIdentifier(T errorCode) {
459 | this.errorCode = errorCode;
460 | }
461 |
462 | ErrorCodeIdentifier(Class errorCodeClass) {
463 | this.errorCodeClass = errorCodeClass;
464 | }
465 |
466 | @Override
467 | public boolean equals(Object o) {
468 | if (this == o) return true;
469 | if (o == null || getClass() != o.getClass()) return false;
470 |
471 | ErrorCodeIdentifier that = (ErrorCodeIdentifier) o;
472 |
473 | if (errorCode != null ? !errorCode.equals(that.errorCode) : that.errorCode != null)
474 | return false;
475 | return errorCodeClass != null ? errorCodeClass.equals(that.errorCodeClass) : that.errorCodeClass == null;
476 |
477 | }
478 |
479 | @Override
480 | public int hashCode() {
481 | int result = errorCode != null ? errorCode.hashCode() : 0;
482 | result = 31 * result + (errorCodeClass != null ? errorCodeClass.hashCode() : 0);
483 | return result;
484 | }
485 | }
486 | }
487 |
--------------------------------------------------------------------------------
/errorhandler/src/main/java/com/workable/errorhandler/ExceptionMatcher.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2013-2016 Workable SA
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | package com.workable.errorhandler;
26 |
27 | public class ExceptionMatcher implements Matcher {
28 |
29 | private Class extends Exception> errorClass;
30 |
31 | public ExceptionMatcher(Class extends Exception> errorClass) {
32 | this.errorClass = errorClass;
33 | }
34 |
35 | @Override
36 | public boolean matches(Throwable throwable) {
37 | return errorClass.isInstance(throwable);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/errorhandler/src/main/java/com/workable/errorhandler/Matcher.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2013-2016 Workable SA
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | package com.workable.errorhandler;
26 |
27 | public interface Matcher {
28 |
29 | boolean matches(Throwable throwable);
30 |
31 | }
32 |
33 |
34 |
--------------------------------------------------------------------------------
/errorhandler/src/main/java/com/workable/errorhandler/MatcherFactory.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2013-2016 Workable SA
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | package com.workable.errorhandler;
26 |
27 | /**
28 | * A functional interface for {@link Matcher} factories
29 | *
30 | * @author Stratos Pavlakis - pavlakis@workable.com
31 | */
32 | public interface MatcherFactory {
33 |
34 | /**
35 | * Build a {@link Matcher} to match the given error code against an error
36 | *
37 | * @param errorCode the error code
38 | * @return a new {@link Matcher}
39 | */
40 | Matcher build(T errorCode);
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/errorhandler/src/main/java/com/workable/errorhandler/UnknownErrorCodeException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2013-2016 Workable SA
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | package com.workable.errorhandler;
26 |
27 | /**
28 | * Runtime exception to indicate that the user tried to register
29 | * an {@link Action} via {@link ErrorHandler#on(Object, Action)} with an unknown error code.
30 | *
31 | * @author Stratos Pavlakis - pavlakis@workable.com
32 | */
33 | public class UnknownErrorCodeException extends RuntimeException {
34 |
35 | private Object errorCode;
36 |
37 | public UnknownErrorCodeException(Object errorCode) {
38 | this.errorCode = errorCode;
39 | }
40 |
41 | public Object getErrorCode() {
42 | return errorCode;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/errorhandler/src/test/java/com/workable/errorhandler/BarException.java:
--------------------------------------------------------------------------------
1 | package com.workable.errorhandler;
2 |
3 | /**
4 | * A runtime test exception for using in {@link ErrorHandler} unit tests
5 | *
6 | * @author Stratos Pavlakis
7 | */
8 | public class BarException extends RuntimeException {
9 |
10 | private boolean openBar = true;
11 |
12 | public BarException(String message) {
13 | super(message);
14 | }
15 |
16 | public BarException(String message, boolean openBar) {
17 | super(message);
18 | this.openBar = openBar;
19 | }
20 |
21 | public boolean isOpenBar() {
22 | return openBar;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/errorhandler/src/test/java/com/workable/errorhandler/DBErrorException.java:
--------------------------------------------------------------------------------
1 | package com.workable.errorhandler;
2 |
3 | /**
4 | * A runtime test exception for using in {@link ErrorHandler} unit tests
5 | *
6 | * @author Pavlos-Petros Tournaris
7 | */
8 | public class DBErrorException extends Exception {
9 |
10 | public DBErrorException(String message) {
11 | super(message);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/errorhandler/src/test/java/com/workable/errorhandler/ErrorHandlerTest.java:
--------------------------------------------------------------------------------
1 | package com.workable.errorhandler;
2 |
3 | import junit.framework.TestCase;
4 | import org.junit.Test;
5 | import org.mockito.InOrder;
6 | import org.mockito.Mockito;
7 |
8 | import static org.mockito.Mockito.*;
9 |
10 | /**
11 | * {@link ErrorHandler} unit tests
12 | *
13 | * @author Stratos Pavlakis
14 | */
15 | public class ErrorHandlerTest extends TestCase {
16 |
17 | interface ActionDelegate {
18 |
19 | void action1();
20 |
21 | void action2();
22 |
23 | void action3();
24 |
25 | void action4();
26 |
27 | void action5();
28 |
29 | void otherwise1();
30 |
31 | void always1();
32 |
33 | void defaultAction1();
34 |
35 | void defaultAction2();
36 |
37 | void defaultAction3();
38 |
39 | void defaultOtherwise();
40 |
41 | void defaultAlways();
42 | }
43 |
44 | private ActionDelegate actionDelegateMock;
45 |
46 | protected void setUp() {
47 | actionDelegateMock = mock(ActionDelegate.class);
48 |
49 | ErrorHandler
50 | .defaultErrorHandler()
51 | .bind("closed:bar", errorCode -> throwable -> {
52 | if (throwable instanceof BarException) {
53 | return !((BarException) throwable).isOpenBar();
54 | } else {
55 | return false;
56 | }
57 | })
58 | .bindClass(Integer.class, errorCode -> throwable -> {
59 | if (throwable instanceof QuxException) {
60 | return ((QuxException) throwable).getErrorStatus() == errorCode;
61 | } else {
62 | return false;
63 | }
64 | })
65 | .on(FooException.class, (throwable, errorHandler) -> actionDelegateMock.defaultAction1())
66 | .on(500, (throwable, errorHandler) -> actionDelegateMock.defaultAction2())
67 | .on("closed:bar", (throwable, errorHandler) -> actionDelegateMock.defaultAction3())
68 | .otherwise((throwable, errorHandler) -> actionDelegateMock.defaultOtherwise())
69 | .always((throwable, errorHandler) -> actionDelegateMock.defaultAlways());
70 | }
71 |
72 | protected void tearDown() {
73 | ErrorHandler
74 | .defaultErrorHandler()
75 | .clear();
76 | }
77 |
78 | @Test
79 | public void testActionsExecutionOrder() {
80 | ErrorHandler errorHandler = ErrorHandler
81 | .create()
82 | .on(FooException.class, (throwable, handler) -> actionDelegateMock.action1())
83 | .on(
84 | (throwable) -> {
85 | try {
86 | return FooException.class.cast(throwable).isFatal();
87 | } catch (ClassCastException ignore) {
88 | return false;
89 | }
90 | },
91 | (throwable, handler) -> actionDelegateMock.action2()
92 | )
93 | .on("closed:bar", (throwable, handler) -> actionDelegateMock.action3())
94 | .on(400, (throwable, handler) -> actionDelegateMock.action4())
95 | .on(500, (throwable, handler) -> actionDelegateMock.action5())
96 | .otherwise((throwable, handler) -> actionDelegateMock.otherwise1())
97 | .always((throwable, handler) -> actionDelegateMock.always1());
98 |
99 |
100 | InOrder testVerifier1 = inOrder(actionDelegateMock);
101 |
102 | errorHandler.handle(new FooException("test1"));
103 |
104 | testVerifier1.verify(actionDelegateMock).action1();
105 | testVerifier1.verify(actionDelegateMock).always1();
106 | testVerifier1.verify(actionDelegateMock).defaultAction1();
107 | testVerifier1.verify(actionDelegateMock).defaultAlways();
108 | testVerifier1.verifyNoMoreInteractions();
109 | Mockito.verifyNoMoreInteractions(actionDelegateMock);
110 |
111 | reset(actionDelegateMock);
112 |
113 | InOrder testVerifier2 = inOrder(actionDelegateMock);
114 |
115 | errorHandler.handle(new BarException("What a shame", false));
116 |
117 | testVerifier2.verify(actionDelegateMock).action3();
118 | testVerifier2.verify(actionDelegateMock).always1();
119 | testVerifier2.verify(actionDelegateMock).defaultAction3();
120 | testVerifier2.verify(actionDelegateMock).defaultAlways();
121 | testVerifier2.verifyNoMoreInteractions();
122 | Mockito.verifyNoMoreInteractions(actionDelegateMock);
123 |
124 | reset(actionDelegateMock);
125 |
126 |
127 | InOrder testVerifier3 = inOrder(actionDelegateMock);
128 |
129 | errorHandler.handle(new QuxException(500));
130 |
131 | testVerifier3.verify(actionDelegateMock).action5();
132 | testVerifier3.verify(actionDelegateMock).always1();
133 | testVerifier3.verify(actionDelegateMock).defaultAction2();
134 | testVerifier3.verify(actionDelegateMock).defaultAlways();
135 | testVerifier3.verifyNoMoreInteractions();
136 | Mockito.verifyNoMoreInteractions(actionDelegateMock);
137 | }
138 |
139 | @Test
140 | public void testSkipDefaults() {
141 | ErrorHandler
142 | .create()
143 | .on(FooException.class, (throwable, handler) -> {
144 | actionDelegateMock.action1();
145 | })
146 | .handle(new FooException("foo error"));
147 |
148 | Mockito.verify(actionDelegateMock, times(1)).action1();
149 | Mockito.verify(actionDelegateMock, times(1)).defaultAction1();
150 |
151 | reset(actionDelegateMock);
152 |
153 | ErrorHandler
154 | .create()
155 | .on(FooException.class, (throwable, handler) -> {
156 | actionDelegateMock.action1();
157 | handler.skipDefaults();
158 | })
159 | .handle(new FooException("foo error"));
160 |
161 | Mockito.verify(actionDelegateMock, times(1)).action1();
162 | Mockito.verify(actionDelegateMock, never()).defaultAction1();
163 | }
164 |
165 | @Test
166 | public void testSkipFollowing() {
167 | InOrder testVerifier = inOrder(actionDelegateMock);
168 |
169 | ErrorHandler
170 | .create()
171 | .on(FooException.class, (throwable, handler) -> {
172 | actionDelegateMock.action1();
173 | })
174 | .on(FooException.class, (throwable, handler) -> {
175 | actionDelegateMock.action2();
176 | handler.skipFollowing();
177 | })
178 | .on(FooException.class, (throwable, handler) -> {
179 | actionDelegateMock.action3();
180 | })
181 | .on(FooException.class, (throwable, handler) -> {
182 | actionDelegateMock.action4();
183 | })
184 | .handle(new FooException("foo error"));
185 |
186 | testVerifier.verify(actionDelegateMock).action1();
187 | testVerifier.verify(actionDelegateMock).action2();
188 | testVerifier.verify(actionDelegateMock).defaultAlways();
189 | testVerifier.verifyNoMoreInteractions();
190 | Mockito.verifyNoMoreInteractions(actionDelegateMock);
191 | }
192 |
193 | @Test
194 | public void testSkipAlways() {
195 | InOrder testVerifier = inOrder(actionDelegateMock);
196 |
197 | ErrorHandler
198 | .create()
199 | .on(FooException.class, (throwable, handler) -> {
200 | actionDelegateMock.action1();
201 | })
202 | .on(FooException.class, (throwable, handler) -> {
203 | actionDelegateMock.action2();
204 | handler.skipAlways();
205 | })
206 | .on(FooException.class, (throwable, handler) -> {
207 | actionDelegateMock.action3();
208 | })
209 | .on(FooException.class, (throwable, handler) -> {
210 | actionDelegateMock.action4();
211 | })
212 | .handle(new FooException("foo error"));
213 |
214 | testVerifier.verify(actionDelegateMock).action1();
215 | testVerifier.verify(actionDelegateMock).action2();
216 | testVerifier.verify(actionDelegateMock).action3();
217 | testVerifier.verify(actionDelegateMock).action4();
218 | testVerifier.verify(actionDelegateMock).defaultAction1();
219 | testVerifier.verifyNoMoreInteractions();
220 | Mockito.verifyNoMoreInteractions(actionDelegateMock);
221 | }
222 |
223 | @Test
224 | public void testEnumClassHandling() {
225 | InOrder testVerifier = inOrder(actionDelegateMock);
226 |
227 | ErrorHandler
228 | .create()
229 | .bindClass(DBError.class, errorCode -> throwable -> {
230 | if (throwable instanceof DBErrorException) {
231 | return DBError.from((DBErrorException) throwable) == errorCode;
232 | }
233 | return false;
234 | })
235 | .on(DBError.READ_ONLY, (throwable, errorHandler) -> {
236 | actionDelegateMock.action1();
237 | errorHandler.skipAlways();
238 | })
239 | .handle(new DBErrorException("read-only"));
240 |
241 | testVerifier.verify(actionDelegateMock).action1();
242 | testVerifier.verifyNoMoreInteractions();
243 | Mockito.verifyNoMoreInteractions(actionDelegateMock);
244 | }
245 |
246 | @Test
247 | public void testErrorHandlerBlockExecutorExceptionHandling() {
248 | InOrder testVerifier = inOrder(actionDelegateMock);
249 |
250 | ErrorHandler
251 | .createIsolated()
252 | .bindClass(DBError.class, errorCode -> throwable -> throwable instanceof DBErrorException && DBError.from((DBErrorException) throwable) == errorCode)
253 | .on(DBError.READ_ONLY, (throwable, errorHandler) -> {
254 | actionDelegateMock.action1();
255 | errorHandler.skipAlways();
256 | })
257 | .run(() -> {
258 | throw new DBErrorException("read-only");
259 | });
260 |
261 | testVerifier.verify(actionDelegateMock).action1();
262 | testVerifier.verifyNoMoreInteractions();
263 | Mockito.verifyNoMoreInteractions(actionDelegateMock);
264 | }
265 |
266 | @Test
267 | public void testErrorHandlerBlockExecutorIgnoresNotMatchedException() {
268 | InOrder testVerifier = inOrder(actionDelegateMock);
269 |
270 | ErrorHandler
271 | .createIsolated()
272 | .bindClass(DBError.class, errorCode -> throwable -> throwable instanceof DBErrorException && DBError.from((DBErrorException) throwable) == errorCode)
273 | .on(DBError.READ_ONLY, (throwable, errorHandler) -> {
274 | actionDelegateMock.action1();
275 | errorHandler.skipAlways();
276 | })
277 | .run(() -> {
278 | throw new DBErrorException("read");
279 | });
280 |
281 | testVerifier.verifyNoMoreInteractions();
282 | Mockito.verifyNoMoreInteractions(actionDelegateMock);
283 | }
284 |
285 | @Test
286 | public void testErrorHandlerIfSkipDefaults() {
287 | InOrder testVerifier = inOrder(actionDelegateMock);
288 |
289 | ErrorHandler
290 | .create()
291 | .skipDefaults()
292 | .on("closed:bar", (throwable, handler) -> {
293 | actionDelegateMock.action1();
294 | })
295 | .run(() -> {
296 | throw new BarException("", false);
297 | });
298 |
299 | testVerifier.verify(actionDelegateMock).action1();
300 | Mockito.verifyNoMoreInteractions(actionDelegateMock);
301 |
302 | ErrorHandler
303 | .create()
304 | .on("closed:bar", (throwable, handler) -> {
305 | actionDelegateMock.action2();
306 | })
307 | .run(() -> {
308 | throw new BarException("", false);
309 | });
310 |
311 | testVerifier.verify(actionDelegateMock).action2();
312 | testVerifier.verify(actionDelegateMock).defaultAction3();
313 | }
314 |
315 | private enum DBError {
316 | READ_ONLY,
317 | DEADLOCK,
318 | FATAL;
319 |
320 | public static DBError from(DBErrorException error) {
321 | switch (error.getMessage()) {
322 | case "read-only":
323 | return DBError.READ_ONLY;
324 | case "deadlock":
325 | return DBError.DEADLOCK;
326 | default:
327 | return DBError.FATAL;
328 | }
329 | }
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/errorhandler/src/test/java/com/workable/errorhandler/FooException.java:
--------------------------------------------------------------------------------
1 | package com.workable.errorhandler;
2 |
3 | /**
4 | * A checked test exception for using in {@link ErrorHandler} unit tests
5 | *
6 | * @author Stratos Pavlakis
7 | */
8 | public class FooException extends Exception {
9 |
10 | private boolean fatal = false;
11 |
12 | public FooException(String message) {
13 | super(message);
14 | }
15 |
16 | public FooException(String message, boolean fatal) {
17 | super(message);
18 | this.fatal = fatal;
19 | }
20 |
21 | public boolean isFatal() {
22 | return fatal;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/errorhandler/src/test/java/com/workable/errorhandler/QuxException.java:
--------------------------------------------------------------------------------
1 | package com.workable.errorhandler;
2 |
3 | /**
4 | * A checked test exception for using in {@link ErrorHandler} unit tests
5 | *
6 | * @author Stratos Pavlakis
7 | */
8 | public class QuxException extends Exception {
9 |
10 | private int errorStatus = 0;
11 |
12 | public QuxException(int errorStatus) {
13 | this.errorStatus = errorStatus;
14 | }
15 |
16 | public int getErrorStatus() {
17 | return errorStatus;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Workable/java-error-handler/1c16c8f487593eddd88b55f31e9c9bb8243522df/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Sep 09 12:25:52 EEST 2016
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn ( ) {
37 | echo "$*"
38 | }
39 |
40 | die ( ) {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'parent'
2 | include ':errorhandler'
3 | include ':errorhandler-matchers:retrofit-rx-matcher'
4 |
--------------------------------------------------------------------------------