diff --git a/pom.xml b/pom.xml index 853f188..8cce5e8 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,24 @@ 4.8.2 test + + org.easymock + easymock + 2.3 + test + + + org.aspectj + aspectjrt + 1.6.6 + test + + + org.aspectj + aspectjweaver + 1.6.6 + test + org.springframework spring-test @@ -85,6 +103,12 @@ spring-context ${spring.framework.version} + + org.springframework + spring-tx + ${spring.framework.version} + test + log4j log4j @@ -128,7 +152,7 @@ org.apache.maven.plugins maven-surefire-plugin - + **/*Tests.java diff --git a/src/main/java/org/springframework/classify/BackToBackPatternClassifier.java b/src/main/java/org/springframework/classify/BackToBackPatternClassifier.java new file mode 100644 index 0000000..a3ec528 --- /dev/null +++ b/src/main/java/org/springframework/classify/BackToBackPatternClassifier.java @@ -0,0 +1,83 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.classify; + +import java.util.Map; + +/** + * A special purpose {@link Classifier} with easy configuration options for + * mapping from one arbitrary type of object to another via a pattern matcher. + * + * @author Dave Syer + * + */ +public class BackToBackPatternClassifier implements Classifier { + + private Classifier router; + + private Classifier matcher; + + /** + * Default constructor, provided as a convenience for people using setter + * injection. + */ + public BackToBackPatternClassifier() { + } + + /** + * Set up a classifier with input to the router and output from the matcher. + * + * @param router see {@link #setRouterDelegate(Object)} + * @param matcher see {@link #setMatcherMap(Map)} + */ + public BackToBackPatternClassifier(Classifier router, Classifier matcher) { + super(); + this.router = router; + this.matcher = matcher; + } + + /** + * A convenience method for creating a pattern matching classifier for the + * matcher component. + * + * @param map maps pattern keys with wildcards to output values + */ + public void setMatcherMap(Map map) { + this.matcher = new PatternMatchingClassifier(map); + } + + /** + * A convenience method of creating a router classifier based on a plain old + * Java Object. The object provided must have precisely one public method + * that either has the @Classifier annotation or accepts a single argument + * and outputs a String. This will be used to create an input classifier for + * the router component.
+ * + * @param delegate the delegate object used to create a router classifier + */ + public void setRouterDelegate(Object delegate) { + this.router = new ClassifierAdapter(delegate); + } + + /** + * Classify the input and map to a String, then take that and put it into a + * pattern matcher to match to an output value. + */ + public T classify(C classifiable) { + return matcher.classify(router.classify(classifiable)); + } + +} diff --git a/src/main/java/org/springframework/commons/classify/BinaryExceptionClassifier.java b/src/main/java/org/springframework/classify/BinaryExceptionClassifier.java similarity index 98% rename from src/main/java/org/springframework/commons/classify/BinaryExceptionClassifier.java rename to src/main/java/org/springframework/classify/BinaryExceptionClassifier.java index ebfbfa4..edd6ab1 100644 --- a/src/main/java/org/springframework/commons/classify/BinaryExceptionClassifier.java +++ b/src/main/java/org/springframework/classify/BinaryExceptionClassifier.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.commons.classify; +package org.springframework.classify; import java.util.Collection; import java.util.HashMap; diff --git a/src/main/java/org/springframework/commons/classify/Classifier.java b/src/main/java/org/springframework/classify/Classifier.java similarity index 96% rename from src/main/java/org/springframework/commons/classify/Classifier.java rename to src/main/java/org/springframework/classify/Classifier.java index 83864ac..5e58b8c 100644 --- a/src/main/java/org/springframework/commons/classify/Classifier.java +++ b/src/main/java/org/springframework/classify/Classifier.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.classify; +package org.springframework.classify; /** * Interface for a classifier. At its simplest a {@link Classifier} is just a diff --git a/src/main/java/org/springframework/classify/ClassifierAdapter.java b/src/main/java/org/springframework/classify/ClassifierAdapter.java new file mode 100644 index 0000000..9dab62b --- /dev/null +++ b/src/main/java/org/springframework/classify/ClassifierAdapter.java @@ -0,0 +1,101 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.classify; + +import org.springframework.classify.util.MethodInvoker; +import org.springframework.classify.util.MethodInvokerUtils; +import org.springframework.util.Assert; + +/** + * Wrapper for an object to adapt it to the {@link Classifier} interface. + * + * @author Dave Syer + * + */ +public class ClassifierAdapter implements Classifier { + + private MethodInvoker invoker; + + private Classifier classifier; + + /** + * Default constructor for use with setter injection. + */ + public ClassifierAdapter() { + super(); + } + + /** + * Create a new {@link Classifier} from the delegate provided. Use the + * constructor as an alternative to the {@link #setDelegate(Object)} method. + * + * @param delegate + */ + public ClassifierAdapter(Object delegate) { + setDelegate(delegate); + } + + /** + * Create a new {@link Classifier} from the delegate provided. Use the + * constructor as an alternative to the {@link #setDelegate(Classifier)} + * method. + * + * @param delegate + */ + public ClassifierAdapter(Classifier delegate) { + classifier = delegate; + } + + public void setDelegate(Classifier delegate) { + classifier = delegate; + invoker = null; + } + + /** + * Search for the + * {@link org.springframework.classify.annotation.Classifier + * Classifier} annotation on a method in the supplied delegate and use that + * to create a {@link Classifier} from the parameter type to the return + * type. If the annotation is not found a unique non-void method with a + * single parameter will be used, if it exists. The signature of the method + * cannot be checked here, so might be a runtime exception when the method + * is invoked if the signature doesn't match the classifier types. + * + * @param delegate an object with an annotated method + */ + public final void setDelegate(Object delegate) { + classifier = null; + invoker = MethodInvokerUtils.getMethodInvokerByAnnotation( + org.springframework.classify.annotation.Classifier.class, delegate); + if (invoker == null) { + invoker = MethodInvokerUtils. getMethodInvokerForSingleArgument(delegate); + } + Assert.state(invoker != null, "No single argument public method with or without " + + "@Classifier was found in delegate of type " + delegate.getClass()); + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + public T classify(C classifiable) { + if (classifier != null) { + return classifier.classify(classifiable); + } + return (T) invoker.invokeMethod(classifiable); + } + +} diff --git a/src/main/java/org/springframework/commons/classify/ClassifierSupport.java b/src/main/java/org/springframework/classify/ClassifierSupport.java similarity index 91% rename from src/main/java/org/springframework/commons/classify/ClassifierSupport.java rename to src/main/java/org/springframework/classify/ClassifierSupport.java index c76877c..d24aec2 100644 --- a/src/main/java/org/springframework/commons/classify/ClassifierSupport.java +++ b/src/main/java/org/springframework/classify/ClassifierSupport.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.classify; +package org.springframework.classify; /** * Base class for {@link Classifier} implementations. Provides default behaviour @@ -39,7 +39,7 @@ public class ClassifierSupport implements Classifier { * Always returns the default value. This is the main extension point for * subclasses, so it must be able to classify null. * - * @see org.springframework.commons.classify.Classifier#classify(Object) + * @see org.springframework.classify.Classifier#classify(Object) */ public T classify(C throwable) { return defaultValue; diff --git a/src/main/java/org/springframework/classify/PatternMatcher.java b/src/main/java/org/springframework/classify/PatternMatcher.java new file mode 100644 index 0000000..b8d96a0 --- /dev/null +++ b/src/main/java/org/springframework/classify/PatternMatcher.java @@ -0,0 +1,231 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.classify; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * @author Dave Syer + * @author Dan Garrette + */ +public class PatternMatcher { + + private Map map = new HashMap(); + private List sorted = new ArrayList(); + + /** + * Initialize a new {@link PatternMatcher} with a map of patterns to values + * @param map a map from String patterns to values + */ + public PatternMatcher(Map map) { + super(); + this.map = map; + // Sort keys to start with the most specific + sorted = new ArrayList(map.keySet()); + Collections.sort(sorted, new Comparator() { + public int compare(String o1, String o2) { + String s1 = o1; // .replace('?', '{'); + String s2 = o2; // .replace('*', '}'); + return s2.compareTo(s1); + } + }); + } + + /** + * Lifted from AntPathMatcher in Spring Core. Tests whether or not a string + * matches against a pattern. The pattern may contain two special + * characters:
+ * '*' means zero or more characters
+ * '?' means one and only one character + * + * @param pattern pattern to match against. Must not be null. + * @param str string which must be matched against the pattern. Must not be + * null. + * @return true if the string matches against the pattern, or + * false otherwise. + */ + public static boolean match(String pattern, String str) { + char[] patArr = pattern.toCharArray(); + char[] strArr = str.toCharArray(); + int patIdxStart = 0; + int patIdxEnd = patArr.length - 1; + int strIdxStart = 0; + int strIdxEnd = strArr.length - 1; + char ch; + + boolean containsStar = pattern.contains("*"); + + if (!containsStar) { + // No '*'s, so we make a shortcut + if (patIdxEnd != strIdxEnd) { + return false; // Pattern and string do not have the same size + } + for (int i = 0; i <= patIdxEnd; i++) { + ch = patArr[i]; + if (ch != '?') { + if (ch != strArr[i]) { + return false;// Character mismatch + } + } + } + return true; // String matches against pattern + } + + if (patIdxEnd == 0) { + return true; // Pattern contains only '*', which matches anything + } + + // Process characters before first star + while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) { + if (ch != '?') { + if (ch != strArr[strIdxStart]) { + return false;// Character mismatch + } + } + patIdxStart++; + strIdxStart++; + } + if (strIdxStart > strIdxEnd) { + // All characters in the string are used. Check if only '*'s are + // left in the pattern. If so, we succeeded. Otherwise failure. + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + return true; + } + + // Process characters after last star + while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) { + if (ch != '?') { + if (ch != strArr[strIdxEnd]) { + return false;// Character mismatch + } + } + patIdxEnd--; + strIdxEnd--; + } + if (strIdxStart > strIdxEnd) { + // All characters in the string are used. Check if only '*'s are + // left in the pattern. If so, we succeeded. Otherwise failure. + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + return true; + } + + // process pattern between stars. padIdxStart and patIdxEnd point + // always to a '*'. + while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) { + int patIdxTmp = -1; + for (int i = patIdxStart + 1; i <= patIdxEnd; i++) { + if (patArr[i] == '*') { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == patIdxStart + 1) { + // Two stars next to each other, skip the first one. + patIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - patIdxStart - 1); + int strLength = (strIdxEnd - strIdxStart + 1); + int foundIdx = -1; + strLoop: for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + ch = patArr[patIdxStart + j + 1]; + if (ch != '?') { + if (ch != strArr[strIdxStart + i + j]) { + continue strLoop; + } + } + } + + foundIdx = strIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + patIdxStart = patIdxTmp; + strIdxStart = foundIdx + patLength; + } + + // All characters in the string are used. Check if only '*'s are left + // in the pattern. If so, we succeeded. Otherwise failure. + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + + return true; + } + + /** + *

+ * This method takes a String key and a map from Strings to values of any + * type. During processing, the method will identify the most specific key + * in the map that matches the line. Once the correct is identified, its + * value is returned. Note that if the map contains the wildcard string "*" + * as a key, then it will serve as the "default" case, matching every line + * that does not match anything else. + * + *

+ * If no matching prefix is found, a {@link IllegalStateException} will be + * thrown. + * + *

+ * Null keys are not allowed in the map. + * + * @param line An input string + * @return the value whose prefix matches the given line + */ + public S match(String line) { + + S value = null; + Assert.notNull(line, "A non-null key must be provided to match against."); + + for (String key : sorted) { + if (PatternMatcher.match(key, line)) { + value = map.get(key); + break; + } + } + + if (value == null) { + throw new IllegalStateException("Could not find a matching pattern for key=[" + line + "]"); + } + return value; + + } + +} diff --git a/src/main/java/org/springframework/classify/PatternMatchingClassifier.java b/src/main/java/org/springframework/classify/PatternMatchingClassifier.java new file mode 100644 index 0000000..be9b67f --- /dev/null +++ b/src/main/java/org/springframework/classify/PatternMatchingClassifier.java @@ -0,0 +1,75 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.classify; + +import java.util.HashMap; +import java.util.Map; + +/** + * A {@link Classifier} that maps from String patterns with wildcards to a set + * of values of a given type. An input String is matched with the most specific + * pattern possible to the corresponding value in an input map. A default value + * should be specified with a pattern key of "*". + * + * @author Dave Syer + * + */ +public class PatternMatchingClassifier implements Classifier { + + private PatternMatcher values; + + /** + * Default constructor. Use the setter or the other constructor to create a + * sensible classifier, otherwise all inputs will cause an exception. + */ + public PatternMatchingClassifier() { + this(new HashMap()); + } + + /** + * Create a classifier from the provided map. The keys are patterns, using + * '?' as a single character and '*' as multi-character wildcard. + * + * @param values + */ + public PatternMatchingClassifier(Map values) { + super(); + this.values = new PatternMatcher(values); + } + + /** + * A map from pattern to value + * @param values the pattern map to set + */ + public void setPatternMap(Map values) { + this.values = new PatternMatcher(values); + } + + /** + * Classify the input by matching it against the patterns provided in + * {@link #setPatternMap(Map)}. The most specific pattern that matches will + * be used to locate a value. + * + * @return the value matching the most specific pattern possible + * + * @throws IllegalStateException if no matching value is found. + */ + public T classify(String classifiable) { + T value = values.match(classifiable); + return value; + } + +} diff --git a/src/main/java/org/springframework/commons/classify/SubclassClassifier.java b/src/main/java/org/springframework/classify/SubclassClassifier.java similarity index 98% rename from src/main/java/org/springframework/commons/classify/SubclassClassifier.java rename to src/main/java/org/springframework/classify/SubclassClassifier.java index f23deb6..9428983 100644 --- a/src/main/java/org/springframework/commons/classify/SubclassClassifier.java +++ b/src/main/java/org/springframework/classify/SubclassClassifier.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.commons.classify; +package org.springframework.classify; import java.io.Serializable; import java.util.Comparator; @@ -26,7 +26,7 @@ import java.util.TreeSet; * A {@link Classifier} for a parameterised object type based on a map. * Classifies objects according to their inheritance relation with the supplied * type map. If the object to be classified is one of the keys of the provided - * map, or is a subclass of one of the keys, then the map entry vale for that + * map, or is a subclass of one of the keys, then the map entry value for that * key is returned. Otherwise returns the default value which is null by * default. * diff --git a/src/main/java/org/springframework/classify/annotation/Classifier.java b/src/main/java/org/springframework/classify/annotation/Classifier.java new file mode 100644 index 0000000..b661329 --- /dev/null +++ b/src/main/java/org/springframework/classify/annotation/Classifier.java @@ -0,0 +1,38 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.classify.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mark a method as capable of classifying its input to an instance of its + * output. Should only be used on non-void methods with one parameter. + * + * @author Dave Syer + * + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface Classifier { + +} diff --git a/src/main/java/org/springframework/classify/util/AnnotationMethodResolver.java b/src/main/java/org/springframework/classify/util/AnnotationMethodResolver.java new file mode 100644 index 0000000..9aec6b5 --- /dev/null +++ b/src/main/java/org/springframework/classify/util/AnnotationMethodResolver.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.classify.util; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicReference; + +import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * MethodResolver implementation that finds a single Method on the + * given Class that contains the specified annotation type. + * + * @author Mark Fisher + */ +public class AnnotationMethodResolver implements MethodResolver { + + private Class annotationType; + + + /** + * Create a MethodResolver for the specified Method-level annotation type + */ + public AnnotationMethodResolver(Class annotationType) { + Assert.notNull(annotationType, "annotationType must not be null"); + Assert.isTrue(ObjectUtils.containsElement( + annotationType.getAnnotation(Target.class).value(), ElementType.METHOD), + "Annotation [" + annotationType + "] is not a Method-level annotation."); + this.annotationType = annotationType; + } + + + /** + * Find a single Method on the Class of the given candidate object + * that contains the annotation type for which this resolver is searching. + * + * @param candidate the instance whose Class will be checked for the + * annotation + * + * @return a single matching Method instance or null if the + * candidate's Class contains no Methods with the specified annotation + * + * @throws IllegalArgumentException if more than one Method has the + * specified annotation + */ + public Method findMethod(Object candidate) { + Assert.notNull(candidate, "candidate object must not be null"); + Class targetClass = AopUtils.getTargetClass(candidate); + if (targetClass == null) { + targetClass = candidate.getClass(); + } + return this.findMethod(targetClass); + } + + /** + * Find a single Method on the given Class that contains the + * annotation type for which this resolver is searching. + * + * @param clazz the Class instance to check for the annotation + * + * @return a single matching Method instance or null if the + * Class contains no Methods with the specified annotation + * + * @throws IllegalArgumentException if more than one Method has the + * specified annotation + */ + public Method findMethod(final Class clazz) { + Assert.notNull(clazz, "class must not be null"); + final AtomicReference annotatedMethod = new AtomicReference(); + ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() { + public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType); + if (annotation != null) { + Assert.isNull(annotatedMethod.get(), "found more than one method on target class [" + + clazz + "] with the annotation type [" + annotationType + "]"); + annotatedMethod.set(method); + } + } + }); + return annotatedMethod.get(); + } + +} diff --git a/src/main/java/org/springframework/classify/util/MethodInvoker.java b/src/main/java/org/springframework/classify/util/MethodInvoker.java new file mode 100644 index 0000000..2e276e2 --- /dev/null +++ b/src/main/java/org/springframework/classify/util/MethodInvoker.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.classify.util; + +/** + * A strategy interface for invoking a method. + * Typically used by adapters. + * + * @author Mark Fisher + */ +public interface MethodInvoker { + + Object invokeMethod(Object ... args); + +} diff --git a/src/main/java/org/springframework/classify/util/MethodInvokerUtils.java b/src/main/java/org/springframework/classify/util/MethodInvokerUtils.java new file mode 100644 index 0000000..d0b5721 --- /dev/null +++ b/src/main/java/org/springframework/classify/util/MethodInvokerUtils.java @@ -0,0 +1,210 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.classify.util; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicReference; + +import org.springframework.aop.framework.Advised; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Utility methods for create MethodInvoker instances. + * + * @author Lucas Ward + * @since 2.0 + */ +public class MethodInvokerUtils { + + /** + * Create a {@link MethodInvoker} using the provided method name to search. + * + * @param object to be invoked + * @param methodName of the method to be invoked + * @param paramsRequired boolean indicating whether the parameters are + * required, if false, a no args version of the method will be searched for. + * @param paramTypes - parameter types of the method to search for. + * @return MethodInvoker if the method is found, null if it is not. + */ + public static MethodInvoker getMethodInvokerByName(Object object, String methodName, boolean paramsRequired, + Class... paramTypes) { + Assert.notNull(object, "Object to invoke must not be null"); + Method method = ClassUtils.getMethodIfAvailable(object.getClass(), methodName, paramTypes); + if (method == null) { + String errorMsg = "no method found with name [" + methodName + "] on class [" + + object.getClass().getSimpleName() + "] compatable with the signature [" + + getParamTypesString(paramTypes) + "]."; + Assert.isTrue(!paramsRequired, errorMsg); + // if no method was found for the given parameters, and the + // parameters aren't required, then try with no params + method = ClassUtils.getMethodIfAvailable(object.getClass(), methodName, new Class[] {}); + Assert.notNull(method, errorMsg); + } + return new SimpleMethodInvoker(object, method); + } + + /** + * Create a String representation of the array of parameter types. + * + * @param paramTypes + * @return String + */ + public static String getParamTypesString(Class... paramTypes) { + StringBuffer paramTypesList = new StringBuffer("("); + for (int i = 0; i < paramTypes.length; i++) { + paramTypesList.append(paramTypes[i].getSimpleName()); + if (i + 1 < paramTypes.length) { + paramTypesList.append(", "); + } + } + return paramTypesList.append(")").toString(); + } + + /** + * Create a {@link MethodInvoker} using the provided interface, and method + * name from that interface. + * + * @param cls the interface to search for the method named + * @param methodName of the method to be invoked + * @param object to be invoked + * @param paramTypes - parameter types of the method to search for. + * @return MethodInvoker if the method is found, null if it is not. + */ + public static MethodInvoker getMethodInvokerForInterface(Class cls, String methodName, Object object, + Class... paramTypes) { + + if (cls.isAssignableFrom(object.getClass())) { + return MethodInvokerUtils.getMethodInvokerByName(object, methodName, true, paramTypes); + } + else { + return null; + } + } + + /** + * Create a MethodInvoker from the delegate based on the annotationType. + * Ensure that the annotated method has a valid set of parameters. + * + * @param annotationType the annotation to scan for + * @param target the target object + * @param expectedParamTypes the expected parameter types for the method + * @return a MethodInvoker + */ + public static MethodInvoker getMethodInvokerByAnnotation(final Class annotationType, + final Object target, final Class... expectedParamTypes) { + MethodInvoker mi = MethodInvokerUtils.getMethodInvokerByAnnotation(annotationType, target); + final Class targetClass = (target instanceof Advised) ? ((Advised) target).getTargetSource() + .getTargetClass() : target.getClass(); + if (mi != null) { + ReflectionUtils.doWithMethods(targetClass, new ReflectionUtils.MethodCallback() { + public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType); + if (annotation != null) { + Class[] paramTypes = method.getParameterTypes(); + if (paramTypes.length > 0) { + String errorMsg = "The method [" + method.getName() + "] on target class [" + + targetClass.getSimpleName() + "] is incompatable with the signature [" + + getParamTypesString(expectedParamTypes) + "] expected for the annotation [" + + annotationType.getSimpleName() + "]."; + + Assert.isTrue(paramTypes.length == expectedParamTypes.length, errorMsg); + for (int i = 0; i < paramTypes.length; i++) { + Assert.isTrue(expectedParamTypes[i].isAssignableFrom(paramTypes[i]), errorMsg); + } + } + } + } + }); + } + return mi; + } + + /** + * Create {@link MethodInvoker} for the method with the provided annotation + * on the provided object. Annotations that cannot be applied to methods + * (i.e. that aren't annotated with an element type of METHOD) will cause an + * exception to be thrown. + * + * @param annotationType to be searched for + * @param target to be invoked + * @return MethodInvoker for the provided annotation, null if none is found. + */ + public static MethodInvoker getMethodInvokerByAnnotation(final Class annotationType, + final Object target) { + Assert.notNull(target, "Target must not be null"); + Assert.notNull(annotationType, "AnnotationType must not be null"); + Assert.isTrue(ObjectUtils.containsElement(annotationType.getAnnotation(Target.class).value(), + ElementType.METHOD), "Annotation [" + annotationType + "] is not a Method-level annotation."); + final Class targetClass = (target instanceof Advised) ? ((Advised) target).getTargetSource() + .getTargetClass() : target.getClass(); + if (targetClass == null) { + // Proxy with no target cannot have annotations + return null; + } + final AtomicReference annotatedMethod = new AtomicReference(); + ReflectionUtils.doWithMethods(targetClass, new ReflectionUtils.MethodCallback() { + public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType); + if (annotation != null) { + Assert.isNull(annotatedMethod.get(), "found more than one method on target class [" + + targetClass.getSimpleName() + "] with the annotation type [" + + annotationType.getSimpleName() + "]."); + annotatedMethod.set(method); + } + } + }); + Method method = annotatedMethod.get(); + if (method == null) { + return null; + } + else { + return new SimpleMethodInvoker(target, annotatedMethod.get()); + } + } + + /** + * Create a {@link MethodInvoker} for the delegate from a single public + * method. + * + * @param target an object to search for an appropriate method + * @return a MethodInvoker that calls a method on the delegate + */ + public static MethodInvoker getMethodInvokerForSingleArgument(Object target) { + final AtomicReference methodHolder = new AtomicReference(); + ReflectionUtils.doWithMethods(target.getClass(), new ReflectionUtils.MethodCallback() { + public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + if (method.getParameterTypes() == null || method.getParameterTypes().length != 1) { + return; + } + if (method.getReturnType().equals(Void.TYPE) || ReflectionUtils.isEqualsMethod(method)) { + return; + } + Assert.state(methodHolder.get() == null, + "More than one non-void public method detected with single argument."); + methodHolder.set(method); + } + }); + Method method = methodHolder.get(); + return new SimpleMethodInvoker(target, method); + } +} diff --git a/src/main/java/org/springframework/classify/util/MethodResolver.java b/src/main/java/org/springframework/classify/util/MethodResolver.java new file mode 100644 index 0000000..cc71ea8 --- /dev/null +++ b/src/main/java/org/springframework/classify/util/MethodResolver.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.classify.util; + +import java.lang.reflect.Method; + +/** + * Strategy interface for detecting a single Method on a Class. + * + * @author Mark Fisher + */ +public interface MethodResolver { + + /** + * Find a single Method on the provided Object that matches this resolver's + * criteria. + * + * @param candidate the candidate Object whose Class should be searched for + * a Method + * + * @return a single Method or null if no Method matching this + * resolver's criteria can be found. + * + * @throws IllegalArgumentException if more than one Method defined on the + * given candidate's Class matches this resolver's criteria + */ + Method findMethod(Object candidate) throws IllegalArgumentException; + + /** + * Find a single Method on the given Class that matches this + * resolver's criteria. + * + * @param clazz the Class instance on which to search for a Method + * + * @return a single Method or null if no Method matching this + * resolver's criteria can be found. + * + * @throws IllegalArgumentException if more than one Method defined on the + * given Class matches this resolver's criteria + */ + Method findMethod(Class clazz); + +} diff --git a/src/main/java/org/springframework/classify/util/SimpleMethodInvoker.java b/src/main/java/org/springframework/classify/util/SimpleMethodInvoker.java new file mode 100644 index 0000000..bb37a5c --- /dev/null +++ b/src/main/java/org/springframework/classify/util/SimpleMethodInvoker.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.classify.util; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.springframework.aop.framework.Advised; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Simple implementation of the {@link MethodInvoker} interface that invokes a + * method on an object. If the method has no arguments, but arguments are + * provided, they are ignored and the method is invoked anyway. If there are + * more arguments than there are provided, then an exception is thrown. + * + * @author Lucas Ward + * @since 2.0 + */ +public class SimpleMethodInvoker implements MethodInvoker { + + private final Object object; + + private Method method; + + public SimpleMethodInvoker(Object object, Method method) { + Assert.notNull(object, "Object to invoke must not be null"); + Assert.notNull(method, "Method to invoke must not be null"); + this.method = method; + this.object = object; + } + + public SimpleMethodInvoker(Object object, String methodName, Class... paramTypes) { + Assert.notNull(object, "Object to invoke must not be null"); + this.method = ClassUtils.getMethodIfAvailable(object.getClass(), methodName, paramTypes); + if (this.method == null) { + // try with no params + this.method = ClassUtils.getMethodIfAvailable(object.getClass(), methodName, new Class[] {}); + } + if (this.method == null) { + throw new IllegalArgumentException("No methods found for name: [" + methodName + "] in class: [" + + object.getClass() + "] with arguments of type: [" + Arrays.toString(paramTypes) + "]"); + } + this.object = object; + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.batch.core.configuration.util.MethodInvoker#invokeMethod + * (java.lang.Object[]) + */ + public Object invokeMethod(Object... args) { + + Class[] parameterTypes = method.getParameterTypes(); + Object[] invokeArgs; + if (parameterTypes.length == 0) { + invokeArgs = new Object[] {}; + } + else if (parameterTypes.length != args.length) { + throw new IllegalArgumentException("Wrong number of arguments, expected no more than: [" + + parameterTypes.length + "]"); + } + else { + invokeArgs = args; + } + + method.setAccessible(true); + + try { + // Extract the target from an Advised as late as possible + // in case it contains a lazy initialization + Object target = extractTarget(object, method); + return method.invoke(target, invokeArgs); + } + catch (Exception e) { + throw new IllegalArgumentException("Unable to invoke method: [" + method + "] on object: [" + object + + "] with arguments: [" + Arrays.toString(args) + "]", e); + } + } + + private Object extractTarget(Object target, Method method) { + if (target instanceof Advised) { + Object source; + try { + source = ((Advised) target).getTargetSource().getTarget(); + } + catch (Exception e) { + throw new IllegalStateException("Could not extract target from proxy", e); + } + if (source instanceof Advised) { + source = extractTarget(source, method); + } + if (method.getDeclaringClass().isAssignableFrom(source.getClass())) { + target = source; + } + } + return target; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SimpleMethodInvoker)) { + return false; + } + + if (obj == this) { + return true; + } + SimpleMethodInvoker rhs = (SimpleMethodInvoker) obj; + return (rhs.method.equals(this.method)) && (rhs.object.equals(this.object)); + } + + @Override + public int hashCode() { + int result = 25; + result = 31 * result + object.hashCode(); + result = 31 * result + method.hashCode(); + return result; + } +} diff --git a/src/main/java/org/springframework/repeat/CompletionPolicy.java b/src/main/java/org/springframework/repeat/CompletionPolicy.java new file mode 100644 index 0000000..51fead0 --- /dev/null +++ b/src/main/java/org/springframework/repeat/CompletionPolicy.java @@ -0,0 +1,80 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat; + + +/** + * Interface for batch completion policies, to enable batch operations to + * strategise normal completion conditions. Stateful implementations of batch + * iterators should only update state using the update method. If you + * need custom behaviour consider extending an existing implementation or using + * the composite provided. + * + * @author Dave Syer + * + */ +public interface CompletionPolicy { + + /** + * Determine whether a batch is complete given the latest result from the + * callback. If this method returns true then + * {@link #isComplete(RepeatContext)} should also (but not necessarily vice + * versa, since the answer here depends on the result). + * + * @param context the current batch context. + * @param result the result of the latest batch item processing. + * + * @return true if the batch should terminate. + * + * @see #isComplete(RepeatContext) + */ + boolean isComplete(RepeatContext context, RepeatStatus result); + + /** + * Allow policy to signal completion according to internal state, without + * having to wait for the callback to complete. + * + * @param context the current batch context. + * + * @return true if the batch should terminate. + */ + boolean isComplete(RepeatContext context); + + /** + * Create a new context for the execution of a batch. N.B. implementations + * should not return the parent from this method - they must + * create a new context to meet the specific needs of the policy. The best + * way to do this might be to override an existing implementation and use + * the {@link RepeatContext} to store state in its attributes. + * + * @param parent the current context if one is already in progress. + * @return a context object that can be used by the implementation to store + * internal state for a batch. + */ + RepeatContext start(RepeatContext parent); + + /** + * Give implementations the opportunity to update the state of the current + * batch. Will be called once per callback, after it has been + * launched, but not necessarily after it completes (if the batch is + * asynchronous). + * + * @param context the value returned by start. + */ + void update(RepeatContext context); + +} diff --git a/src/main/java/org/springframework/commons/repeat/RepeatCallback.java b/src/main/java/org/springframework/repeat/RepeatCallback.java similarity index 97% rename from src/main/java/org/springframework/commons/repeat/RepeatCallback.java rename to src/main/java/org/springframework/repeat/RepeatCallback.java index 2e5d426..2996171 100644 --- a/src/main/java/org/springframework/commons/repeat/RepeatCallback.java +++ b/src/main/java/org/springframework/repeat/RepeatCallback.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.repeat; +package org.springframework.repeat; /** diff --git a/src/main/java/org/springframework/commons/repeat/RepeatContext.java b/src/main/java/org/springframework/repeat/RepeatContext.java similarity index 98% rename from src/main/java/org/springframework/commons/repeat/RepeatContext.java rename to src/main/java/org/springframework/repeat/RepeatContext.java index 099cf8a..bcc528d 100644 --- a/src/main/java/org/springframework/commons/repeat/RepeatContext.java +++ b/src/main/java/org/springframework/repeat/RepeatContext.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.repeat; +package org.springframework.repeat; import org.springframework.core.AttributeAccessor; diff --git a/src/main/java/org/springframework/commons/repeat/RepeatException.java b/src/main/java/org/springframework/repeat/RepeatException.java similarity index 95% rename from src/main/java/org/springframework/commons/repeat/RepeatException.java rename to src/main/java/org/springframework/repeat/RepeatException.java index 8e110b0..9f02abf 100644 --- a/src/main/java/org/springframework/commons/repeat/RepeatException.java +++ b/src/main/java/org/springframework/repeat/RepeatException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.repeat; +package org.springframework.repeat; import org.springframework.core.NestedRuntimeException; diff --git a/src/main/java/org/springframework/repeat/RepeatListener.java b/src/main/java/org/springframework/repeat/RepeatListener.java new file mode 100644 index 0000000..834d40b --- /dev/null +++ b/src/main/java/org/springframework/repeat/RepeatListener.java @@ -0,0 +1,80 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat; + + +/** + * Interface for listeners to the batch process. Implementers can provide + * enhance the behaviour of a batch in small cross-cutting modules. The + * framework provides callbacks at key points in the processing. + * + * @author Dave Syer + * + */ +public interface RepeatListener { + /** + * Called by the framework before each batch item. Implementers can halt a + * batch by setting the complete flag on the context. + * + * @param context the current batch context. + */ + void before(RepeatContext context); + + /** + * Called by the framework after each item has been processed, unless the + * item processing results in an exception. This method is called as soon as + * the result is known. + * + * @param context the current batch context + * @param result the result of the callback + */ + void after(RepeatContext context, RepeatStatus result); + + /** + * Called once at the start of a complete batch, before any items are + * processed. Implementers can use this method to acquire any resources that + * might be needed during processing. Implementers can halt the current + * operation by setting the complete flag on the context. To halt all + * enclosing batches (the whole job), the would need to use the parent + * context (recursively). + * + * @param context the current batch context + */ + void open(RepeatContext context); + + /** + * Called when a repeat callback fails by throwing an exception. There will + * be one call to this method for each exception thrown during a repeat + * operation (e.g. a chunk).
+ * + * There is no need to re-throw the exception here - that will be done by + * the enclosing framework. + * + * @param context the current batch context + * @param e the error that was encountered in an item callback. + */ + void onError(RepeatContext context, Throwable e); + + /** + * Called once at the end of a complete batch, after normal or abnormal + * completion (i.e. even after an exception). Implementers can use this + * method to clean up any resources. + * + * @param context the current batch context. + */ + void close(RepeatContext context); +} diff --git a/src/main/java/org/springframework/commons/repeat/RepeatOperations.java b/src/main/java/org/springframework/repeat/RepeatOperations.java similarity index 97% rename from src/main/java/org/springframework/commons/repeat/RepeatOperations.java rename to src/main/java/org/springframework/repeat/RepeatOperations.java index da49ee2..c6a8965 100644 --- a/src/main/java/org/springframework/commons/repeat/RepeatOperations.java +++ b/src/main/java/org/springframework/repeat/RepeatOperations.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.repeat; +package org.springframework.repeat; /** diff --git a/src/main/java/org/springframework/commons/repeat/RepeatStatus.java b/src/main/java/org/springframework/repeat/RepeatStatus.java similarity index 96% rename from src/main/java/org/springframework/commons/repeat/RepeatStatus.java rename to src/main/java/org/springframework/repeat/RepeatStatus.java index 3a117ab..7b41a51 100644 --- a/src/main/java/org/springframework/commons/repeat/RepeatStatus.java +++ b/src/main/java/org/springframework/repeat/RepeatStatus.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.repeat; +package org.springframework.repeat; public enum RepeatStatus { diff --git a/src/main/java/org/springframework/repeat/callback/NestedRepeatCallback.java b/src/main/java/org/springframework/repeat/callback/NestedRepeatCallback.java new file mode 100644 index 0000000..c5708fe --- /dev/null +++ b/src/main/java/org/springframework/repeat/callback/NestedRepeatCallback.java @@ -0,0 +1,61 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.callback; + +import org.springframework.repeat.RepeatCallback; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatOperations; +import org.springframework.repeat.RepeatStatus; + +/** + * Callback that delegates to another callback, via a {@link RepeatOperations} + * instance. Useful when nesting or composing batches in one another, e.g. for + * breaking a batch down into chunks. + * + * @author Dave Syer + * + */ +public class NestedRepeatCallback implements RepeatCallback { + + private RepeatOperations template; + + private RepeatCallback callback; + + /** + * Constructor setting mandatory fields. + * + * @param template the {@link RepeatOperations} to use when calling the + * delegate callback + * @param callback the {@link RepeatCallback} delegate + */ + public NestedRepeatCallback(RepeatOperations template, RepeatCallback callback) { + super(); + this.template = template; + this.callback = callback; + } + + /** + * Simply calls template.execute(callback). Clients can use this to repeat a + * batch process, or to break a process up into smaller chunks (e.g. to + * change the transaction boundaries). + * + * @see org.springframework.repeat.RepeatCallback#doInIteration(RepeatContext) + */ + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + return template.iterate(callback); + } +} diff --git a/src/main/java/org/springframework/repeat/callback/package.html b/src/main/java/org/springframework/repeat/callback/package.html new file mode 100644 index 0000000..a25a6cf --- /dev/null +++ b/src/main/java/org/springframework/repeat/callback/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat callback concerns. +

+ + diff --git a/src/main/java/org/springframework/repeat/context/RepeatContextCounter.java b/src/main/java/org/springframework/repeat/context/RepeatContextCounter.java new file mode 100644 index 0000000..9830607 --- /dev/null +++ b/src/main/java/org/springframework/repeat/context/RepeatContextCounter.java @@ -0,0 +1,115 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.context; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.repeat.RepeatContext; +import org.springframework.util.Assert; + +/** + * Helper class for policies that need to count the number of occurrences of + * some event (e.g. an exception type in the context) in the scope of a batch. + * The value of the counter can be stored between batches in a nested context, + * so that the termination decision is based on the aggregate of a number of + * sibling batches. + * + * @author Dave Syer + * + */ +public class RepeatContextCounter { + + final private String countKey; + + /** + * Flag to indicate whether the count is stored at the level of the parent + * context, or just local to the current context. Default value is false. + */ + final private boolean useParent; + + final private RepeatContext context; + + /** + * Increment the counter. + * + * @param delta the amount by which to increment the counter. + */ + final public void increment(int delta) { + AtomicInteger count = getCounter(); + count.addAndGet(delta); + } + + /** + * Increment by 1. + */ + final public void increment() { + increment(1); + } + + /** + * Convenience constructor with useParent=false. + * @param context the current context. + * @param countKey the key to use to store the counter in the context. + */ + public RepeatContextCounter(RepeatContext context, String countKey) { + this(context, countKey, false); + } + + /** + * Construct a new {@link RepeatContextCounter}. + * + * @param context the current context. + * @param countKey the key to use to store the counter in the context. + * @param useParent true if the counter is to be shared between siblings. + * The state will be stored in the parent of the context (if it exists) + * instead of the context itself. + */ + public RepeatContextCounter(RepeatContext context, String countKey, boolean useParent) { + + super(); + + Assert.notNull(context, "The context must be provided to initialize a counter"); + + this.countKey = countKey; + this.useParent = useParent; + + RepeatContext parent = context.getParent(); + + if (this.useParent && parent != null) { + this.context = parent; + } + else { + this.context = context; + } + if (!this.context.hasAttribute(countKey)) { + this.context.setAttribute(countKey, new AtomicInteger()); + } + + } + + /** + * @return the current value of the counter + */ + public int getCount() { + return getCounter().intValue(); + } + + private AtomicInteger getCounter() { + return ((AtomicInteger) context.getAttribute(countKey)); + } + +} diff --git a/src/main/java/org/springframework/repeat/context/RepeatContextSupport.java b/src/main/java/org/springframework/repeat/context/RepeatContextSupport.java new file mode 100644 index 0000000..8430b7a --- /dev/null +++ b/src/main/java/org/springframework/repeat/context/RepeatContextSupport.java @@ -0,0 +1,178 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.context; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.repeat.RepeatContext; + +public class RepeatContextSupport extends SynchronizedAttributeAccessor implements RepeatContext { + + private RepeatContext parent; + + private int count; + + private volatile boolean completeOnly; + + private volatile boolean terminateOnly; + + private Map> callbacks = new HashMap>(); + + /** + * Constructor for {@link RepeatContextSupport}. The parent can be null, but + * should be set to the enclosing repeat context if there is one, e.g. if + * this context is an inner loop. + * @param parent + */ + public RepeatContextSupport(RepeatContext parent) { + super(); + this.parent = parent; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.batch.repeat.RepeatContext#isCompleteOnly() + */ + public boolean isCompleteOnly() { + return completeOnly; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.batch.repeat.RepeatContext#setCompleteOnly() + */ + public void setCompleteOnly() { + completeOnly = true; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.batch.repeat.RepeatContext#isTerminateOnly() + */ + public boolean isTerminateOnly() { + return terminateOnly; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.batch.repeat.RepeatContext#setTerminateOnly() + */ + public void setTerminateOnly() { + terminateOnly = true; + setCompleteOnly(); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.batch.repeat.RepeatContext#getParent() + */ + public RepeatContext getParent() { + return parent; + } + + /** + * Used by clients to increment the started count. + */ + public synchronized void increment() { + count++; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.batch.repeat.RepeatContext#getStartedCount() + */ + public synchronized int getStartedCount() { + return count; + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.batch.repeat.RepeatContext#registerDestructionCallback + * (java.lang.String, java.lang.Runnable) + */ + public void registerDestructionCallback(String name, Runnable callback) { + synchronized (callbacks) { + Set set = callbacks.get(name); + if (set == null) { + set = new HashSet(); + callbacks.put(name, set); + } + set.add(callback); + } + } + + /* + * (non-Javadoc) + * + * @see org.springframework.batch.repeat.RepeatContext#close() + */ + public void close() { + + List errors = new ArrayList(); + + Set>> copy; + + synchronized (callbacks) { + copy = new HashSet>>(callbacks.entrySet()); + } + + for (Map.Entry> entry : copy) { + + for (Runnable callback : entry.getValue()) { + /* + * Potentially we could check here if there is an attribute with + * the given name - if it has been removed, maybe the callback + * is invalid. On the other hand it is less surprising for the + * callback register if it is always executed. + */ + if (callback != null) { + /* + * The documentation of the interface says that these + * callbacks must not throw exceptions, but we don't trust + * them necessarily... + */ + try { + callback.run(); + } + catch (RuntimeException t) { + errors.add(t); + } + } + } + } + + if (errors.isEmpty()) { + return; + } + + throw (RuntimeException) errors.get(0); + } + +} diff --git a/src/main/java/org/springframework/repeat/context/SynchronizedAttributeAccessor.java b/src/main/java/org/springframework/repeat/context/SynchronizedAttributeAccessor.java new file mode 100644 index 0000000..f67b2b6 --- /dev/null +++ b/src/main/java/org/springframework/repeat/context/SynchronizedAttributeAccessor.java @@ -0,0 +1,161 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.context; + +import org.springframework.core.AttributeAccessor; +import org.springframework.core.AttributeAccessorSupport; + +/** + * An {@link AttributeAccessor} that synchronizes on a mutex (not this) before + * modifying or accessing the underlying attributes. + * + * @author Dave Syer + * + */ +public class SynchronizedAttributeAccessor implements AttributeAccessor { + + /** + * All methods are delegated to this support object. + */ + AttributeAccessorSupport support = new AttributeAccessorSupport() { + /** + * Generated serial UID. + */ + private static final long serialVersionUID = -7664290016506582290L; + }; + + /* + * (non-Javadoc) + * @see org.springframework.core.AttributeAccessor#attributeNames() + */ + public String[] attributeNames() { + synchronized (support) { + return support.attributeNames(); + } + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object other) { + if (this == other) { + return true; + } + AttributeAccessorSupport that; + if (other instanceof SynchronizedAttributeAccessor) { + that = ((SynchronizedAttributeAccessor) other).support; + } + else if (other instanceof AttributeAccessorSupport) { + that = (AttributeAccessorSupport) other; + } + else { + return false; + } + synchronized (support) { + return support.equals(that); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.core.AttributeAccessor#getAttribute(java.lang.String) + */ + public Object getAttribute(String name) { + synchronized (support) { + return support.getAttribute(name); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.core.AttributeAccessor#hasAttribute(java.lang.String) + */ + public boolean hasAttribute(String name) { + synchronized (support) { + return support.hasAttribute(name); + } + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + public int hashCode() { + return support.hashCode(); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.AttributeAccessor#removeAttribute(java.lang.String) + */ + public Object removeAttribute(String name) { + synchronized (support) { + return support.removeAttribute(name); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.core.AttributeAccessor#setAttribute(java.lang.String, + * java.lang.Object) + */ + public void setAttribute(String name, Object value) { + synchronized (support) { + support.setAttribute(name, value); + } + } + + /** + * Additional support for atomic put if absent. + * @param name the key for the attribute name + * @param value the value of the attribute + * @return null if the attribute was not already set, the existing value + * otherwise. + */ + public Object setAttributeIfAbsent(String name, Object value) { + synchronized (support) { + Object old = getAttribute(name); + if (old != null) { + return old; + } + setAttribute(name, value); + } + return null; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + public String toString() { + StringBuffer buffer = new StringBuffer("SynchronizedAttributeAccessor: ["); + synchronized (support) { + String[] names = attributeNames(); + for (int i = 0; i < names.length; i++) { + String name = names[i]; + buffer.append(names[i]).append("=").append(getAttribute(name)); + if (i < names.length - 1) { + buffer.append(", "); + } + } + buffer.append("]"); + return buffer.toString(); + } + } + +} diff --git a/src/main/java/org/springframework/repeat/context/package.html b/src/main/java/org/springframework/repeat/context/package.html new file mode 100644 index 0000000..7bb43fa --- /dev/null +++ b/src/main/java/org/springframework/repeat/context/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat context concerns. +

+ + diff --git a/src/main/java/org/springframework/repeat/exception/CompositeExceptionHandler.java b/src/main/java/org/springframework/repeat/exception/CompositeExceptionHandler.java new file mode 100644 index 0000000..ba808ef --- /dev/null +++ b/src/main/java/org/springframework/repeat/exception/CompositeExceptionHandler.java @@ -0,0 +1,49 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.exception; + +import java.util.Arrays; + +import org.springframework.repeat.RepeatContext; + +/** + * Composiste {@link ExceptionHandler} that loops though a list of delegates. + * + * @author Dave Syer + * + */ +public class CompositeExceptionHandler implements ExceptionHandler { + + private ExceptionHandler[] handlers = new ExceptionHandler[0]; + + public void setHandlers(ExceptionHandler[] handlers) { + this.handlers = Arrays.asList(handlers).toArray(new ExceptionHandler[handlers.length]); + } + + /** + * Iterate over the handlers delegating the call to each in turn. The chain + * ends if an exception is thrown. + * + * @see ExceptionHandler#handleException(RepeatContext, Throwable) + */ + public void handleException(RepeatContext context, Throwable throwable) throws Throwable { + for (int i = 0; i < handlers.length; i++) { + ExceptionHandler handler = handlers[i]; + handler.handleException(context, throwable); + } + } +} diff --git a/src/main/java/org/springframework/repeat/exception/DefaultExceptionHandler.java b/src/main/java/org/springframework/repeat/exception/DefaultExceptionHandler.java new file mode 100644 index 0000000..eb1ce51 --- /dev/null +++ b/src/main/java/org/springframework/repeat/exception/DefaultExceptionHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.exception; + +import org.springframework.repeat.RepeatContext; + +/** + * Default implementation of {@link ExceptionHandler} - just re-throws the exception it encounters. + * + * @author Dave Syer + * + */ +public class DefaultExceptionHandler implements ExceptionHandler { + + /** + * Re-throw the throwable. + * + * @see org.springframework.repeat.exception.ExceptionHandler#handleException(RepeatContext, + * Throwable) + */ + public void handleException(RepeatContext context, Throwable throwable) throws Throwable { + throw throwable; + } + +} diff --git a/src/main/java/org/springframework/repeat/exception/ExceptionHandler.java b/src/main/java/org/springframework/repeat/exception/ExceptionHandler.java new file mode 100644 index 0000000..0634952 --- /dev/null +++ b/src/main/java/org/springframework/repeat/exception/ExceptionHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.exception; + +import org.springframework.repeat.CompletionPolicy; +import org.springframework.repeat.RepeatContext; + +/** + * Handler to allow strategies for re-throwing exceptions. Normally a + * {@link CompletionPolicy} will be used to decide whether to end a batch when + * there is no exception, and the {@link ExceptionHandler} is used to signal an + * abnormal ending - an abnormal ending would result in an + * {@link ExceptionHandler} throwing an exception. The caller will catch and + * re-throw it if necessary. + * + * @author Dave Syer + * @author Robert Kasanicky + * + */ +public interface ExceptionHandler { + + /** + * Deal with a Throwable during a batch - decide whether it should be + * re-thrown in the first place. + * + * @param context the current {@link RepeatContext}. Can be used to store + * state (via attributes), for example to count the number of occurrences of + * a particular exception type and implement a threshold policy. + * @param throwable an exception. + * @throws Throwable implementations are free to re-throw the exception + */ + void handleException(RepeatContext context, Throwable throwable) throws Throwable; + +} diff --git a/src/main/java/org/springframework/repeat/exception/LogOrRethrowExceptionHandler.java b/src/main/java/org/springframework/repeat/exception/LogOrRethrowExceptionHandler.java new file mode 100644 index 0000000..51f03d1 --- /dev/null +++ b/src/main/java/org/springframework/repeat/exception/LogOrRethrowExceptionHandler.java @@ -0,0 +1,111 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.exception; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.classify.Classifier; +import org.springframework.classify.ClassifierSupport; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatException; + +/** + * Implementation of {@link ExceptionHandler} based on an {@link Classifier}. + * The classifier determines whether to log the exception or rethrow it. The + * keys in the classifier must be the same as the static enum in this class. + * + * @author Dave Syer + * + */ +public class LogOrRethrowExceptionHandler implements ExceptionHandler { + + /** + * Logging levels for the handler. + * + * @author Dave Syer + * + */ + public static enum Level { + + /** + * Key for {@link Classifier} signalling that the throwable should be + * rethrown. If the throwable is not a RuntimeException it is wrapped in + * a {@link RepeatException}. + */ + RETHROW, + + /** + * Key for {@link Classifier} signalling that the throwable should be + * logged at debug level. + */ + DEBUG, + + /** + * Key for {@link Classifier} signalling that the throwable should be + * logged at warn level. + */ + WARN, + + /** + * Key for {@link Classifier} signalling that the throwable should be + * logged at error level. + */ + ERROR + + } + + protected final Log logger = LogFactory.getLog(LogOrRethrowExceptionHandler.class); + + private Classifier exceptionClassifier = new ClassifierSupport(Level.RETHROW); + + /** + * Setter for the {@link Classifier} used by this handler. The default is to + * map all throwable instances to {@link Level#RETHROW}. + * + * @param exceptionClassifier the ExceptionClassifier to use + */ + public void setExceptionClassifier(Classifier exceptionClassifier) { + this.exceptionClassifier = exceptionClassifier; + } + + /** + * Classify the throwables and decide whether to rethrow based on the + * result. The context is not used. + * + * @throws Throwable + * + * @see ExceptionHandler#handleException(RepeatContext, Throwable) + */ + public void handleException(RepeatContext context, Throwable throwable) throws Throwable { + + Level key = exceptionClassifier.classify(throwable); + if (Level.ERROR.equals(key)) { + logger.error("Exception encountered in batch repeat.", throwable); + } + else if (Level.WARN.equals(key)) { + logger.warn("Exception encountered in batch repeat.", throwable); + } + else if (Level.DEBUG.equals(key) && logger.isDebugEnabled()) { + logger.debug("Exception encountered in batch repeat.", throwable); + } + else if (Level.RETHROW.equals(key)) { + throw throwable; + } + + } + +} diff --git a/src/main/java/org/springframework/repeat/exception/RethrowOnThresholdExceptionHandler.java b/src/main/java/org/springframework/repeat/exception/RethrowOnThresholdExceptionHandler.java new file mode 100644 index 0000000..22b61a9 --- /dev/null +++ b/src/main/java/org/springframework/repeat/exception/RethrowOnThresholdExceptionHandler.java @@ -0,0 +1,150 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.exception; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.classify.Classifier; +import org.springframework.classify.SubclassClassifier; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.context.RepeatContextCounter; +import org.springframework.util.ObjectUtils; + +/** + * Implementation of {@link ExceptionHandler} that rethrows when exceptions of a + * given type reach a threshold. Requires an {@link Classifier} that maps + * exception types to unique keys, and also a map from those keys to threshold + * values (Integer type). + * + * @author Dave Syer + * + */ +public class RethrowOnThresholdExceptionHandler implements ExceptionHandler { + + protected static final IntegerHolder ZERO = new IntegerHolder(0); + + protected final Log logger = LogFactory.getLog(RethrowOnThresholdExceptionHandler.class); + + private Classifier exceptionClassifier = new Classifier() { + public RethrowOnThresholdExceptionHandler.IntegerHolder classify(Throwable classifiable) { + return ZERO; + } + }; + + private boolean useParent = false; + + /** + * Flag to indicate the the exception counters should be shared between + * sibling contexts in a nested batch. Default is false. + * + * @param useParent true if the parent context should be used to store the + * counters. + */ + public void setUseParent(boolean useParent) { + this.useParent = useParent; + } + + /** + * Set up the exception handler. Creates a default exception handler and + * threshold that maps all exceptions to a threshold of 0 - all exceptions + * are rethrown by default. + */ + public RethrowOnThresholdExceptionHandler() { + super(); + } + + /** + * A map from exception classes to a threshold value of type Integer. + * + * @param thresholds the threshold value map. + */ + public void setThresholds(Map, Integer> thresholds) { + Map, IntegerHolder> typeMap = new HashMap, IntegerHolder>(); + for (Entry, Integer> entry : thresholds.entrySet()) { + typeMap.put(entry.getKey(), new IntegerHolder(entry.getValue())); + } + exceptionClassifier = new SubclassClassifier(typeMap, ZERO); + } + + /** + * Classify the throwables and decide whether to re-throw based on the + * result. The context is used to accumulate the number of exceptions of the + * same type according to the classifier. + * + * @throws Throwable + * @see ExceptionHandler#handleException(RepeatContext, Throwable) + */ + public void handleException(RepeatContext context, Throwable throwable) throws Throwable { + + IntegerHolder key = exceptionClassifier.classify(throwable); + + RepeatContextCounter counter = getCounter(context, key); + counter.increment(); + int count = counter.getCount(); + int threshold = key.getValue(); + if (count > threshold) { + throw throwable; + } + + } + + private RepeatContextCounter getCounter(RepeatContext context, IntegerHolder key) { + String attribute = RethrowOnThresholdExceptionHandler.class.getName() + "." + key; + // Creates a new counter and stores it in the correct context: + return new RepeatContextCounter(context, attribute, useParent); + } + + /** + * @author Dave Syer + * + */ + private static class IntegerHolder { + + private final int value; + + /** + * @param value + */ + public IntegerHolder(int value) { + this.value = value; + } + + /** + * Public getter for the value. + * @return the value + */ + public int getValue() { + return value; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return ObjectUtils.getIdentityHexString(this) + "." + value; + } + + } + +} diff --git a/src/main/java/org/springframework/repeat/exception/SimpleLimitExceptionHandler.java b/src/main/java/org/springframework/repeat/exception/SimpleLimitExceptionHandler.java new file mode 100644 index 0000000..1786091 --- /dev/null +++ b/src/main/java/org/springframework/repeat/exception/SimpleLimitExceptionHandler.java @@ -0,0 +1,147 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.exception; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.repeat.RepeatContext; + +/** + * Simple implementation of exception handler which looks for given exception + * types. If one of the types is found then a counter is incremented and the + * limit is checked to determine if it has been exceeded and the Throwable + * should be re-thrown. Also allows to specify list of 'fatal' exceptions that + * are never subject to counting, but are immediately re-thrown. The fatal list + * has higher priority so the two lists needn't be exclusive. + * + * @author Dave Syer + * @author Robert Kasanicky + */ +public class SimpleLimitExceptionHandler implements ExceptionHandler, InitializingBean { + + private RethrowOnThresholdExceptionHandler delegate = new RethrowOnThresholdExceptionHandler(); + + private Collection> exceptionClasses = Collections + .> singleton(Exception.class); + + private Collection> fatalExceptionClasses = Collections + .> singleton(Error.class); + + private int limit = 0; + + /** + * Apply the provided properties to create a delegate handler. + * + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() throws Exception { + if (limit <= 0) { + return; + } + Map, Integer> thresholds = new HashMap, Integer>(); + for (Class type : exceptionClasses) { + thresholds.put(type, limit); + } + // do the fatalExceptionClasses last so they override the others + for (Class type : fatalExceptionClasses) { + thresholds.put(type, 0); + } + delegate.setThresholds(thresholds); + } + + /** + * Flag to indicate the the exception counters should be shared between + * sibling contexts in a nested batch (i.e. inner loop). Default is false. + * Set this flag to true if you want to count exceptions for the whole + * (outer) loop in a typical container. + * + * @param useParent true if the parent context should be used to store the + * counters. + */ + public void setUseParent(boolean useParent) { + delegate.setUseParent(useParent); + } + + /** + * Convenience constructor for the {@link SimpleLimitExceptionHandler} to + * set the limit. + * + * @param limit the limit + */ + public SimpleLimitExceptionHandler(int limit) { + this(); + this.limit = limit; + } + + /** + * Default constructor for the {@link SimpleLimitExceptionHandler}. + */ + public SimpleLimitExceptionHandler() { + super(); + } + + /** + * Rethrows only if the limit is breached for this context on the exception + * type specified. + * + * @see #setExceptionClasses(Collection) + * @see #setLimit(int) + * + * @see org.springframework.repeat.exception.ExceptionHandler#handleException(org.springframework.repeat.RepeatContext, + * Throwable) + */ + public void handleException(RepeatContext context, Throwable throwable) throws Throwable { + delegate.handleException(context, throwable); + } + + /** + * The limit on the given exception type within a single context before it + * is rethrown. + * + * @param limit the limit + */ + public void setLimit(final int limit) { + this.limit = limit; + } + + /** + * Setter for the exception classes that this handler counts. Defaults to + * {@link Exception}. If more exceptionClasses are specified handler uses + * single counter that is incremented when one of the recognized exception + * exceptionClasses is handled. + * @param classes exceptionClasses + */ + public void setExceptionClasses(Collection> classes) { + this.exceptionClasses = classes; + } + + /** + * Setter for the exception classes that shouldn't be counted, but rethrown + * immediately. This list has higher priority than + * {@link #setExceptionClasses(Collection)}. + * + * @param fatalExceptionClasses defaults to {@link Error} + */ + public void setFatalExceptionClasses(Collection> fatalExceptionClasses) { + this.fatalExceptionClasses = fatalExceptionClasses; + } + +} diff --git a/src/main/java/org/springframework/repeat/exception/package.html b/src/main/java/org/springframework/repeat/exception/package.html new file mode 100644 index 0000000..edbc051 --- /dev/null +++ b/src/main/java/org/springframework/repeat/exception/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat exception handler concerns. +

+ + diff --git a/src/main/java/org/springframework/repeat/interceptor/RepeatOperationsInterceptor.java b/src/main/java/org/springframework/repeat/interceptor/RepeatOperationsInterceptor.java new file mode 100644 index 0000000..37c5c3b --- /dev/null +++ b/src/main/java/org/springframework/repeat/interceptor/RepeatOperationsInterceptor.java @@ -0,0 +1,203 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.interceptor; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.ProxyMethodInvocation; +import org.springframework.repeat.RepeatCallback; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatException; +import org.springframework.repeat.RepeatOperations; +import org.springframework.repeat.RepeatStatus; +import org.springframework.repeat.support.RepeatTemplate; +import org.springframework.util.Assert; + +/** + * A {@link MethodInterceptor} that can be used to automatically repeat calls to + * a method on a service. The injected {@link RepeatOperations} is used to + * control the completion of the loop. Independent of the completion policy in + * the {@link RepeatOperations} the loop will repeat until the target method + * returns null or false. Be careful when injecting a bespoke + * {@link RepeatOperations} that the loop will actually terminate, because the + * default policy for a vanilla {@link RepeatTemplate} will never complete if + * the return type of the target method is void (the value returned is always + * not-null, representing the {@link Void#TYPE}). + * + * @author Dave Syer + */ +public class RepeatOperationsInterceptor implements MethodInterceptor { + + private RepeatOperations repeatOperations = new RepeatTemplate(); + + /** + * Setter for the {@link RepeatOperations}. + * + * @param batchTempate + * @throws IllegalArgumentException if the argument is null. + */ + public void setRepeatOperations(RepeatOperations batchTempate) { + Assert.notNull(batchTempate, "'repeatOperations' cannot be null."); + this.repeatOperations = batchTempate; + } + + /** + * Invoke the proceeding method call repeatedly, according to the properties + * of the injected {@link RepeatOperations}. + * + * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) + */ + public Object invoke(final MethodInvocation invocation) throws Throwable { + + final ResultHolder result = new ResultHolder(); + // Cache void return value if intercepted method returns void + final boolean voidReturnType = Void.TYPE.equals(invocation.getMethod().getReturnType()); + if (voidReturnType) { + // This will be ignored anyway, but we want it to be non-null for + // convenience of checking that there is a result. + result.setValue(new Object()); + } + + try { + repeatOperations.iterate(new RepeatCallback() { + + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + try { + + MethodInvocation clone = invocation; + if (invocation instanceof ProxyMethodInvocation) { + clone = ((ProxyMethodInvocation) invocation).invocableClone(); + } + else { + throw new IllegalStateException( + "MethodInvocation of the wrong type detected - this should not happen with Spring AOP, so please raise an issue if you see this exception"); + } + + Object value = clone.proceed(); + if (voidReturnType) { + return RepeatStatus.CONTINUABLE; + } + if (!isComplete(value)) { + // Save the last result + result.setValue(value); + return RepeatStatus.CONTINUABLE; + } + else { + result.setFinalValue(value); + return RepeatStatus.FINISHED; + } + } + catch (Throwable e) { + if (e instanceof Exception) { + throw (Exception) e; + } + else { + throw new RepeatOperationsInterceptorException("Unexpected error in batch interceptor", e); + } + } + } + + }); + } + catch (Throwable t) { + // The repeat exception should be unwrapped by the template + throw t; + } + + if (result.isReady()) { + return result.getValue(); + } + + // No result means something weird happened + throw new IllegalStateException("No result available for attempted repeat call to " + invocation + + ". The invocation was never called, so maybe there is a problem with the completion policy?"); + } + + /** + * @param result + * @return + */ + private boolean isComplete(Object result) { + return result == null || (result instanceof Boolean) && !((Boolean) result).booleanValue(); + } + + /** + * Simple wrapper exception class to enable nasty errors to be passed out of + * the scope of the repeat operations and handled by the caller. + * + * @author Dave Syer + * + */ + private static class RepeatOperationsInterceptorException extends RepeatException { + /** + * @param message + * @param e + */ + public RepeatOperationsInterceptorException(String message, Throwable e) { + super(message, e); + } + } + + /** + * Simple wrapper object for the result from a method invocation. + * + * @author Dave Syer + * + */ + private static class ResultHolder { + private Object value = null; + + private boolean ready = false; + + /** + * Public setter for the Object. + * @param value the value to set + */ + public void setValue(Object value) { + this.ready = true; + this.value = value; + } + + /** + * @param value + */ + public void setFinalValue(Object value) { + if (ready) { + // Only set the value the last time if the last time was also + // the first time + return; + } + setValue(value); + } + + /** + * Public getter for the Object. + * @return the value + */ + public Object getValue() { + return value; + } + + /** + * @return true if a value has been set + */ + public boolean isReady() { + return ready; + } + } + +} diff --git a/src/main/java/org/springframework/repeat/interceptor/package.html b/src/main/java/org/springframework/repeat/interceptor/package.html new file mode 100644 index 0000000..bb348ce --- /dev/null +++ b/src/main/java/org/springframework/repeat/interceptor/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat aop concerns. +

+ + diff --git a/src/main/java/org/springframework/repeat/listener/CompositeRepeatListener.java b/src/main/java/org/springframework/repeat/listener/CompositeRepeatListener.java new file mode 100644 index 0000000..57c3204 --- /dev/null +++ b/src/main/java/org/springframework/repeat/listener/CompositeRepeatListener.java @@ -0,0 +1,99 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.repeat.listener; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatListener; +import org.springframework.repeat.RepeatStatus; + +/** + * @author Dave Syer + * + */ +public class CompositeRepeatListener implements RepeatListener { + + private List listeners = new ArrayList(); + + /** + * Public setter for the listeners. + * + * @param listeners + */ + public void setListeners(RepeatListener[] listeners) { + this.listeners = Arrays.asList(listeners); + } + + /** + * Register additional listener. + * + * @param listener + */ + public void register(RepeatListener listener) { + if (!listeners.contains(listener)) { + listeners.add(listener); + } + } + + /* (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatListener#after(org.springframework.batch.repeat.RepeatContext, org.springframework.batch.repeat.ExitStatus) + */ + public void after(RepeatContext context, RepeatStatus result) { + for (RepeatListener listener : listeners) { + listener.after(context, result); + } + } + + /* (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatListener#before(org.springframework.batch.repeat.RepeatContext) + */ + public void before(RepeatContext context) { + for (RepeatListener listener : listeners) { + listener.before(context); + } + } + + /* (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatListener#close(org.springframework.batch.repeat.RepeatContext) + */ + public void close(RepeatContext context) { + for (RepeatListener listener : listeners) { + listener.close(context); + } + } + + /* (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatListener#onError(org.springframework.batch.repeat.RepeatContext, java.lang.Throwable) + */ + public void onError(RepeatContext context, Throwable e) { + for (RepeatListener listener : listeners) { + listener.onError(context, e); + } + } + + /* (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatListener#open(org.springframework.batch.repeat.RepeatContext) + */ + public void open(RepeatContext context) { + for (RepeatListener listener : listeners) { + listener.open(context); + } + } + +} diff --git a/src/main/java/org/springframework/repeat/listener/RepeatListenerSupport.java b/src/main/java/org/springframework/repeat/listener/RepeatListenerSupport.java new file mode 100644 index 0000000..8335adb --- /dev/null +++ b/src/main/java/org/springframework/repeat/listener/RepeatListenerSupport.java @@ -0,0 +1,46 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.listener; + +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatListener; +import org.springframework.repeat.RepeatStatus; + +/** + * Empty method implementation of {@link RepeatListener}. + * + * @author Dave Syer + * + */ +public class RepeatListenerSupport implements RepeatListener { + + public void before(RepeatContext context) { + } + + public void after(RepeatContext context, RepeatStatus result) { + } + + public void close(RepeatContext context) { + } + + public void onError(RepeatContext context, Throwable e) { + } + + public void open(RepeatContext context) { + } + +} diff --git a/src/main/java/org/springframework/repeat/listener/package.html b/src/main/java/org/springframework/repeat/listener/package.html new file mode 100644 index 0000000..8a95d83 --- /dev/null +++ b/src/main/java/org/springframework/repeat/listener/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat interceptor concerns. +

+ + diff --git a/src/main/java/org/springframework/repeat/package.html b/src/main/java/org/springframework/repeat/package.html new file mode 100644 index 0000000..531b1f8 --- /dev/null +++ b/src/main/java/org/springframework/repeat/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat concerns. +

+ + diff --git a/src/main/java/org/springframework/repeat/policy/CompletionPolicySupport.java b/src/main/java/org/springframework/repeat/policy/CompletionPolicySupport.java new file mode 100644 index 0000000..952dfe9 --- /dev/null +++ b/src/main/java/org/springframework/repeat/policy/CompletionPolicySupport.java @@ -0,0 +1,77 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.policy; + +import org.springframework.repeat.CompletionPolicy; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatStatus; +import org.springframework.repeat.context.RepeatContextSupport; + +/** + * Very simple base class for {@link CompletionPolicy} implementations. + * + * @author Dave Syer + * + */ +public class CompletionPolicySupport implements CompletionPolicy { + + /** + * If exit status is not continuable return true, otherwise + * delegate to {@link #isComplete(RepeatContext)}. + * + * @see org.springframework.repeat.CompletionPolicy#isComplete(org.springframework.repeat.RepeatContext, + * RepeatStatus) + */ + public boolean isComplete(RepeatContext context, RepeatStatus result) { + if (result != null && !result.isContinuable()) { + return true; + } + else { + return isComplete(context); + } + } + + /** + * Always true. + * + * @see org.springframework.repeat.CompletionPolicy#isComplete(org.springframework.repeat.RepeatContext) + */ + public boolean isComplete(RepeatContext context) { + return true; + } + + /** + * Build a new {@link RepeatContextSupport} and return it. + * + * @see org.springframework.repeat.CompletionPolicy#start(RepeatContext) + */ + public RepeatContext start(RepeatContext context) { + return new RepeatContextSupport(context); + } + + /** + * Increment the context so the counter is up to date. Do nothing else. + * + * @see org.springframework.repeat.CompletionPolicy#update(org.springframework.repeat.RepeatContext) + */ + public void update(RepeatContext context) { + if (context instanceof RepeatContextSupport) { + ((RepeatContextSupport) context).increment(); + } + } + +} diff --git a/src/main/java/org/springframework/repeat/policy/CompositeCompletionPolicy.java b/src/main/java/org/springframework/repeat/policy/CompositeCompletionPolicy.java new file mode 100644 index 0000000..589c6ae --- /dev/null +++ b/src/main/java/org/springframework/repeat/policy/CompositeCompletionPolicy.java @@ -0,0 +1,133 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.policy; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.repeat.CompletionPolicy; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatStatus; +import org.springframework.repeat.context.RepeatContextSupport; + +/** + * Composite policy that loops through a list of delegate policies and answers + * calls by a concensus. + * + * @author Dave Syer + * + */ +public class CompositeCompletionPolicy implements CompletionPolicy { + + CompletionPolicy[] policies = new CompletionPolicy[0]; + + /** + * Setter for the policies. + * + * @param policies + */ + public void setPolicies(CompletionPolicy[] policies) { + this.policies = Arrays.asList(policies).toArray(new CompletionPolicy[policies.length]); + } + + /** + * This policy is complete if any of the composed policies is complete. + * + * @see org.springframework.repeat.CompletionPolicy#isComplete(org.springframework.repeat.RepeatContext, + * RepeatStatus) + */ + public boolean isComplete(RepeatContext context, RepeatStatus result) { + RepeatContext[] contexts = ((CompositeBatchContext) context).contexts; + CompletionPolicy[] policies = ((CompositeBatchContext) context).policies; + for (int i = 0; i < policies.length; i++) { + if (policies[i].isComplete(contexts[i], result)) { + return true; + } + } + return false; + } + + /** + * This policy is complete if any of the composed policies is complete. + * + * @see org.springframework.repeat.CompletionPolicy#isComplete(org.springframework.repeat.RepeatContext) + */ + public boolean isComplete(RepeatContext context) { + RepeatContext[] contexts = ((CompositeBatchContext) context).contexts; + CompletionPolicy[] policies = ((CompositeBatchContext) context).policies; + for (int i = 0; i < policies.length; i++) { + if (policies[i].isComplete(contexts[i])) { + return true; + } + } + return false; + } + + /** + * Create a new composite context from all the available policies. + * + * @see org.springframework.repeat.CompletionPolicy#start(RepeatContext) + */ + public RepeatContext start(RepeatContext context) { + List list = new ArrayList(); + for (int i = 0; i < policies.length; i++) { + list.add(policies[i].start(context)); + } + return new CompositeBatchContext(context, list); + + } + + /** + * Update all the composed contexts, and also increment the parent context. + * + * @see org.springframework.repeat.CompletionPolicy#update(org.springframework.repeat.RepeatContext) + */ + public void update(RepeatContext context) { + RepeatContext[] contexts = ((CompositeBatchContext) context).contexts; + CompletionPolicy[] policies = ((CompositeBatchContext) context).policies; + for (int i = 0; i < policies.length; i++) { + policies[i].update(contexts[i]); + } + ((RepeatContextSupport) context).increment(); + } + + /** + * Composite context that knows about the policies and contexts is was + * created with. + * + * @author Dave Syer + * + */ + protected class CompositeBatchContext extends RepeatContextSupport { + + private RepeatContext[] contexts; + + // Save a reference to the policies when we were created - gives some + // protection against reference changes (e.g. if the number of policies + // change). + private CompletionPolicy[] policies; + + public CompositeBatchContext(RepeatContext context, List contexts) { + super(context); + this.contexts = contexts.toArray(new RepeatContext[contexts.size()]); + this.policies = CompositeCompletionPolicy.this.policies; + } + + } + +} diff --git a/src/main/java/org/springframework/repeat/policy/CountingCompletionPolicy.java b/src/main/java/org/springframework/repeat/policy/CountingCompletionPolicy.java new file mode 100644 index 0000000..528a6c3 --- /dev/null +++ b/src/main/java/org/springframework/repeat/policy/CountingCompletionPolicy.java @@ -0,0 +1,132 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.policy; + +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.context.RepeatContextCounter; +import org.springframework.repeat.context.RepeatContextSupport; + +/** + * Abstract base class for policies that need to count the number of occurrences + * of some event (e.g. an exception type in the context), and terminate based on + * a limit for the counter. The value of the counter can be stored between + * batches in a nested context, so that the termination decision is based on the + * aggregate of a number of sibling batches. + * + * @author Dave Syer + * + */ +public abstract class CountingCompletionPolicy extends DefaultResultCompletionPolicy { + + /** + * Session key for global counter. + */ + public static final String COUNT = CountingCompletionPolicy.class.getName() + ".COUNT"; + + private boolean useParent = false; + + private int maxCount = 0; + + /** + * Flag to indicate whether the count is at the level of the parent context, + * or just local to the context. If true then the count is aggregated among + * siblings in a nested batch. + * + * @param useParent whether to use the parent context to cache the total + * count. Default value is false. + */ + public void setUseParent(boolean useParent) { + this.useParent = useParent; + } + + /** + * Setter for maximum value of count before termination. + * + * @param maxCount the maximum number of counts before termination. Default + * 0 so termination is immediate. + */ + public void setMaxCount(int maxCount) { + this.maxCount = maxCount; + } + + /** + * Extension point for subclasses. Obtain the value of the count in the + * current context. Subclasses can count the number of attempts or + * violations and store the result in their context. This policy base class + * will take care of the termination contract and aggregating at the level + * of the session if required. + * + * @param context the current context, specific to the subclass. + * @return the value of the counter in the context. + */ + protected abstract int getCount(RepeatContext context); + + /** + * Extension point for subclasses. Inspect the context and update the state + * of a counter in whatever way is appropriate. This will be added to the + * session-level counter if {@link #setUseParent(boolean)} is true. + * + * @param context the current context. + * + * @return the change in the value of the counter (default 0). + */ + protected int doUpdate(RepeatContext context) { + return 0; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.policy.CompletionPolicySupport#isComplete(org.springframework.batch.repeat.BatchContext) + */ + final public boolean isComplete(RepeatContext context) { + int count = ((CountingBatchContext) context).getCounter().getCount(); + return count >= maxCount; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.policy.CompletionPolicySupport#start(org.springframework.batch.repeat.BatchContext) + */ + public RepeatContext start(RepeatContext parent) { + return new CountingBatchContext(parent); + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.policy.CompletionPolicySupport#update(org.springframework.batch.repeat.BatchContext) + */ + final public void update(RepeatContext context) { + super.update(context); + int delta = doUpdate(context); + ((CountingBatchContext) context).getCounter().increment(delta); + } + + protected class CountingBatchContext extends RepeatContextSupport { + + RepeatContextCounter counter; + + public CountingBatchContext(RepeatContext parent) { + super(parent); + counter = new RepeatContextCounter(this, COUNT, useParent); + } + + public RepeatContextCounter getCounter() { + return counter; + } + + } +} diff --git a/src/main/java/org/springframework/repeat/policy/DefaultResultCompletionPolicy.java b/src/main/java/org/springframework/repeat/policy/DefaultResultCompletionPolicy.java new file mode 100644 index 0000000..782c7b7 --- /dev/null +++ b/src/main/java/org/springframework/repeat/policy/DefaultResultCompletionPolicy.java @@ -0,0 +1,53 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.policy; + +import org.springframework.repeat.CompletionPolicy; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatStatus; + +/** + * Very simple {@link CompletionPolicy} that bases its decision on the result of + * a batch operation. If the result is null or not continuable according to the + * {@link RepeatStatus} the batch is complete, otherwise not. + * + * @author Dave Syer + * + */ +public class DefaultResultCompletionPolicy extends CompletionPolicySupport { + + /** + * True if the result is null, or a {@link RepeatStatus} indicating + * completion. + * + * @see org.springframework.repeat.CompletionPolicy#isComplete(org.springframework.repeat.RepeatContext, + * RepeatStatus) + */ + public boolean isComplete(RepeatContext context, RepeatStatus result) { + return (result == null || !result.isContinuable()); + } + + /** + * Always false. + * + * @see org.springframework.repeat.CompletionPolicy#isComplete(org.springframework.repeat.RepeatContext) + */ + public boolean isComplete(RepeatContext context) { + return false; + } + +} diff --git a/src/main/java/org/springframework/repeat/policy/SimpleCompletionPolicy.java b/src/main/java/org/springframework/repeat/policy/SimpleCompletionPolicy.java new file mode 100644 index 0000000..d0aa862 --- /dev/null +++ b/src/main/java/org/springframework/repeat/policy/SimpleCompletionPolicy.java @@ -0,0 +1,115 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.policy; + +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatStatus; +import org.springframework.repeat.context.RepeatContextSupport; +import org.springframework.repeat.support.RepeatTemplate; +import org.springframework.util.ClassUtils; + +/** + * Policy for terminating a batch after a fixed number of operations. Internal + * state is maintained and a counter incremented, so successful use of this + * policy requires that isComplete() is only called once per batch item. Using + * the standard {@link RepeatTemplate} should ensure this contract is kept, but it needs + * to be carefully monitored. + * + * @author Dave Syer + * + */ +public class SimpleCompletionPolicy extends DefaultResultCompletionPolicy { + + public static final int DEFAULT_CHUNK_SIZE = 5; + + int chunkSize = 0; + + public SimpleCompletionPolicy() { + this(DEFAULT_CHUNK_SIZE); + } + + public SimpleCompletionPolicy(int chunkSize) { + super(); + this.chunkSize = chunkSize; + } + + public void setChunkSize(int chunkSize) { + this.chunkSize = chunkSize; + } + + /** + * Reset the counter. + * + * @see org.springframework.repeat.CompletionPolicy#start(RepeatContext) + */ + public RepeatContext start(RepeatContext context) { + return new SimpleTerminationContext(context); + } + + /** + * Terminate if the chunk size has been reached, or the result is null. + * + * @see org.springframework.repeat.CompletionPolicy#isComplete(RepeatContext, + * RepeatStatus) + * @throws RuntimeException (normally terminating the batch) if the result is + * itself an exception. + */ + public boolean isComplete(RepeatContext context, RepeatStatus result) { + return super.isComplete(context, result) || ((SimpleTerminationContext) context).isComplete(); + } + + /** + * Terminate if the chunk size has been reached. + * + * @see org.springframework.repeat.CompletionPolicy#isComplete(RepeatContext) + */ + public boolean isComplete(RepeatContext context) { + return ((SimpleTerminationContext) context).isComplete(); + } + + /** + * Increment the counter in the context. + * + * @see org.springframework.repeat.CompletionPolicy#update(RepeatContext) + */ + public void update(RepeatContext context) { + ((SimpleTerminationContext) context).update(); + } + + protected class SimpleTerminationContext extends RepeatContextSupport { + + public SimpleTerminationContext(RepeatContext context) { + super(context); + } + + public void update() { + increment(); + } + + public boolean isComplete() { + return getStartedCount() >= chunkSize; + } + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + public String toString() { + return ClassUtils.getShortName(SimpleCompletionPolicy.class)+": chunkSize="+chunkSize; + } + +} diff --git a/src/main/java/org/springframework/repeat/policy/TimeoutTerminationPolicy.java b/src/main/java/org/springframework/repeat/policy/TimeoutTerminationPolicy.java new file mode 100644 index 0000000..daf6485 --- /dev/null +++ b/src/main/java/org/springframework/repeat/policy/TimeoutTerminationPolicy.java @@ -0,0 +1,98 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.policy; + +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.context.RepeatContextSupport; + +/** + * Termination policy that times out after a fixed period. Allows graceful exit + * from a batch if the latest result comes in after the timeout expires (i.e. + * does not throw a timeout exception).
+ * + * N.B. It may often be the case that the batch governed by this policy will be + * transactional, and the transaction might have its own timeout. In this case + * the transaction might throw a timeout exception on commit if its timeout + * threshold is lower than the termination policy. + * + * @author Dave Syer + * + */ +public class TimeoutTerminationPolicy extends CompletionPolicySupport { + + /** + * Default timeout value in millisecs (the value equivalent to 30 seconds). + */ + public static final long DEFAULT_TIMEOUT = 30000L; + + private long timeout = DEFAULT_TIMEOUT; + + /** + * Default constructor. + */ + public TimeoutTerminationPolicy() { + super(); + } + + /** + * Construct a {@link TimeoutTerminationPolicy} with the specified timeout + * value (in milliseconds). + * + * @param timeout + */ + public TimeoutTerminationPolicy(long timeout) { + super(); + this.timeout = timeout; + } + + /** + * Check the timeout and complete gracefully if it has expires. + * + * @see org.springframework.repeat.CompletionPolicy#isComplete(org.springframework.repeat.RepeatContext) + */ + @Override + public boolean isComplete(RepeatContext context) { + return ((TimeoutBatchContext) context).isComplete(); + } + + /** + * Start the clock on the timeout. + * + * @see org.springframework.repeat.CompletionPolicy#start(RepeatContext) + */ + @Override + public RepeatContext start(RepeatContext context) { + return new TimeoutBatchContext(context); + } + + protected class TimeoutBatchContext extends RepeatContextSupport { + + private volatile long time = System.currentTimeMillis(); + + private final long timeout = TimeoutTerminationPolicy.this.timeout; + + public TimeoutBatchContext(RepeatContext context) { + super(context); + } + + public boolean isComplete() { + return (System.currentTimeMillis() - time) > timeout; + } + + } + +} diff --git a/src/main/java/org/springframework/repeat/policy/package.html b/src/main/java/org/springframework/repeat/policy/package.html new file mode 100644 index 0000000..e30f24f --- /dev/null +++ b/src/main/java/org/springframework/repeat/policy/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat policy concerns. +

+ + diff --git a/src/main/java/org/springframework/repeat/support/RepeatInternalState.java b/src/main/java/org/springframework/repeat/support/RepeatInternalState.java new file mode 100644 index 0000000..7767048 --- /dev/null +++ b/src/main/java/org/springframework/repeat/support/RepeatInternalState.java @@ -0,0 +1,37 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import java.util.Collection; + +/** + * Internal interface for extensions of {@link RepeatTemplate}. + * + * @author Dave Syer + * + */ +public interface RepeatInternalState { + + /** + * Returns a mutable collection of exceptions that have occurred in the + * current repeat context. Clients are expected to mutate this collection. + * + * @return the collection of exceptions being accumulated + */ + Collection getThrowables(); + +} diff --git a/src/main/java/org/springframework/repeat/support/RepeatInternalStateSupport.java b/src/main/java/org/springframework/repeat/support/RepeatInternalStateSupport.java new file mode 100644 index 0000000..d4dc2c5 --- /dev/null +++ b/src/main/java/org/springframework/repeat/support/RepeatInternalStateSupport.java @@ -0,0 +1,35 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class RepeatInternalStateSupport implements RepeatInternalState { + + // Accumulation of failed results. + private final Set throwables = new HashSet(); + + /* (non-Javadoc) + * @see org.springframework.batch.repeat.support.BatchInternalState#getThrowables() + */ + public Collection getThrowables() { + return throwables; + } + +} diff --git a/src/main/java/org/springframework/repeat/support/RepeatSynchronizationManager.java b/src/main/java/org/springframework/repeat/support/RepeatSynchronizationManager.java new file mode 100644 index 0000000..33cdc32 --- /dev/null +++ b/src/main/java/org/springframework/repeat/support/RepeatSynchronizationManager.java @@ -0,0 +1,104 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import org.springframework.repeat.RepeatCallback; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatOperations; + +/** + * Global variable support for repeat clients. Normally it is not necessary for + * clients to be aware of the surrounding environment because a + * {@link RepeatCallback} can always use the context it is passed by the + * enclosing {@link RepeatOperations}. But occasionally it might be helpful to + * have lower level access to the ongoing {@link RepeatContext} so we provide a + * global accessor here. The mutator methods ({@link #clear()} and + * {@link #register(RepeatContext)} should not be used except internally by + * {@link RepeatOperations} implementations. + * + * @author Dave Syer + * + */ +public final class RepeatSynchronizationManager { + + private static final ThreadLocal contextHolder = new ThreadLocal(); + + private RepeatSynchronizationManager() { + } + + /** + * Getter for the current context. A context is shared by all items in the + * batch, so this method is intended to return the same context object + * independent of whether the callback is running synchronously or + * asynchronously with the surrounding {@link RepeatOperations}. + * + * @return the current {@link RepeatContext} or null if there is none (if we + * are not in a batch). + */ + public static RepeatContext getContext() { + return contextHolder.get(); + } + + /** + * Convenience method to set the current repeat operation to complete if it + * exists. + */ + public static void setCompleteOnly() { + RepeatContext context = getContext(); + if (context != null) { + context.setCompleteOnly(); + } + } + + /** + * Method for registering a context - should only be used by + * {@link RepeatOperations} implementations to ensure that + * {@link #getContext()} always returns the correct value. + * + * @param context a new context at the start of a batch. + * @return the old value if there was one. + */ + public static RepeatContext register(RepeatContext context) { + RepeatContext oldSession = getContext(); + RepeatSynchronizationManager.contextHolder.set(context); + return oldSession; + } + + /** + * Clear the current context at the end of a batch - should only be used by + * {@link RepeatOperations} implementations. + * + * @return the old value if there was one. + */ + public static RepeatContext clear() { + RepeatContext context = getContext(); + RepeatSynchronizationManager.contextHolder.set(null); + return context; + } + + /** + * Set current session and all ancestors (via parent) to complete., + */ + public static void setAncestorsCompleteOnly() { + RepeatContext context = getContext(); + while (context != null) { + context.setCompleteOnly(); + context = context.getParent(); + } + } + +} diff --git a/src/main/java/org/springframework/repeat/support/RepeatTemplate.java b/src/main/java/org/springframework/repeat/support/RepeatTemplate.java new file mode 100644 index 0000000..05a8d08 --- /dev/null +++ b/src/main/java/org/springframework/repeat/support/RepeatTemplate.java @@ -0,0 +1,476 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.repeat.CompletionPolicy; +import org.springframework.repeat.RepeatCallback; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatException; +import org.springframework.repeat.RepeatListener; +import org.springframework.repeat.RepeatOperations; +import org.springframework.repeat.RepeatStatus; +import org.springframework.repeat.exception.DefaultExceptionHandler; +import org.springframework.repeat.exception.ExceptionHandler; +import org.springframework.repeat.policy.DefaultResultCompletionPolicy; +import org.springframework.util.Assert; + +/** + * Simple implementation and base class for batch templates implementing + * {@link RepeatOperations}. Provides a framework including interceptors and + * policies. Subclasses just need to provide a method that gets the next result + * and one that waits for all the results to be returned from concurrent + * processes or threads.
+ * + * N.B. the template accumulates thrown exceptions during the iteration, and + * they are all processed together when the main loop ends (i.e. finished + * processing the items). Clients that do not want to stop execution when an + * exception is thrown can use a specific {@link CompletionPolicy} that does not + * finish when exceptions are received. This is not the default behaviour.
+ * + * Clients that want to take some business action when an exception is thrown by + * the {@link RepeatCallback} can consider using a custom {@link RepeatListener} + * instead of trying to customise the {@link CompletionPolicy}. This is + * generally a friendlier interface to implement, and the + * {@link RepeatListener#after(RepeatContext, RepeatStatus)} method is passed in + * the result of the callback, which would be an instance of {@link Throwable} + * if the business processing had thrown an exception. If the exception is not + * to be propagated to the caller, then a non-default {@link CompletionPolicy} + * needs to be provided as well, but that could be off the shelf, with the + * business action implemented only in the interceptor. + * + * @author Dave Syer + * + */ +public class RepeatTemplate implements RepeatOperations { + + protected Log logger = LogFactory.getLog(getClass()); + + private RepeatListener[] listeners = new RepeatListener[] {}; + + private CompletionPolicy completionPolicy = new DefaultResultCompletionPolicy(); + + private ExceptionHandler exceptionHandler = new DefaultExceptionHandler(); + + /** + * Set the listeners for this template, registering them for callbacks at + * appropriate times in the iteration. + * + * @param listeners + */ + public void setListeners(RepeatListener[] listeners) { + this.listeners = Arrays.asList(listeners).toArray(new RepeatListener[listeners.length]); + } + + /** + * Register an additional listener. + * + * @param listener + */ + public void registerListener(RepeatListener listener) { + List list = new ArrayList(Arrays.asList(listeners)); + list.add(listener); + listeners = (RepeatListener[]) list.toArray(new RepeatListener[list.size()]); + } + + /** + * Setter for exception handler strategy. The exception handler is called at + * the end of a batch, after the {@link CompletionPolicy} has determined + * that the batch is complete. By default all exceptions are re-thrown. + * + * @see ExceptionHandler + * @see DefaultExceptionHandler + * @see #setCompletionPolicy(CompletionPolicy) + * + * @param exceptionHandler the {@link ExceptionHandler} to use. + */ + public void setExceptionHandler(ExceptionHandler exceptionHandler) { + this.exceptionHandler = exceptionHandler; + } + + /** + * Setter for policy to decide when the batch is complete. The default is to + * complete normally when the callback returns a {@link RepeatStatus} which + * is not marked as continuable, and abnormally when the callback throws an + * exception (but the decision to re-throw the exception is deferred to the + * {@link ExceptionHandler}). + * + * @see #setExceptionHandler(ExceptionHandler) + * + * @param terminationPolicy a TerminationPolicy. + * @throws IllegalArgumentException if the argument is null + */ + public void setCompletionPolicy(CompletionPolicy terminationPolicy) { + Assert.notNull(terminationPolicy); + this.completionPolicy = terminationPolicy; + } + + /** + * Execute the batch callback until the completion policy decides that we + * are finished. Wait for the whole batch to finish before returning even if + * the task executor is asynchronous. + * + * @see org.springframework.repeat.RepeatOperations#iterate(org.springframework.repeat.RepeatCallback) + */ + public RepeatStatus iterate(RepeatCallback callback) { + + RepeatContext outer = RepeatSynchronizationManager.getContext(); + + RepeatStatus result = RepeatStatus.CONTINUABLE; + try { + // This works with an asynchronous TaskExecutor: the + // interceptors have to wait for the child processes. + result = executeInternal(callback); + } + finally { + RepeatSynchronizationManager.clear(); + if (outer != null) { + RepeatSynchronizationManager.register(outer); + } + } + + return result; + } + + /** + * Internal convenience method to loop over interceptors and batch + * callbacks. + * + * @param callback the callback to process each element of the loop. + * + * @return the aggregate of {@link ContinuationPolicy#canContinue(Object)} + * for all the results from the callback. + * + */ + private RepeatStatus executeInternal(final RepeatCallback callback) { + + // Reset the termination policy if there is one... + RepeatContext context = start(); + + // Make sure if we are already marked complete before we start then no + // processing takes place. + boolean running = !isMarkedComplete(context); + + for (int i = 0; i < listeners.length; i++) { + RepeatListener interceptor = listeners[i]; + interceptor.open(context); + running = running && !isMarkedComplete(context); + if (!running) + break; + } + + // Return value, default is to allow continued processing. + RepeatStatus result = RepeatStatus.CONTINUABLE; + + RepeatInternalState state = createInternalState(context); + // This is the list of exceptions thrown by all active callbacks + Collection throwables = state.getThrowables(); + // Keep a separate list of exceptions we handled that need to be + // rethrown + Collection deferred = new ArrayList(); + + try { + + while (running) { + + /* + * Run the before interceptors here, not in the task executor so + * that they all happen in the same thread - it's easier for + * tracking batch status, amongst other things. + */ + for (int i = 0; i < listeners.length; i++) { + RepeatListener interceptor = listeners[i]; + interceptor.before(context); + // Allow before interceptors to veto the batch by setting + // flag. + running = running && !isMarkedComplete(context); + } + + // Check that we are still running (should always be true) ... + if (running) { + + try { + + result = getNextResult(context, callback, state); + executeAfterInterceptors(context, result); + + } + catch (Throwable throwable) { + doHandle(throwable, context, deferred); + } + + // N.B. the order may be important here: + if (isComplete(context, result) || isMarkedComplete(context) || !deferred.isEmpty()) { + running = false; + } + + } + + } + + result = result.and(waitForResults(state)); + for (Throwable throwable : throwables) { + doHandle(throwable, context, deferred); + } + + // Explicitly drop any references to internal state... + state = null; + + } + /* + * No need for explicit catch here - if the business processing threw an + * exception it was already handled by the helper methods. An exception + * here is necessarily fatal. + */ + finally { + + try { + + if (!deferred.isEmpty()) { + Throwable throwable = (Throwable) deferred.iterator().next(); + logger.debug("Handling fatal exception explicitly (rethrowing first of " + deferred.size() + "): " + + throwable.getClass().getName() + ": " + throwable.getMessage()); + rethrow(throwable); + } + + } + finally { + + try { + for (int i = listeners.length; i-- > 0;) { + RepeatListener interceptor = listeners[i]; + interceptor.close(context); + } + } + finally { + context.close(); + } + + } + + } + + return result; + + } + + private void doHandle(Throwable throwable, RepeatContext context, Collection deferred) { + // An exception alone is not sufficient grounds for not + // continuing + Throwable unwrappedThrowable = unwrapIfRethrown(throwable); + try { + + for (int i = listeners.length; i-- > 0;) { + RepeatListener interceptor = listeners[i]; + // This is not an error - only log at debug + // level. + logger.debug("Exception intercepted (" + (i + 1) + " of " + listeners.length + ")", unwrappedThrowable); + interceptor.onError(context, unwrappedThrowable); + } + + logger.debug("Handling exception: " + throwable.getClass().getName() + ", caused by: " + + unwrappedThrowable.getClass().getName() + ": " + unwrappedThrowable.getMessage()); + exceptionHandler.handleException(context, unwrappedThrowable); + + } + catch (Throwable handled) { + deferred.add(handled); + } + } + + /** + * Re-throws the original throwable if it is unchecked, wraps checked + * exceptions into {@link RepeatException}. + */ + private static void rethrow(Throwable throwable) throws RuntimeException { + if (throwable instanceof Error) { + throw (Error) throwable; + } + else if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } + else { + throw new RepeatException("Exception in batch process", throwable); + } + } + + /** + * Unwraps the throwable if it has been wrapped by + * {@link #rethrow(Throwable)}. + */ + private static Throwable unwrapIfRethrown(Throwable throwable) { + if (throwable instanceof RepeatException) { + return throwable.getCause(); + } + else { + return throwable; + } + } + + /** + * Create an internal state object that is used to store data needed + * internally in the scope of an iteration. Used by subclasses to manage the + * queueing and retrieval of asynchronous results. The default just provides + * an accumulation of Throwable instances for processing at the end of the + * batch. + * + * @param context the current {@link RepeatContext} + * @return a {@link RepeatInternalState} instance. + * + * @see RepeatTemplate#waitForResults(RepeatInternalState) + */ + protected RepeatInternalState createInternalState(RepeatContext context) { + return new RepeatInternalStateSupport(); + } + + /** + * Get the next completed result, possibly executing several callbacks until + * one finally finishes. Normally a subclass would have to override both + * this method and {@link #createInternalState(RepeatContext)} because the + * implementation of this method would rely on the details of the internal + * state. + * + * @param context current BatchContext. + * @param callback the callback to execute. + * @param state maintained by the implementation. + * @return a finished result. + * + * @see #isComplete(RepeatContext) + * @see #createInternalState(RepeatContext) + */ + protected RepeatStatus getNextResult(RepeatContext context, RepeatCallback callback, RepeatInternalState state) + throws Throwable { + update(context); + if (logger.isDebugEnabled()) { + logger.debug("Repeat operation about to start at count=" + context.getStartedCount()); + } + return callback.doInIteration(context); + + } + + /** + * If necessary, wait for results to come back from remote or concurrent + * processes. By default does nothing and returns true. + * + * @param state the internal state. + * @return true if {@link #canContinue(RepeatStatus)} is true for all + * results retrieved. + */ + protected boolean waitForResults(RepeatInternalState state) { + // no-op by default + return true; + } + + /** + * Check return value from batch operation. + * + * @param value the last callback result. + * @return true if the value is {@link RepeatStatus#CONTINUABLE}. + */ + protected final boolean canContinue(RepeatStatus value) { + return ((RepeatStatus) value).isContinuable(); + } + + private boolean isMarkedComplete(RepeatContext context) { + boolean complete = context.isCompleteOnly(); + if (context.getParent() != null) { + complete = complete || isMarkedComplete(context.getParent()); + } + if (complete) { + logger.debug("Repeat is complete according to context alone."); + } + return complete; + + } + + /** + * Convenience method to execute after interceptors on a callback result. + * + * @param context the current batch context. + * @param value the result of the callback to process. + */ + protected void executeAfterInterceptors(final RepeatContext context, RepeatStatus value) { + + // Don't re-throw exceptions here: let the exception handler deal with + // that... + + if (value != null && value.isContinuable()) { + for (int i = listeners.length; i-- > 0;) { + RepeatListener interceptor = listeners[i]; + interceptor.after(context, value); + } + + } + + } + + /** + * Delegate to the {@link CompletionPolicy}. + * + * @see org.springframework.repeat.CompletionPolicy#isComplete(RepeatContext, + * RepeatStatus) + */ + protected boolean isComplete(RepeatContext context, RepeatStatus result) { + boolean complete = completionPolicy.isComplete(context, result); + if (complete) { + logger.debug("Repeat is complete according to policy and result value."); + } + return complete; + } + + /** + * Delegate to {@link CompletionPolicy}. + * + * @see org.springframework.repeat.CompletionPolicy#isComplete(RepeatContext) + */ + protected boolean isComplete(RepeatContext context) { + boolean complete = completionPolicy.isComplete(context); + if (complete) { + logger.debug("Repeat is complete according to policy alone not including result."); + } + return complete; + } + + /** + * Delegate to the {@link CompletionPolicy}. + * + * @see org.springframework.repeat.CompletionPolicy#start(RepeatContext) + */ + protected RepeatContext start() { + RepeatContext parent = RepeatSynchronizationManager.getContext(); + RepeatContext context = completionPolicy.start(parent); + RepeatSynchronizationManager.register(context); + logger.debug("Starting repeat context."); + return context; + } + + /** + * Delegate to the {@link CompletionPolicy}. + * + * @see org.springframework.repeat.CompletionPolicy#update(RepeatContext) + */ + protected void update(RepeatContext context) { + completionPolicy.update(context); + } + +} diff --git a/src/main/java/org/springframework/repeat/support/ResultHolder.java b/src/main/java/org/springframework/repeat/support/ResultHolder.java new file mode 100644 index 0000000..a2d73be --- /dev/null +++ b/src/main/java/org/springframework/repeat/support/ResultHolder.java @@ -0,0 +1,52 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatStatus; + +/** + * Interface for result holder. + * + * @author Dave Syer + */ +interface ResultHolder { + /** + * Get the result for client from this holder. Does not block if none is + * available yet. + * + * @return the result, or null if there is none. + * @throws IllegalStateException + */ + RepeatStatus getResult(); + + /** + * Get the error for client from this holder if any. Does not block if + * none is available yet. + * + * @return the error, or null if there is none. + * @throws IllegalStateException + */ + Throwable getError(); + + /** + * Get the context in which the result evaluation is executing. + * + * @return the context of the result evaluation. + */ + RepeatContext getContext(); +} \ No newline at end of file diff --git a/src/main/java/org/springframework/repeat/support/ResultHolderResultQueue.java b/src/main/java/org/springframework/repeat/support/ResultHolderResultQueue.java new file mode 100644 index 0000000..3e16597 --- /dev/null +++ b/src/main/java/org/springframework/repeat/support/ResultHolderResultQueue.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import java.util.Comparator; +import java.util.NoSuchElementException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.Semaphore; + +import org.springframework.repeat.RepeatStatus; + +/** + * An implementation of the {@link ResultQueue} that throttles the number of + * expected results, limiting it to a maximum at any given time. + * + * @author Dave Syer + */ +public class ResultHolderResultQueue implements ResultQueue { + + // Accumulation of result objects as they finish. + private final BlockingQueue results; + + // Accumulation of dummy objects flagging expected results in the future. + private final Semaphore waits; + + private final Object lock = new Object(); + + private volatile int count = 0; + + /** + * @param throttleLimit the maximum number of results that can be expected + * at any given time. + */ + public ResultHolderResultQueue(int throttleLimit) { + results = new PriorityBlockingQueue(throttleLimit, new ResultHolderComparator()); + waits = new Semaphore(throttleLimit); + } + + public boolean isEmpty() { + return results.isEmpty(); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.batch.repeat.support.ResultQueue#isExpecting() + */ + public boolean isExpecting() { + // Base the decision about whether we expect more results on a + // counter of the number of expected results actually collected. + // Do not synchronize! Otherwise put and expect can deadlock. + return count > 0; + } + + /** + * Tell the queue to expect one more result. Blocks until a new result is + * available if already expecting too many (as determined by the throttle + * limit). + * + * @see ResultQueue#expect() + */ + public void expect() throws InterruptedException { + waits.acquire(); + // Don't acquire the lock in a synchronized block - might deadlock + synchronized (lock) { + count++; + } + } + + public void put(ResultHolder holder) throws IllegalArgumentException { + if (!isExpecting()) { + throw new IllegalArgumentException("Not expecting a result. Call expect() before put()."); + } + results.add(holder); + // Take from the waits queue now to allow another result to + // accumulate. But don't decrement the counter. + waits.release(); + synchronized (lock) { + lock.notifyAll(); + } + } + + /** + * Get the next result as soon as it becomes available.
+ *
+ * Release result immediately if: + *
    + *
  • There is a result that is continuable.
  • + *
+ * Otherwise block if either: + *
    + *
  • There is no result (as per contract of {@link ResultQueue}).
  • + *
  • The number of results is less than the number expected.
  • + *
+ * Error if either: + *
    + *
  • Not expecting.
  • + *
  • Interrupted.
  • + *
+ * + * @see ResultQueue#take() + */ + public ResultHolder take() throws NoSuchElementException, InterruptedException { + if (!isExpecting()) { + throw new NoSuchElementException("Not expecting a result. Call expect() before take()."); + } + ResultHolder value; + synchronized (lock) { + value = results.take(); + if (isContinuable(value)) { + // Decrement the counter only when the result is collected. + count--; + return value; + } + } + results.put(value); + synchronized (lock) { + while (count > results.size()) { + lock.wait(); + } + value = results.take(); + count--; + } + return value; + } + + private boolean isContinuable(ResultHolder value) { + return value.getResult() != null && value.getResult().isContinuable(); + } + + /** + * Compares ResultHolders so that one that is continuable ranks lowest. + * + * @author Dave Syer + * + */ + private static class ResultHolderComparator implements Comparator { + public int compare(ResultHolder h1, ResultHolder h2) { + RepeatStatus result1 = h1.getResult(); + RepeatStatus result2 = h2.getResult(); + if (result1 == null && result2 == null) { + return 0; + } + if (result1 == null) { + return -1; + } + else if (result2 == null) { + return 1; + } + if ((result1.isContinuable() && result2.isContinuable()) + || (!result1.isContinuable() && !result2.isContinuable())) { + return 0; + } + if (result1.isContinuable()) { + return -1; + } + return 1; + } + } + +} diff --git a/src/main/java/org/springframework/repeat/support/ResultQueue.java b/src/main/java/org/springframework/repeat/support/ResultQueue.java new file mode 100644 index 0000000..55a85d4 --- /dev/null +++ b/src/main/java/org/springframework/repeat/support/ResultQueue.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import java.util.NoSuchElementException; +import java.util.concurrent.BlockingQueue; + +import org.springframework.core.task.TaskExecutor; + +/** + * Abstraction for queue of {@link ResultHolder} objects. Acts a bit likeT a + * {@link BlockingQueue} with the ability to count the number of items it + * expects to ever hold. When clients schedule an item to be added they call + * {@link #expect()}, and then collect the result later with {@link #take()}. + * Result providers in another thread call {@link #put(Object)} to notify the + * expecting client of a new result. + * + * @author Dave Syer + * @author Ben Hale + */ +interface ResultQueue { + + /** + * In a master-slave pattern, the master calls this method paired with + * {@link #take()} to manage the flow of items. Normally a task is submitted + * for processing in another thread, at which point the master uses this + * method to keep track of the number of expected results. It has the + * personality of an counter increment, rather than a work queue, which is + * usually managed elsewhere, e.g. by a {@link TaskExecutor}.

+ * Implementations may choose to block here, if they need to limit the + * number or rate of tasks being submitted. + * + * @throws InterruptedException if the call blocks and is then interrupted. + */ + void expect() throws InterruptedException; + + /** + * Once it is expecting a result, clients call this method to satisfy the + * expectation. In a master-worker pattern, the workers call this method to + * deposit the result of a finished task on the queue for collection. + * + * @param result the result for later collection. + * + * @throws IllegalArgumentException if the queue is not expecting a new + * result + */ + void put(T result) throws IllegalArgumentException; + + /** + * Gets the next available result, blocking if there are none yet available. + * + * @return a result previously deposited + * + * @throws NoSuchElementException if there is no result expected + * @throws InterruptedException if the operation is interrupted while + * waiting + */ + T take() throws NoSuchElementException, InterruptedException; + + /** + * Used by master thread to verify that there are results available from + * {@link #take()} without possibly having to block and wait. + * + * @return true if there are no results available + */ + boolean isEmpty(); + + /** + * Check if any results are expected. Usually used by master thread to drain + * queue when it is finished. + * + * @return true if more results are expected, but possibly not yet + * available. + */ + public boolean isExpecting(); + +} diff --git a/src/main/java/org/springframework/repeat/support/TaskExecutorRepeatTemplate.java b/src/main/java/org/springframework/repeat/support/TaskExecutorRepeatTemplate.java new file mode 100644 index 0000000..dc57b2b --- /dev/null +++ b/src/main/java/org/springframework/repeat/support/TaskExecutorRepeatTemplate.java @@ -0,0 +1,324 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.repeat.RepeatCallback; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatException; +import org.springframework.repeat.RepeatOperations; +import org.springframework.repeat.RepeatStatus; +import org.springframework.util.Assert; + +/** + * Provides {@link RepeatOperations} support including interceptors that can be + * used to modify or monitor the behaviour at run time.
+ * + * This implementation is sufficient to be used to configure transactional + * behaviour for each item by making the {@link RepeatCallback} transactional, + * or for the whole batch by making the execute method transactional (but only + * then if the task executor is synchronous).
+ * + * This class is thread safe if its collaborators are thread safe (interceptors, + * terminationPolicy, callback). Normally this will be the case, but clients + * need to be aware that if the task executor is asynchronous, then the other + * collaborators should be also. In particular the {@link RepeatCallback} that + * is wrapped in the execute method must be thread safe - often it is based on + * some form of data source, which itself should be both thread safe and + * transactional (multiple threads could be accessing it at any given time, and + * each thread would have its own transaction).
+ * + * @author Dave Syer + * + */ +public class TaskExecutorRepeatTemplate extends RepeatTemplate { + + /** + * Default limit for maximum number of concurrent unfinished results allowed + * by the template. + * {@link #getNextResult(RepeatContext, RepeatCallback, RepeatInternalState)} + * . + */ + public static final int DEFAULT_THROTTLE_LIMIT = 4; + + private int throttleLimit = DEFAULT_THROTTLE_LIMIT; + + private TaskExecutor taskExecutor = new SyncTaskExecutor(); + + /** + * Public setter for the throttle limit. The throttle limit is the largest + * number of concurrent tasks that can be executing at one time - if a new + * task arrives and the throttle limit is breached we wait for one of the + * executing tasks to finish before submitting the new one to the + * {@link TaskExecutor}. Default value is {@link #DEFAULT_THROTTLE_LIMIT}. + * N.B. when used with a thread pooled {@link TaskExecutor} the thread pool + * might prevent the throttle limit actually being reached (so make the core + * pool size larger than the throttle limit if possible). + * + * @param throttleLimit the throttleLimit to set. + */ + public void setThrottleLimit(int throttleLimit) { + this.throttleLimit = throttleLimit; + } + + /** + * Setter for task executor to be used to run the individual item callbacks. + * + * @param taskExecutor a TaskExecutor + * @throws IllegalArgumentException if the argument is null + */ + public void setTaskExecutor(TaskExecutor taskExecutor) { + Assert.notNull(taskExecutor); + this.taskExecutor = taskExecutor; + } + + /** + * Use the {@link #setTaskExecutor(TaskExecutor)} to generate a result. The + * internal state in this case is a queue of unfinished result holders of + * type {@link ResultHolder}. The holder with the return value should not be + * on the queue when this method exits. The queue is scoped in the calling + * method so there is no need to synchronize access. + * + */ + protected RepeatStatus getNextResult(RepeatContext context, RepeatCallback callback, RepeatInternalState state) + throws Throwable { + + ExecutingRunnable runnable = null; + + ResultQueue queue = ((ResultQueueInternalState) state).getResultQueue(); + + do { + + /* + * Wrap the callback in a runnable that will add its result to the + * queue when it is ready. + */ + runnable = new ExecutingRunnable(callback, context, queue); + + /** + * Tell the runnable that it can expect a result. This could have + * been in-lined with the constructor, but it might block, so it's + * better to do it here, since we have the option (it's a private + * class). + */ + runnable.expect(); + + /* + * Start the task possibly concurrently / in the future. + */ + taskExecutor.execute(runnable); + + /* + * Allow termination policy to update its state. This must happen + * immediately before or after the call to the task executor. + */ + update(context); + + /* + * Keep going until we get a result that is finished, or early + * termination... + */ + } while (queue.isEmpty() && !isComplete(context)); + + /* + * N.B. If the queue is empty then take() blocks until a result appears, + * and there must be at least one because we just submitted one to the + * task executor. + */ + ResultHolder result = queue.take(); + if (result.getError() != null) { + throw result.getError(); + } + return result.getResult(); + } + + /** + * Wait for all the results to appear on the queue and execute the after + * interceptors for each one. + * + * @see org.springframework.repeat.support.RepeatTemplate#waitForResults(org.springframework.repeat.support.RepeatInternalState) + */ + protected boolean waitForResults(RepeatInternalState state) { + + ResultQueue queue = ((ResultQueueInternalState) state).getResultQueue(); + + boolean result = true; + + while (queue.isExpecting()) { + + /* + * Careful that no runnables that are not going to finish ever get + * onto the queue, else this may block forever. + */ + ResultHolder future; + try { + future = (ResultHolder) queue.take(); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RepeatException("InterruptedException while waiting for result."); + } + + if (future.getError() != null) { + state.getThrowables().add(future.getError()); + } + else { + RepeatStatus status = future.getResult(); + result = result && canContinue(status); + executeAfterInterceptors(future.getContext(), status); + } + + } + + Assert.state(queue.isEmpty(), "Future results queue should be empty at end of batch."); + + return result; + } + + protected RepeatInternalState createInternalState(RepeatContext context) { + // Queue of pending results: + return new ResultQueueInternalState(throttleLimit); + } + + /** + * A runnable that puts its result on a queue when it is done. + * + * @author Dave Syer + * + */ + private class ExecutingRunnable implements Runnable, ResultHolder { + + private final RepeatCallback callback; + + private final RepeatContext context; + + private final ResultQueue queue; + + private volatile RepeatStatus result; + + private volatile Throwable error; + + public ExecutingRunnable(RepeatCallback callback, RepeatContext context, ResultQueue queue) { + + super(); + + this.callback = callback; + this.context = context; + this.queue = queue; + + } + + /** + * Tell the queue to expect a result. + */ + public void expect() { + try { + queue.expect(); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RepeatException("InterruptedException waiting for to acquire lock on input."); + } + } + + /** + * Execute the batch callback, and store the result, or any exception + * that is thrown for retrieval later by caller. + * + * @see java.lang.Runnable#run() + */ + public void run() { + boolean clearContext = false; + try { + if (RepeatSynchronizationManager.getContext() == null) { + clearContext = true; + RepeatSynchronizationManager.register(context); + } + + if (logger.isDebugEnabled()) { + logger.debug("Repeat operation about to start at count=" + context.getStartedCount()); + } + + result = callback.doInIteration(context); + + } + catch (Exception e) { + error = e; + } + finally { + + if (clearContext) { + RepeatSynchronizationManager.clear(); + } + + queue.put(this); + + } + } + + /** + * Get the result - never blocks because the queue manages waiting for + * the task to finish. + */ + public RepeatStatus getResult() { + return result; + } + + /** + * Get the error - never blocks because the queue manages waiting for + * the task to finish. + */ + public Throwable getError() { + return error; + } + + /** + * Getter for the context. + */ + public RepeatContext getContext() { + return this.context; + } + + } + + /** + * @author Dave Syer + * + */ + private static class ResultQueueInternalState extends RepeatInternalStateSupport { + + private final ResultQueue results; + + /** + * @param throttleLimit the throttle limit for the result queue + */ + public ResultQueueInternalState(int throttleLimit) { + super(); + this.results = new ResultHolderResultQueue(throttleLimit); + } + + /** + * @return the result queue + */ + public ResultQueue getResultQueue() { + return results; + } + + } + +} diff --git a/src/main/java/org/springframework/repeat/support/ThrottleLimitResultQueue.java b/src/main/java/org/springframework/repeat/support/ThrottleLimitResultQueue.java new file mode 100644 index 0000000..dc96536 --- /dev/null +++ b/src/main/java/org/springframework/repeat/support/ThrottleLimitResultQueue.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import java.util.NoSuchElementException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.Semaphore; + +/** + * An implementation of the {@link ResultQueue} that throttles the number of + * expected results, limiting it to a maximum at any given time. + * + * @author Dave Syer + */ +public class ThrottleLimitResultQueue implements ResultQueue { + + // Accumulation of result objects as they finish. + private final BlockingQueue results; + + // Accumulation of dummy objects flagging expected results in the future. + private final Semaphore waits; + + private final Object lock = new Object(); + + private volatile int count = 0; + + /** + * @param throttleLimit the maximum number of results that can be expected + * at any given time. + */ + public ThrottleLimitResultQueue(int throttleLimit) { + results = new LinkedBlockingQueue(); + waits = new Semaphore(throttleLimit); + } + + public boolean isEmpty() { + return results.isEmpty(); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.batch.repeat.support.ResultQueue#isExpecting() + */ + public boolean isExpecting() { + // Base the decision about whether we expect more results on a + // counter of the number of expected results actually collected. + // Do not synchronize! Otherwise put and expect can deadlock. + return count > 0; + } + + /** + * Tell the queue to expect one more result. Blocks until a new result is + * available if already expecting too many (as determined by the throttle + * limit). + * + * @see ResultQueue#expect() + */ + public void expect() throws InterruptedException { + synchronized (lock) { + waits.acquire(); + count++; + } + } + + public void put(T holder) throws IllegalArgumentException { + if (!isExpecting()) { + throw new IllegalArgumentException("Not expecting a result. Call expect() before put()."); + } + // There should be no need to block here, or to use offer() + results.add(holder); + // Take from the waits queue now to allow another result to + // accumulate. But don't decrement the counter. + waits.release(); + } + + public T take() throws NoSuchElementException, InterruptedException { + if (!isExpecting()) { + throw new NoSuchElementException("Not expecting a result. Call expect() before take()."); + } + T value; + synchronized (lock) { + value = results.take(); + // Decrement the counter only when the result is collected. + count--; + } + return value; + } + +} diff --git a/src/main/java/org/springframework/repeat/support/package.html b/src/main/java/org/springframework/repeat/support/package.html new file mode 100644 index 0000000..6e3f0ee --- /dev/null +++ b/src/main/java/org/springframework/repeat/support/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat support concerns. +

+ + diff --git a/src/main/java/org/springframework/commons/retry/ExhaustedRetryException.java b/src/main/java/org/springframework/retry/ExhaustedRetryException.java similarity index 95% rename from src/main/java/org/springframework/commons/retry/ExhaustedRetryException.java rename to src/main/java/org/springframework/retry/ExhaustedRetryException.java index 2a630fd..dc2f507 100644 --- a/src/main/java/org/springframework/commons/retry/ExhaustedRetryException.java +++ b/src/main/java/org/springframework/retry/ExhaustedRetryException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.retry; +package org.springframework.retry; public class ExhaustedRetryException extends RetryException { diff --git a/src/main/java/org/springframework/commons/retry/RecoveryCallback.java b/src/main/java/org/springframework/retry/RecoveryCallback.java similarity index 95% rename from src/main/java/org/springframework/commons/retry/RecoveryCallback.java rename to src/main/java/org/springframework/retry/RecoveryCallback.java index d158dc8..ee5906a 100644 --- a/src/main/java/org/springframework/commons/retry/RecoveryCallback.java +++ b/src/main/java/org/springframework/retry/RecoveryCallback.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.commons.retry; +package org.springframework.retry; /** * Callback for stateful retry after all tries are exhausted. diff --git a/src/main/java/org/springframework/commons/retry/RetryCallback.java b/src/main/java/org/springframework/retry/RetryCallback.java similarity index 96% rename from src/main/java/org/springframework/commons/retry/RetryCallback.java rename to src/main/java/org/springframework/retry/RetryCallback.java index 167fe68..ed9e825 100644 --- a/src/main/java/org/springframework/commons/retry/RetryCallback.java +++ b/src/main/java/org/springframework/retry/RetryCallback.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.retry; +package org.springframework.retry; /** * Callback interface for an operation that can be retried using a diff --git a/src/main/java/org/springframework/commons/retry/RetryContext.java b/src/main/java/org/springframework/retry/RetryContext.java similarity index 97% rename from src/main/java/org/springframework/commons/retry/RetryContext.java rename to src/main/java/org/springframework/retry/RetryContext.java index d785066..a2906c6 100644 --- a/src/main/java/org/springframework/commons/retry/RetryContext.java +++ b/src/main/java/org/springframework/retry/RetryContext.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.retry; +package org.springframework.retry; import org.springframework.core.AttributeAccessor; diff --git a/src/main/java/org/springframework/commons/retry/RetryException.java b/src/main/java/org/springframework/retry/RetryException.java similarity index 95% rename from src/main/java/org/springframework/commons/retry/RetryException.java rename to src/main/java/org/springframework/retry/RetryException.java index 92900be..2ff3a3f 100644 --- a/src/main/java/org/springframework/commons/retry/RetryException.java +++ b/src/main/java/org/springframework/retry/RetryException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.retry; +package org.springframework.retry; import org.springframework.core.NestedRuntimeException; diff --git a/src/main/java/org/springframework/commons/retry/RetryListener.java b/src/main/java/org/springframework/retry/RetryListener.java similarity index 98% rename from src/main/java/org/springframework/commons/retry/RetryListener.java rename to src/main/java/org/springframework/retry/RetryListener.java index d8499c3..1541fcf 100644 --- a/src/main/java/org/springframework/commons/retry/RetryListener.java +++ b/src/main/java/org/springframework/retry/RetryListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.retry; +package org.springframework.retry; /** diff --git a/src/main/java/org/springframework/commons/retry/RetryOperations.java b/src/main/java/org/springframework/retry/RetryOperations.java similarity index 96% rename from src/main/java/org/springframework/commons/retry/RetryOperations.java rename to src/main/java/org/springframework/retry/RetryOperations.java index f77e825..55fc0b3 100644 --- a/src/main/java/org/springframework/commons/retry/RetryOperations.java +++ b/src/main/java/org/springframework/retry/RetryOperations.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.commons.retry; +package org.springframework.retry; -import org.springframework.commons.retry.support.DefaultRetryState; +import org.springframework.retry.support.DefaultRetryState; /** * Defines the basic set of operations implemented by {@link RetryOperations} to diff --git a/src/main/java/org/springframework/commons/retry/RetryPolicy.java b/src/main/java/org/springframework/retry/RetryPolicy.java similarity index 97% rename from src/main/java/org/springframework/commons/retry/RetryPolicy.java rename to src/main/java/org/springframework/retry/RetryPolicy.java index 273f870..0f4a182 100644 --- a/src/main/java/org/springframework/commons/retry/RetryPolicy.java +++ b/src/main/java/org/springframework/retry/RetryPolicy.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.retry; +package org.springframework.retry; /** * A {@link RetryPolicy} is responsible for allocating and managing resources diff --git a/src/main/java/org/springframework/commons/retry/RetryState.java b/src/main/java/org/springframework/retry/RetryState.java similarity index 98% rename from src/main/java/org/springframework/commons/retry/RetryState.java rename to src/main/java/org/springframework/retry/RetryState.java index 6dd1331..8089dc9 100644 --- a/src/main/java/org/springframework/commons/retry/RetryState.java +++ b/src/main/java/org/springframework/retry/RetryState.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.commons.retry; +package org.springframework.retry; /** * Stateful retry is characterised by having to recognise the items that are diff --git a/src/main/java/org/springframework/commons/retry/RetryStatistics.java b/src/main/java/org/springframework/retry/RetryStatistics.java similarity index 97% rename from src/main/java/org/springframework/commons/retry/RetryStatistics.java rename to src/main/java/org/springframework/retry/RetryStatistics.java index 098220d..b7b0409 100644 --- a/src/main/java/org/springframework/commons/retry/RetryStatistics.java +++ b/src/main/java/org/springframework/retry/RetryStatistics.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.retry; +package org.springframework.retry; /** * Interface for statistics reporting of retry attempts. Counts the number of diff --git a/src/main/java/org/springframework/commons/retry/TerminatedRetryException.java b/src/main/java/org/springframework/retry/TerminatedRetryException.java similarity index 95% rename from src/main/java/org/springframework/commons/retry/TerminatedRetryException.java rename to src/main/java/org/springframework/retry/TerminatedRetryException.java index 6642bb7..4e8680b 100644 --- a/src/main/java/org/springframework/commons/retry/TerminatedRetryException.java +++ b/src/main/java/org/springframework/retry/TerminatedRetryException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.retry; +package org.springframework.retry; public class TerminatedRetryException extends RetryException { diff --git a/src/main/java/org/springframework/commons/retry/backoff/BackOffContext.java b/src/main/java/org/springframework/retry/backoff/BackOffContext.java similarity index 93% rename from src/main/java/org/springframework/commons/retry/backoff/BackOffContext.java rename to src/main/java/org/springframework/retry/backoff/BackOffContext.java index 6f445a2..8d7249f 100644 --- a/src/main/java/org/springframework/commons/retry/backoff/BackOffContext.java +++ b/src/main/java/org/springframework/retry/backoff/BackOffContext.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.retry.backoff; +package org.springframework.retry.backoff; /** * @author Rob Harrop diff --git a/src/main/java/org/springframework/commons/retry/backoff/BackOffInterruptedException.java b/src/main/java/org/springframework/retry/backoff/BackOffInterruptedException.java similarity index 91% rename from src/main/java/org/springframework/commons/retry/backoff/BackOffInterruptedException.java rename to src/main/java/org/springframework/retry/backoff/BackOffInterruptedException.java index 9a84e1b..acc639b 100644 --- a/src/main/java/org/springframework/commons/retry/backoff/BackOffInterruptedException.java +++ b/src/main/java/org/springframework/retry/backoff/BackOffInterruptedException.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.commons.retry.backoff; +package org.springframework.retry.backoff; -import org.springframework.commons.retry.RetryException; +import org.springframework.retry.RetryException; /** * Exception class signifiying that an attempt to back off using a diff --git a/src/main/java/org/springframework/commons/retry/backoff/BackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/BackOffPolicy.java similarity index 89% rename from src/main/java/org/springframework/commons/retry/backoff/BackOffPolicy.java rename to src/main/java/org/springframework/retry/backoff/BackOffPolicy.java index 14c7857..3b7ce79 100644 --- a/src/main/java/org/springframework/commons/retry/backoff/BackOffPolicy.java +++ b/src/main/java/org/springframework/retry/backoff/BackOffPolicy.java @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.commons.retry.backoff; +package org.springframework.retry.backoff; -import org.springframework.commons.retry.RetryContext; +import org.springframework.retry.RetryContext; /** * Strategy interface to control back off between attempts in a single - * {@link org.springframework.commons.retry.support.RetryTemplate retry operation}. + * {@link org.springframework.retry.support.RetryTemplate retry operation}. *

Implementations are expected to be thread-safe and should be designed * for concurrent access. Configuration for each implementation is also expected * to be thread-safe but need not be suitable for high load concurrent access. @@ -29,7 +29,7 @@ import org.springframework.commons.retry.RetryContext; * {@link BackOffContext} that can be used to track state through subsequent * back off invocations.

Each back off process is handled via a call to * {@link #backOff}. The - * {@link org.springframework.commons.retry.support.RetryTemplate} will pass in + * {@link org.springframework.retry.support.RetryTemplate} will pass in * the corresponding {@link BackOffContext} object created by the call to * {@link #start}. * diff --git a/src/main/java/org/springframework/commons/retry/backoff/ExponentialBackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/ExponentialBackOffPolicy.java similarity index 98% rename from src/main/java/org/springframework/commons/retry/backoff/ExponentialBackOffPolicy.java rename to src/main/java/org/springframework/retry/backoff/ExponentialBackOffPolicy.java index b286e3c..1c85a48 100644 --- a/src/main/java/org/springframework/commons/retry/backoff/ExponentialBackOffPolicy.java +++ b/src/main/java/org/springframework/retry/backoff/ExponentialBackOffPolicy.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.commons.retry.backoff; +package org.springframework.retry.backoff; -import org.springframework.commons.retry.RetryContext; +import org.springframework.retry.RetryContext; import org.springframework.util.ClassUtils; /** diff --git a/src/main/java/org/springframework/commons/retry/backoff/FixedBackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java similarity index 97% rename from src/main/java/org/springframework/commons/retry/backoff/FixedBackOffPolicy.java rename to src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java index ec44217..665af8f 100644 --- a/src/main/java/org/springframework/commons/retry/backoff/FixedBackOffPolicy.java +++ b/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.retry.backoff; +package org.springframework.retry.backoff; /** diff --git a/src/main/java/org/springframework/commons/retry/backoff/NoBackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/NoBackOffPolicy.java similarity index 94% rename from src/main/java/org/springframework/commons/retry/backoff/NoBackOffPolicy.java rename to src/main/java/org/springframework/retry/backoff/NoBackOffPolicy.java index 66c99d0..231bd55 100644 --- a/src/main/java/org/springframework/commons/retry/backoff/NoBackOffPolicy.java +++ b/src/main/java/org/springframework/retry/backoff/NoBackOffPolicy.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.retry.backoff; +package org.springframework.retry.backoff; /** diff --git a/src/main/java/org/springframework/commons/retry/backoff/ObjectWaitSleeper.java b/src/main/java/org/springframework/retry/backoff/ObjectWaitSleeper.java similarity index 88% rename from src/main/java/org/springframework/commons/retry/backoff/ObjectWaitSleeper.java rename to src/main/java/org/springframework/retry/backoff/ObjectWaitSleeper.java index 10a88ad..2821c17 100644 --- a/src/main/java/org/springframework/commons/retry/backoff/ObjectWaitSleeper.java +++ b/src/main/java/org/springframework/retry/backoff/ObjectWaitSleeper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.commons.retry.backoff; +package org.springframework.retry.backoff; /** * Simple {@link Sleeper} implementation that just waits on a local Object. @@ -25,7 +25,7 @@ public class ObjectWaitSleeper implements Sleeper { /* * (non-Javadoc) - * @see org.springframework.commons.retry.backoff.Sleeper#sleep(long) + * @see org.springframework.batch.retry.backoff.Sleeper#sleep(long) */ public void sleep(long backOffPeriod) throws InterruptedException { Object mutex = new Object(); diff --git a/src/main/java/org/springframework/commons/retry/backoff/Sleeper.java b/src/main/java/org/springframework/retry/backoff/Sleeper.java similarity index 94% rename from src/main/java/org/springframework/commons/retry/backoff/Sleeper.java rename to src/main/java/org/springframework/retry/backoff/Sleeper.java index 8eccf45..4a313c9 100644 --- a/src/main/java/org/springframework/commons/retry/backoff/Sleeper.java +++ b/src/main/java/org/springframework/retry/backoff/Sleeper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.commons.retry.backoff; +package org.springframework.retry.backoff; /** * Strategy interface for backoff policies to delegate the pausing of execution. diff --git a/src/main/java/org/springframework/commons/retry/backoff/StatelessBackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/StatelessBackOffPolicy.java similarity index 93% rename from src/main/java/org/springframework/commons/retry/backoff/StatelessBackOffPolicy.java rename to src/main/java/org/springframework/retry/backoff/StatelessBackOffPolicy.java index 14ee264..3a16170 100644 --- a/src/main/java/org/springframework/commons/retry/backoff/StatelessBackOffPolicy.java +++ b/src/main/java/org/springframework/retry/backoff/StatelessBackOffPolicy.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.commons.retry.backoff; +package org.springframework.retry.backoff; -import org.springframework.commons.retry.RetryContext; +import org.springframework.retry.RetryContext; /** * Simple base class for {@link BackOffPolicy} implementations that maintain no diff --git a/src/main/java/org/springframework/retry/backoff/package.html b/src/main/java/org/springframework/retry/backoff/package.html new file mode 100644 index 0000000..53837ce --- /dev/null +++ b/src/main/java/org/springframework/retry/backoff/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry backoff concerns. +

+ + diff --git a/src/main/java/org/springframework/commons/retry/context/RetryContextSupport.java b/src/main/java/org/springframework/retry/context/RetryContextSupport.java similarity index 93% rename from src/main/java/org/springframework/commons/retry/context/RetryContextSupport.java rename to src/main/java/org/springframework/retry/context/RetryContextSupport.java index 0c5292a..8aa295a 100644 --- a/src/main/java/org/springframework/commons/retry/context/RetryContextSupport.java +++ b/src/main/java/org/springframework/retry/context/RetryContextSupport.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.springframework.commons.retry.context; +package org.springframework.retry.context; -import org.springframework.commons.retry.RetryContext; -import org.springframework.commons.retry.RetryPolicy; import org.springframework.core.AttributeAccessorSupport; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryPolicy; public class RetryContextSupport extends AttributeAccessorSupport implements RetryContext { diff --git a/src/main/java/org/springframework/retry/context/package.html b/src/main/java/org/springframework/retry/context/package.html new file mode 100644 index 0000000..d277563 --- /dev/null +++ b/src/main/java/org/springframework/retry/context/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry context concerns. +

+ + diff --git a/src/main/java/org/springframework/commons/retry/interceptor/MethodArgumentsKeyGenerator.java b/src/main/java/org/springframework/retry/interceptor/MethodArgumentsKeyGenerator.java similarity index 94% rename from src/main/java/org/springframework/commons/retry/interceptor/MethodArgumentsKeyGenerator.java rename to src/main/java/org/springframework/retry/interceptor/MethodArgumentsKeyGenerator.java index fd15b31..80c4e88 100644 --- a/src/main/java/org/springframework/commons/retry/interceptor/MethodArgumentsKeyGenerator.java +++ b/src/main/java/org/springframework/retry/interceptor/MethodArgumentsKeyGenerator.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.commons.retry.interceptor; +package org.springframework.retry.interceptor; /** * Interface that allows method parameters to be identified and tagged by a diff --git a/src/main/java/org/springframework/commons/retry/interceptor/MethodInvocationRecoverer.java b/src/main/java/org/springframework/retry/interceptor/MethodInvocationRecoverer.java similarity index 95% rename from src/main/java/org/springframework/commons/retry/interceptor/MethodInvocationRecoverer.java rename to src/main/java/org/springframework/retry/interceptor/MethodInvocationRecoverer.java index 376a6b9..e254cdc 100644 --- a/src/main/java/org/springframework/commons/retry/interceptor/MethodInvocationRecoverer.java +++ b/src/main/java/org/springframework/retry/interceptor/MethodInvocationRecoverer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.retry.interceptor; +package org.springframework.retry.interceptor; /** diff --git a/src/main/java/org/springframework/commons/retry/interceptor/NewMethodArgumentsIdentifier.java b/src/main/java/org/springframework/retry/interceptor/NewMethodArgumentsIdentifier.java similarity index 95% rename from src/main/java/org/springframework/commons/retry/interceptor/NewMethodArgumentsIdentifier.java rename to src/main/java/org/springframework/retry/interceptor/NewMethodArgumentsIdentifier.java index 92b1f2b..2d5c0c6 100644 --- a/src/main/java/org/springframework/commons/retry/interceptor/NewMethodArgumentsIdentifier.java +++ b/src/main/java/org/springframework/retry/interceptor/NewMethodArgumentsIdentifier.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.retry.interceptor; +package org.springframework.retry.interceptor; /** * Strategy interface to distinguish new arguments from ones that have been diff --git a/src/main/java/org/springframework/commons/retry/interceptor/RetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java similarity index 91% rename from src/main/java/org/springframework/commons/retry/interceptor/RetryOperationsInterceptor.java rename to src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java index 377482f..97ac085 100644 --- a/src/main/java/org/springframework/commons/retry/interceptor/RetryOperationsInterceptor.java +++ b/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package org.springframework.commons.retry.interceptor; +package org.springframework.retry.interceptor; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.ProxyMethodInvocation; -import org.springframework.commons.retry.RetryCallback; -import org.springframework.commons.retry.RetryContext; -import org.springframework.commons.retry.RetryOperations; -import org.springframework.commons.retry.support.RetryTemplate; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryOperations; +import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/commons/retry/interceptor/StatefulRetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptor.java similarity index 89% rename from src/main/java/org/springframework/commons/retry/interceptor/StatefulRetryOperationsInterceptor.java rename to src/main/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptor.java index 259fbb6..badf6d3 100644 --- a/src/main/java/org/springframework/commons/retry/interceptor/StatefulRetryOperationsInterceptor.java +++ b/src/main/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.retry.interceptor; +package org.springframework.retry.interceptor; import java.util.Arrays; @@ -22,15 +22,15 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.commons.retry.ExhaustedRetryException; -import org.springframework.commons.retry.RecoveryCallback; -import org.springframework.commons.retry.RetryCallback; -import org.springframework.commons.retry.RetryContext; -import org.springframework.commons.retry.RetryOperations; -import org.springframework.commons.retry.RetryState; -import org.springframework.commons.retry.policy.NeverRetryPolicy; -import org.springframework.commons.retry.support.DefaultRetryState; -import org.springframework.commons.retry.support.RetryTemplate; +import org.springframework.retry.ExhaustedRetryException; +import org.springframework.retry.RecoveryCallback; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryOperations; +import org.springframework.retry.RetryState; +import org.springframework.retry.policy.NeverRetryPolicy; +import org.springframework.retry.support.DefaultRetryState; +import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/src/main/java/org/springframework/retry/interceptor/package.html b/src/main/java/org/springframework/retry/interceptor/package.html new file mode 100644 index 0000000..2c89378 --- /dev/null +++ b/src/main/java/org/springframework/retry/interceptor/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry aop concerns. +

+ + diff --git a/src/main/java/org/springframework/commons/retry/listener/RetryListenerSupport.java b/src/main/java/org/springframework/retry/listener/RetryListenerSupport.java similarity index 83% rename from src/main/java/org/springframework/commons/retry/listener/RetryListenerSupport.java rename to src/main/java/org/springframework/retry/listener/RetryListenerSupport.java index a4b84f1..fbc542b 100644 --- a/src/main/java/org/springframework/commons/retry/listener/RetryListenerSupport.java +++ b/src/main/java/org/springframework/retry/listener/RetryListenerSupport.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.springframework.commons.retry.listener; +package org.springframework.retry.listener; -import org.springframework.commons.retry.RetryCallback; -import org.springframework.commons.retry.RetryContext; -import org.springframework.commons.retry.RetryListener; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryListener; /** * Empty method implementation of {@link RetryListener}. diff --git a/src/main/java/org/springframework/retry/listener/package.html b/src/main/java/org/springframework/retry/listener/package.html new file mode 100644 index 0000000..f75aebf --- /dev/null +++ b/src/main/java/org/springframework/retry/listener/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry interceptor concerns. +

+ + diff --git a/src/main/java/org/springframework/retry/package.html b/src/main/java/org/springframework/retry/package.html new file mode 100644 index 0000000..eb1cc9d --- /dev/null +++ b/src/main/java/org/springframework/retry/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry concerns. +

+ + diff --git a/src/main/java/org/springframework/commons/retry/policy/AlwaysRetryPolicy.java b/src/main/java/org/springframework/retry/policy/AlwaysRetryPolicy.java similarity index 78% rename from src/main/java/org/springframework/commons/retry/policy/AlwaysRetryPolicy.java rename to src/main/java/org/springframework/retry/policy/AlwaysRetryPolicy.java index 369922d..8871a60 100644 --- a/src/main/java/org/springframework/commons/retry/policy/AlwaysRetryPolicy.java +++ b/src/main/java/org/springframework/retry/policy/AlwaysRetryPolicy.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package org.springframework.commons.retry.policy; +package org.springframework.retry.policy; -import org.springframework.commons.retry.RetryContext; -import org.springframework.commons.retry.RetryPolicy; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryPolicy; /** * A {@link RetryPolicy} that always permits a retry. Can also be used as a base @@ -31,7 +31,7 @@ public class AlwaysRetryPolicy extends NeverRetryPolicy { /** * Always returns true. * - * @see org.springframework.commons.retry.RetryPolicy#canRetry(org.springframework.commons.retry.RetryContext) + * @see org.springframework.retry.RetryPolicy#canRetry(org.springframework.retry.RetryContext) */ public boolean canRetry(RetryContext context) { return true; diff --git a/src/main/java/org/springframework/commons/retry/policy/CompositeRetryPolicy.java b/src/main/java/org/springframework/retry/policy/CompositeRetryPolicy.java similarity index 84% rename from src/main/java/org/springframework/commons/retry/policy/CompositeRetryPolicy.java rename to src/main/java/org/springframework/retry/policy/CompositeRetryPolicy.java index 117ec71..6b1242f 100644 --- a/src/main/java/org/springframework/commons/retry/policy/CompositeRetryPolicy.java +++ b/src/main/java/org/springframework/retry/policy/CompositeRetryPolicy.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package org.springframework.commons.retry.policy; +package org.springframework.retry.policy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.springframework.commons.retry.RetryContext; -import org.springframework.commons.retry.RetryPolicy; -import org.springframework.commons.retry.context.RetryContextSupport; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryPolicy; +import org.springframework.retry.context.RetryContextSupport; /** * A {@link RetryPolicy} that composes a list of other policies and delegates @@ -49,7 +49,7 @@ public class CompositeRetryPolicy implements RetryPolicy { * created. If any of them cannot retry then return false, oetherwise return * true. * - * @see org.springframework.commons.retry.RetryPolicy#canRetry(org.springframework.commons.retry.RetryContext) + * @see org.springframework.retry.RetryPolicy#canRetry(org.springframework.retry.RetryContext) */ public boolean canRetry(RetryContext context) { RetryContext[] contexts = ((CompositeRetryContext) context).contexts; @@ -67,7 +67,7 @@ public class CompositeRetryPolicy implements RetryPolicy { * created. If any of them fails to close the exception is propagated (and * those later in the chain are closed before re-throwing). * - * @see org.springframework.commons.retry.RetryPolicy#close(org.springframework.commons.retry.RetryContext) + * @see org.springframework.retry.RetryPolicy#close(org.springframework.retry.RetryContext) */ public void close(RetryContext context) { RetryContext[] contexts = ((CompositeRetryContext) context).contexts; @@ -92,7 +92,7 @@ public class CompositeRetryPolicy implements RetryPolicy { * Creates a new context that copies the existing policies and keeps a list * of the contexts from each one. * - * @see org.springframework.commons.retry.RetryPolicy#open(RetryContext) + * @see org.springframework.retry.RetryPolicy#open(RetryContext) */ public RetryContext open(RetryContext parent) { List list = new ArrayList(); @@ -106,7 +106,7 @@ public class CompositeRetryPolicy implements RetryPolicy { * Delegate to the policies that were in operation when the context was * created. * - * @see org.springframework.commons.retry.RetryPolicy#close(org.springframework.commons.retry.RetryContext) + * @see org.springframework.retry.RetryPolicy#close(org.springframework.retry.RetryContext) */ public void registerThrowable(RetryContext context, Throwable throwable) { RetryContext[] contexts = ((CompositeRetryContext) context).contexts; diff --git a/src/main/java/org/springframework/commons/retry/policy/ExceptionClassifierRetryPolicy.java b/src/main/java/org/springframework/retry/policy/ExceptionClassifierRetryPolicy.java similarity index 85% rename from src/main/java/org/springframework/commons/retry/policy/ExceptionClassifierRetryPolicy.java rename to src/main/java/org/springframework/retry/policy/ExceptionClassifierRetryPolicy.java index 301ff19..e94e6dd 100644 --- a/src/main/java/org/springframework/commons/retry/policy/ExceptionClassifierRetryPolicy.java +++ b/src/main/java/org/springframework/retry/policy/ExceptionClassifierRetryPolicy.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package org.springframework.commons.retry.policy; +package org.springframework.retry.policy; import java.util.HashMap; import java.util.Map; -import org.springframework.commons.classify.Classifier; -import org.springframework.commons.classify.ClassifierSupport; -import org.springframework.commons.classify.SubclassClassifier; -import org.springframework.commons.retry.RetryContext; -import org.springframework.commons.retry.RetryPolicy; -import org.springframework.commons.retry.context.RetryContextSupport; +import org.springframework.classify.Classifier; +import org.springframework.classify.ClassifierSupport; +import org.springframework.classify.SubclassClassifier; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryPolicy; +import org.springframework.retry.context.RetryContextSupport; import org.springframework.util.Assert; /** @@ -66,7 +66,7 @@ public class ExceptionClassifierRetryPolicy implements RetryPolicy { /** * Delegate to the policy currently activated in the context. * - * @see org.springframework.commons.retry.RetryPolicy#canRetry(org.springframework.commons.retry.RetryContext) + * @see org.springframework.retry.RetryPolicy#canRetry(org.springframework.retry.RetryContext) */ public boolean canRetry(RetryContext context) { RetryPolicy policy = (RetryPolicy) context; @@ -76,7 +76,7 @@ public class ExceptionClassifierRetryPolicy implements RetryPolicy { /** * Delegate to the policy currently activated in the context. * - * @see org.springframework.commons.retry.RetryPolicy#close(org.springframework.commons.retry.RetryContext) + * @see org.springframework.retry.RetryPolicy#close(org.springframework.retry.RetryContext) */ public void close(RetryContext context) { RetryPolicy policy = (RetryPolicy) context; @@ -87,7 +87,7 @@ public class ExceptionClassifierRetryPolicy implements RetryPolicy { * Create an active context that proxies a retry policy by chosing a target * from the policy map. * - * @see org.springframework.commons.retry.RetryPolicy#open(RetryContext) + * @see org.springframework.retry.RetryPolicy#open(RetryContext) */ public RetryContext open(RetryContext parent) { return new ExceptionClassifierRetryContext(parent, exceptionClassifier).open(parent); @@ -96,7 +96,7 @@ public class ExceptionClassifierRetryPolicy implements RetryPolicy { /** * Delegate to the policy currently activated in the context. * - * @see org.springframework.commons.retry.RetryPolicy#registerThrowable(org.springframework.commons.retry.RetryContext, + * @see org.springframework.retry.RetryPolicy#registerThrowable(org.springframework.retry.RetryContext, * Throwable) */ public void registerThrowable(RetryContext context, Throwable throwable) { diff --git a/src/main/java/org/springframework/commons/retry/policy/MapRetryContextCache.java b/src/main/java/org/springframework/retry/policy/MapRetryContextCache.java similarity index 96% rename from src/main/java/org/springframework/commons/retry/policy/MapRetryContextCache.java rename to src/main/java/org/springframework/retry/policy/MapRetryContextCache.java index d41021c..a3f330c 100644 --- a/src/main/java/org/springframework/commons/retry/policy/MapRetryContextCache.java +++ b/src/main/java/org/springframework/retry/policy/MapRetryContextCache.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.commons.retry.policy; +package org.springframework.retry.policy; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import org.springframework.commons.retry.RetryContext; +import org.springframework.retry.RetryContext; /** * Map-based implementation of {@link RetryContextCache}. The map backing the diff --git a/src/main/java/org/springframework/commons/retry/policy/NeverRetryPolicy.java b/src/main/java/org/springframework/retry/policy/NeverRetryPolicy.java similarity index 78% rename from src/main/java/org/springframework/commons/retry/policy/NeverRetryPolicy.java rename to src/main/java/org/springframework/retry/policy/NeverRetryPolicy.java index 09d53c0..5844551 100644 --- a/src/main/java/org/springframework/commons/retry/policy/NeverRetryPolicy.java +++ b/src/main/java/org/springframework/retry/policy/NeverRetryPolicy.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.springframework.commons.retry.policy; +package org.springframework.retry.policy; -import org.springframework.commons.retry.RetryContext; -import org.springframework.commons.retry.RetryPolicy; -import org.springframework.commons.retry.context.RetryContextSupport; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryPolicy; +import org.springframework.retry.context.RetryContextSupport; /** * A {@link RetryPolicy} that allows the first attempt but never permits a @@ -34,7 +34,7 @@ public class NeverRetryPolicy implements RetryPolicy { * Returns false after the first exception. So there is always one try, and * then the retry is prevented. * - * @see org.springframework.commons.retry.RetryPolicy#canRetry(org.springframework.commons.retry.RetryContext) + * @see org.springframework.retry.RetryPolicy#canRetry(org.springframework.retry.RetryContext) */ public boolean canRetry(RetryContext context) { return !((NeverRetryContext) context).isFinished(); @@ -43,7 +43,7 @@ public class NeverRetryPolicy implements RetryPolicy { /** * Do nothing. * - * @see org.springframework.commons.retry.RetryPolicy#close(org.springframework.commons.retry.RetryContext) + * @see org.springframework.retry.RetryPolicy#close(org.springframework.retry.RetryContext) */ public void close(RetryContext context) { // no-op @@ -53,7 +53,7 @@ public class NeverRetryPolicy implements RetryPolicy { * Return a context that can respond to early termination requests, but does * nothing else. * - * @see org.springframework.commons.retry.RetryPolicy#open(RetryContext) + * @see org.springframework.retry.RetryPolicy#open(RetryContext) */ public RetryContext open(RetryContext parent) { return new NeverRetryContext(parent); @@ -61,7 +61,7 @@ public class NeverRetryPolicy implements RetryPolicy { /** * Make the throwable available for downstream use through the context. - * @see org.springframework.commons.retry.RetryPolicy#registerThrowable(org.springframework.commons.retry.RetryContext, + * @see org.springframework.retry.RetryPolicy#registerThrowable(org.springframework.retry.RetryContext, * Throwable) */ public void registerThrowable(RetryContext context, Throwable throwable) { diff --git a/src/main/java/org/springframework/commons/retry/policy/RetryCacheCapacityExceededException.java b/src/main/java/org/springframework/retry/policy/RetryCacheCapacityExceededException.java similarity index 93% rename from src/main/java/org/springframework/commons/retry/policy/RetryCacheCapacityExceededException.java rename to src/main/java/org/springframework/retry/policy/RetryCacheCapacityExceededException.java index e19db59..2860800 100644 --- a/src/main/java/org/springframework/commons/retry/policy/RetryCacheCapacityExceededException.java +++ b/src/main/java/org/springframework/retry/policy/RetryCacheCapacityExceededException.java @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.commons.retry.policy; +package org.springframework.retry.policy; -import org.springframework.commons.retry.RetryException; +import org.springframework.retry.RetryException; /** * Exception that indicates that a cache limit was exceeded. This is often a diff --git a/src/main/java/org/springframework/commons/retry/policy/RetryContextCache.java b/src/main/java/org/springframework/retry/policy/RetryContextCache.java similarity index 90% rename from src/main/java/org/springframework/commons/retry/policy/RetryContextCache.java rename to src/main/java/org/springframework/retry/policy/RetryContextCache.java index 0def72c..9689488 100644 --- a/src/main/java/org/springframework/commons/retry/policy/RetryContextCache.java +++ b/src/main/java/org/springframework/retry/policy/RetryContextCache.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.commons.retry.policy; +package org.springframework.retry.policy; -import org.springframework.commons.retry.RetryContext; +import org.springframework.retry.RetryContext; /** * Simple map-like abstraction for stateful retry policies to use when storing diff --git a/src/main/java/org/springframework/commons/retry/policy/SimpleRetryPolicy.java b/src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java similarity index 87% rename from src/main/java/org/springframework/commons/retry/policy/SimpleRetryPolicy.java rename to src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java index ce2aec0..7ccf6cc 100644 --- a/src/main/java/org/springframework/commons/retry/policy/SimpleRetryPolicy.java +++ b/src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package org.springframework.commons.retry.policy; +package org.springframework.retry.policy; import java.util.Collections; import java.util.Map; -import org.springframework.commons.classify.BinaryExceptionClassifier; -import org.springframework.commons.retry.RetryContext; -import org.springframework.commons.retry.RetryPolicy; -import org.springframework.commons.retry.context.RetryContextSupport; +import org.springframework.classify.BinaryExceptionClassifier; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryPolicy; +import org.springframework.retry.context.RetryContextSupport; /** * @@ -96,7 +96,7 @@ public class SimpleRetryPolicy implements RetryPolicy { /** * Test for retryable operation based on the status. * - * @see org.springframework.commons.retry.RetryPolicy#canRetry(org.springframework.commons.retry.RetryContext) + * @see org.springframework.retry.RetryPolicy#canRetry(org.springframework.retry.RetryContext) * * @return true if the last exception was retryable and the number of * attempts so far is less than the limit. @@ -107,7 +107,7 @@ public class SimpleRetryPolicy implements RetryPolicy { } /** - * @see org.springframework.commons.retry.RetryPolicy#close(RetryContext) + * @see org.springframework.retry.RetryPolicy#close(RetryContext) */ public void close(RetryContext status) { } @@ -127,7 +127,7 @@ public class SimpleRetryPolicy implements RetryPolicy { * according to this policy. Has to be aware of the latest exception and the * number of attempts. * - * @see org.springframework.commons.retry.RetryPolicy#open(RetryContext) + * @see org.springframework.retry.RetryPolicy#open(RetryContext) */ public RetryContext open(RetryContext parent) { return new SimpleRetryContext(parent); diff --git a/src/main/java/org/springframework/commons/retry/policy/SoftReferenceMapRetryContextCache.java b/src/main/java/org/springframework/retry/policy/SoftReferenceMapRetryContextCache.java similarity index 96% rename from src/main/java/org/springframework/commons/retry/policy/SoftReferenceMapRetryContextCache.java rename to src/main/java/org/springframework/retry/policy/SoftReferenceMapRetryContextCache.java index 3de75de..12140ac 100644 --- a/src/main/java/org/springframework/commons/retry/policy/SoftReferenceMapRetryContextCache.java +++ b/src/main/java/org/springframework/retry/policy/SoftReferenceMapRetryContextCache.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.commons.retry.policy; +package org.springframework.retry.policy; import java.lang.ref.SoftReference; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import org.springframework.commons.retry.RetryContext; +import org.springframework.retry.RetryContext; /** * Map-based implementation of {@link RetryContextCache}. The map backing the diff --git a/src/main/java/org/springframework/commons/retry/policy/TimeoutRetryPolicy.java b/src/main/java/org/springframework/retry/policy/TimeoutRetryPolicy.java similarity index 86% rename from src/main/java/org/springframework/commons/retry/policy/TimeoutRetryPolicy.java rename to src/main/java/org/springframework/retry/policy/TimeoutRetryPolicy.java index 0d206ed..3129895 100644 --- a/src/main/java/org/springframework/commons/retry/policy/TimeoutRetryPolicy.java +++ b/src/main/java/org/springframework/retry/policy/TimeoutRetryPolicy.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.springframework.commons.retry.policy; +package org.springframework.retry.policy; -import org.springframework.commons.retry.RetryContext; -import org.springframework.commons.retry.RetryPolicy; -import org.springframework.commons.retry.context.RetryContextSupport; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryPolicy; +import org.springframework.retry.context.RetryContextSupport; /** * A {@link RetryPolicy} that allows a retry only if it hasn't timed out. The @@ -57,7 +57,7 @@ public class TimeoutRetryPolicy implements RetryPolicy { * Only permits a retry if the timeout has not expired. Does not check the * exception at all. * - * @see org.springframework.commons.retry.RetryPolicy#canRetry(org.springframework.commons.retry.RetryContext) + * @see org.springframework.retry.RetryPolicy#canRetry(org.springframework.retry.RetryContext) */ public boolean canRetry(RetryContext context) { return ((TimeoutRetryContext) context).isAlive(); diff --git a/src/main/java/org/springframework/retry/policy/package.html b/src/main/java/org/springframework/retry/policy/package.html new file mode 100644 index 0000000..7652009 --- /dev/null +++ b/src/main/java/org/springframework/retry/policy/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry policy concerns. +

+ + diff --git a/src/main/java/org/springframework/commons/retry/support/DefaultRetryState.java b/src/main/java/org/springframework/retry/support/DefaultRetryState.java similarity index 84% rename from src/main/java/org/springframework/commons/retry/support/DefaultRetryState.java rename to src/main/java/org/springframework/retry/support/DefaultRetryState.java index 8e8f3bb..b6f8ae8 100644 --- a/src/main/java/org/springframework/commons/retry/support/DefaultRetryState.java +++ b/src/main/java/org/springframework/retry/support/DefaultRetryState.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.commons.retry.support; +package org.springframework.retry.support; -import org.springframework.commons.classify.Classifier; -import org.springframework.commons.retry.RecoveryCallback; -import org.springframework.commons.retry.RetryCallback; -import org.springframework.commons.retry.RetryOperations; -import org.springframework.commons.retry.RetryState; +import org.springframework.classify.Classifier; +import org.springframework.retry.RecoveryCallback; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryOperations; +import org.springframework.retry.RetryState; /** * @@ -84,7 +84,7 @@ public class DefaultRetryState implements RetryState { /* * (non-Javadoc) * - * @see org.springframework.commons.retry.IRetryState#getKey() + * @see org.springframework.batch.retry.IRetryState#getKey() */ public Object getKey() { return key; @@ -93,7 +93,7 @@ public class DefaultRetryState implements RetryState { /* * (non-Javadoc) * - * @see org.springframework.commons.retry.IRetryState#isForceRefresh() + * @see org.springframework.batch.retry.IRetryState#isForceRefresh() */ public boolean isForceRefresh() { return forceRefresh; @@ -103,7 +103,7 @@ public class DefaultRetryState implements RetryState { * (non-Javadoc) * * @see - * org.springframework.commons.retry.RetryState#rollbackFor(java.lang.Throwable + * org.springframework.batch.retry.RetryState#rollbackFor(java.lang.Throwable * ) */ public boolean rollbackFor(Throwable exception) { diff --git a/src/main/java/org/springframework/commons/retry/support/RetrySynchronizationManager.java b/src/main/java/org/springframework/retry/support/RetrySynchronizationManager.java similarity index 90% rename from src/main/java/org/springframework/commons/retry/support/RetrySynchronizationManager.java rename to src/main/java/org/springframework/retry/support/RetrySynchronizationManager.java index 35c3939..bacef57 100644 --- a/src/main/java/org/springframework/commons/retry/support/RetrySynchronizationManager.java +++ b/src/main/java/org/springframework/retry/support/RetrySynchronizationManager.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframework.commons.retry.support; +package org.springframework.retry.support; -import org.springframework.commons.repeat.RepeatOperations; -import org.springframework.commons.retry.RetryCallback; -import org.springframework.commons.retry.RetryContext; -import org.springframework.commons.retry.RetryOperations; +import org.springframework.repeat.RepeatOperations; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryOperations; /** * Global variable support for retry clients. Normally it is not necessary for diff --git a/src/main/java/org/springframework/commons/retry/support/RetryTemplate.java b/src/main/java/org/springframework/retry/support/RetryTemplate.java similarity index 92% rename from src/main/java/org/springframework/commons/retry/support/RetryTemplate.java rename to src/main/java/org/springframework/retry/support/RetryTemplate.java index 20a9914..ca0b11b 100644 --- a/src/main/java/org/springframework/commons/retry/support/RetryTemplate.java +++ b/src/main/java/org/springframework/retry/support/RetryTemplate.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.commons.retry.support; +package org.springframework.retry.support; import java.util.ArrayList; import java.util.Arrays; @@ -23,24 +23,24 @@ import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.commons.repeat.RepeatException; -import org.springframework.commons.retry.ExhaustedRetryException; -import org.springframework.commons.retry.RecoveryCallback; -import org.springframework.commons.retry.RetryCallback; -import org.springframework.commons.retry.RetryContext; -import org.springframework.commons.retry.RetryException; -import org.springframework.commons.retry.RetryListener; -import org.springframework.commons.retry.RetryOperations; -import org.springframework.commons.retry.RetryPolicy; -import org.springframework.commons.retry.RetryState; -import org.springframework.commons.retry.TerminatedRetryException; -import org.springframework.commons.retry.backoff.BackOffContext; -import org.springframework.commons.retry.backoff.BackOffInterruptedException; -import org.springframework.commons.retry.backoff.BackOffPolicy; -import org.springframework.commons.retry.backoff.NoBackOffPolicy; -import org.springframework.commons.retry.policy.MapRetryContextCache; -import org.springframework.commons.retry.policy.RetryContextCache; -import org.springframework.commons.retry.policy.SimpleRetryPolicy; +import org.springframework.repeat.RepeatException; +import org.springframework.retry.ExhaustedRetryException; +import org.springframework.retry.RecoveryCallback; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryException; +import org.springframework.retry.RetryListener; +import org.springframework.retry.RetryOperations; +import org.springframework.retry.RetryPolicy; +import org.springframework.retry.RetryState; +import org.springframework.retry.TerminatedRetryException; +import org.springframework.retry.backoff.BackOffContext; +import org.springframework.retry.backoff.BackOffInterruptedException; +import org.springframework.retry.backoff.BackOffPolicy; +import org.springframework.retry.backoff.NoBackOffPolicy; +import org.springframework.retry.policy.MapRetryContextCache; +import org.springframework.retry.policy.RetryContextCache; +import org.springframework.retry.policy.SimpleRetryPolicy; /** * Template class that simplifies the execution of operations with retry @@ -57,7 +57,7 @@ import org.springframework.commons.retry.policy.SimpleRetryPolicy; * with no back off in between. This behaviour can be configured using the * {@link #setRetryPolicy(RetryPolicy)} and * {@link #setBackOffPolicy(BackOffPolicy)} properties. The - * {@link org.springframework.commons.retry.backoff.BackOffPolicy} controls how + * {@link org.springframework.retry.backoff.BackOffPolicy} controls how * long the pause is between each individual retry attempt.
* * This class is thread-safe and suitable for concurrent access when executing @@ -202,7 +202,9 @@ public class RetryTemplate implements RetryOperations { // Allow the retry policy to initialise itself... RetryContext context = open(retryPolicy, state); - logger.debug("RetryContext retrieved: " + context); + if (logger.isTraceEnabled()) { + logger.trace("RetryContext retrieved: " + context); + } // Make sure the context is available globally for clients who need // it... diff --git a/src/main/java/org/springframework/retry/support/package.html b/src/main/java/org/springframework/retry/support/package.html new file mode 100644 index 0000000..549ec82 --- /dev/null +++ b/src/main/java/org/springframework/retry/support/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry support concerns. +

+ + diff --git a/src/test/java/org/springframework/classify/BackToBackPatternClassifierTests.java b/src/test/java/org/springframework/classify/BackToBackPatternClassifierTests.java new file mode 100644 index 0000000..c0b7204 --- /dev/null +++ b/src/test/java/org/springframework/classify/BackToBackPatternClassifierTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.classify; + +import static org.junit.Assert.assertEquals; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.classify.annotation.Classifier; + +/** + * @author Dave Syer + * + */ +public class BackToBackPatternClassifierTests { + + private BackToBackPatternClassifier classifier = new BackToBackPatternClassifier(); + + private Map map; + + @Before + public void createMap() { + map = new HashMap(); + map.put("foo", "bar"); + map.put("*", "spam"); + } + + @Test(expected = NullPointerException.class) + public void testNoClassifiers() { + classifier.classify("foo"); + } + + @Test + public void testCreateFromConstructor() { + classifier = new BackToBackPatternClassifier( + new PatternMatchingClassifier(Collections.singletonMap( + "oof", "bucket")), + new PatternMatchingClassifier(map)); + assertEquals("spam", classifier.classify("oof")); + } + + @Test + public void testSetRouterDelegate() { + classifier.setRouterDelegate(new Object() { + @SuppressWarnings("unused") + @Classifier + public String convert(String value) { + return "bucket"; + } + }); + classifier.setMatcherMap(map); + assertEquals("spam", classifier.classify("oof")); + } + +} diff --git a/src/test/java/org/springframework/classify/BinaryExceptionClassifierTests.java b/src/test/java/org/springframework/classify/BinaryExceptionClassifierTests.java new file mode 100644 index 0000000..1d763b0 --- /dev/null +++ b/src/test/java/org/springframework/classify/BinaryExceptionClassifierTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.classify; + +import java.util.Collection; +import java.util.Collections; + +import junit.framework.TestCase; + +public class BinaryExceptionClassifierTests extends TestCase { + + BinaryExceptionClassifier classifier = new BinaryExceptionClassifier(false); + + public void testClassifyNullIsDefault() { + assertFalse(classifier.classify(null)); + } + + public void testFalseIsDefault() { + assertFalse(classifier.getDefault()); + } + + public void testDefaultProvided() { + classifier = new BinaryExceptionClassifier(true); + assertTrue(classifier.getDefault()); + } + + public void testClassifyRandomException() { + assertFalse(classifier.classify(new IllegalStateException("foo"))); + } + + public void testClassifyExactMatch() { + Collection> set = Collections + .> singleton(IllegalStateException.class); + assertTrue(new BinaryExceptionClassifier(set).classify(new IllegalStateException("Foo"))); + } + + public void testTypesProvidedInConstructor() { + classifier = new BinaryExceptionClassifier(Collections + .> singleton(IllegalStateException.class)); + assertTrue(classifier.classify(new IllegalStateException("Foo"))); + } + + public void testTypesProvidedInConstructorWithNonDefault() { + classifier = new BinaryExceptionClassifier(Collections + .> singleton(IllegalStateException.class), false); + assertFalse(classifier.classify(new IllegalStateException("Foo"))); + } +} diff --git a/src/test/java/org/springframework/classify/ClassifierAdapterTests.java b/src/test/java/org/springframework/classify/ClassifierAdapterTests.java new file mode 100644 index 0000000..a97bfb3 --- /dev/null +++ b/src/test/java/org/springframework/classify/ClassifierAdapterTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.classify; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.classify.annotation.Classifier; + +/** + * @author Dave Syer + * + */ +public class ClassifierAdapterTests { + + private ClassifierAdapter adapter = new ClassifierAdapter(); + + @Test + public void testClassifierAdapterObject() { + adapter = new ClassifierAdapter(new Object() { + @SuppressWarnings("unused") + @Classifier + public Integer getValue(String key) { + return Integer.parseInt(key); + } + + @SuppressWarnings("unused") + public Integer getAnother(String key) { + throw new UnsupportedOperationException("Not allowed"); + } + }); + assertEquals(23, adapter.classify("23").intValue()); + } + + @Test(expected = IllegalStateException.class) + public void testClassifierAdapterObjectWithNoAnnotation() { + adapter = new ClassifierAdapter(new Object() { + @SuppressWarnings("unused") + public Integer getValue(String key) { + return Integer.parseInt(key); + } + + @SuppressWarnings("unused") + public Integer getAnother(String key) { + throw new UnsupportedOperationException("Not allowed"); + } + }); + assertEquals(23, adapter.classify("23").intValue()); + } + + @Test + public void testClassifierAdapterObjectSingleMethodWithNoAnnotation() { + adapter = new ClassifierAdapter(new Object() { + @SuppressWarnings("unused") + public Integer getValue(String key) { + return Integer.parseInt(key); + } + @SuppressWarnings("unused") + public void doNothing(String key) { + } + @SuppressWarnings("unused") + public String doNothing(String key, int value) { + return "foo"; + } + }); + assertEquals(23, adapter.classify("23").intValue()); + } + + @Test + public void testClassifierAdapterClassifier() { + adapter = new ClassifierAdapter( + new org.springframework.classify.Classifier() { + public Integer classify(String classifiable) { + return Integer.valueOf(classifiable); + } + }); + assertEquals(23, adapter.classify("23").intValue()); + } + + @Test + public void testClassifyWithSetter() { + adapter.setDelegate(new Object() { + @SuppressWarnings("unused") + @Classifier + public Integer getValue(String key) { + return Integer.parseInt(key); + } + }); + assertEquals(23, adapter.classify("23").intValue()); + } + + @Test(expected=IllegalArgumentException.class) + public void testClassifyWithWrongType() { + adapter.setDelegate(new Object() { + @SuppressWarnings("unused") + @Classifier + public String getValue(Integer key) { + return key.toString(); + } + }); + assertEquals(23, adapter.classify("23").intValue()); + } + + @Test + public void testClassifyWithClassifier() { + adapter.setDelegate(new org.springframework.classify.Classifier() { + public Integer classify(String classifiable) { + return Integer.valueOf(classifiable); + } + }); + assertEquals(23, adapter.classify("23").intValue()); + } + +} diff --git a/src/test/java/org/springframework/classify/ClassifierSupportTests.java b/src/test/java/org/springframework/classify/ClassifierSupportTests.java new file mode 100644 index 0000000..c1416c2 --- /dev/null +++ b/src/test/java/org/springframework/classify/ClassifierSupportTests.java @@ -0,0 +1,35 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.classify; + +import org.springframework.classify.ClassifierSupport; + +import junit.framework.TestCase; + +public class ClassifierSupportTests extends TestCase { + + public void testClassifyNullIsDefault() { + ClassifierSupport classifier = new ClassifierSupport("foo"); + assertEquals(classifier.classify(null), "foo"); + } + + public void testClassifyRandomException() { + ClassifierSupport classifier = new ClassifierSupport("foo"); + assertEquals(classifier.classify(new IllegalStateException("Foo")), classifier.classify(null)); + } + +} diff --git a/src/test/java/org/springframework/classify/PatternMatchingClassifierTests.java b/src/test/java/org/springframework/classify/PatternMatchingClassifierTests.java new file mode 100644 index 0000000..bfb0f8d --- /dev/null +++ b/src/test/java/org/springframework/classify/PatternMatchingClassifierTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.classify; + +import static org.junit.Assert.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.classify.PatternMatchingClassifier; + +/** + * @author Dave Syer + * + */ +public class PatternMatchingClassifierTests { + + private PatternMatchingClassifier classifier = new PatternMatchingClassifier(); + + private Map map; + + @Before + public void createMap() { + map = new HashMap(); + map.put("foo", "bar"); + map.put("*", "spam"); + } + + @Test + public void testSetPatternMap() { + classifier.setPatternMap(map); + assertEquals("bar", classifier.classify("foo")); + assertEquals("spam", classifier.classify("bucket")); + } + + @Test + public void testCreateFromMap() { + classifier = new PatternMatchingClassifier(map); + assertEquals("bar", classifier.classify("foo")); + assertEquals("spam", classifier.classify("bucket")); + } + +} \ No newline at end of file diff --git a/src/test/java/org/springframework/classify/SubclassExceptionClassifierTests.java b/src/test/java/org/springframework/classify/SubclassExceptionClassifierTests.java new file mode 100644 index 0000000..4982937 --- /dev/null +++ b/src/test/java/org/springframework/classify/SubclassExceptionClassifierTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.classify; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.Collections; +import java.util.HashMap; + +import org.junit.Test; +import org.springframework.classify.SubclassClassifier; + +public class SubclassExceptionClassifierTests { + + SubclassClassifier classifier = new SubclassClassifier(); + + @Test + public void testClassifyNullIsDefault() { + assertEquals(classifier.classify(null), classifier.getDefault()); + } + + @Test + public void testClassifyNull() { + assertNull(classifier.classify(null)); + } + + @Test + public void testClassifyNullNonDefault() { + classifier = new SubclassClassifier("foo"); + assertEquals("foo", classifier.classify(null)); + } + + @Test + public void testClassifyRandomException() { + assertNull(classifier.classify(new IllegalStateException("Foo"))); + } + + @Test + public void testClassifyExactMatch() { + classifier.setTypeMap(Collections., String> singletonMap( + IllegalStateException.class, "foo")); + assertEquals("foo", classifier.classify(new IllegalStateException("Foo"))); + } + + @Test + public void testClassifySubclassMatch() { + classifier.setTypeMap(Collections., String> singletonMap(RuntimeException.class, + "foo")); + assertEquals("foo", classifier.classify(new IllegalStateException("Foo"))); + } + + @Test + public void testClassifySuperclassDoesNotMatch() { + classifier.setTypeMap(Collections., String> singletonMap( + IllegalStateException.class, "foo")); + assertEquals(classifier.getDefault(), classifier.classify(new RuntimeException("Foo"))); + } + + @Test + public void testClassifyAncestorMatch() { + classifier.setTypeMap(new HashMap, String>() { + { + put(Exception.class, "foo"); + put(IllegalArgumentException.class, "bar"); + put(RuntimeException.class, "spam"); + } + }); + assertEquals("spam", classifier.classify(new IllegalStateException("Foo"))); + } +} diff --git a/src/test/java/org/springframework/repeat/AbstractExceptionTests.java b/src/test/java/org/springframework/repeat/AbstractExceptionTests.java new file mode 100644 index 0000000..54d7f73 --- /dev/null +++ b/src/test/java/org/springframework/repeat/AbstractExceptionTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat; + +import junit.framework.TestCase; + +public abstract class AbstractExceptionTests extends TestCase { + + public void testExceptionString() throws Exception { + Exception exception = getException("foo"); + assertEquals("foo", exception.getMessage()); + } + + public void testExceptionStringThrowable() throws Exception { + Exception exception = getException("foo", new IllegalStateException()); + assertEquals("foo", exception.getMessage().substring(0, 3)); + } + + public abstract Exception getException(String msg) throws Exception; + + public abstract Exception getException(String msg, Throwable t) throws Exception; +} diff --git a/src/test/java/org/springframework/repeat/RepeatExceptionTests.java b/src/test/java/org/springframework/repeat/RepeatExceptionTests.java new file mode 100644 index 0000000..b26cd50 --- /dev/null +++ b/src/test/java/org/springframework/repeat/RepeatExceptionTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat; + +import org.springframework.repeat.RepeatException; + +public class RepeatExceptionTests extends AbstractExceptionTests { + + public Exception getException(String msg) throws Exception { + return new RepeatException(msg); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new RepeatException(msg, t); + } + + public void testNothing() throws Exception { + // fool coverage tools... + } +} diff --git a/src/test/java/org/springframework/repeat/callback/NestedRepeatCallbackTests.java b/src/test/java/org/springframework/repeat/callback/NestedRepeatCallbackTests.java new file mode 100644 index 0000000..3562582 --- /dev/null +++ b/src/test/java/org/springframework/repeat/callback/NestedRepeatCallbackTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.callback; + +import junit.framework.TestCase; + +import org.springframework.repeat.RepeatCallback; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatStatus; +import org.springframework.repeat.support.RepeatTemplate; + +public class NestedRepeatCallbackTests extends TestCase { + + int count = 0; + + public void testExecute() throws Exception { + NestedRepeatCallback callback = new NestedRepeatCallback(new RepeatTemplate(), new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + count++; + return RepeatStatus.continueIf(count <= 1); + } + }); + RepeatStatus result = callback.doInIteration(null); + assertEquals(2, count); + assertFalse(result.isContinuable()); // False because processing has finished + } +} diff --git a/src/test/java/org/springframework/repeat/context/RepeatContextCounterTests.java b/src/test/java/org/springframework/repeat/context/RepeatContextCounterTests.java new file mode 100644 index 0000000..d17373d --- /dev/null +++ b/src/test/java/org/springframework/repeat/context/RepeatContextCounterTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.context; + +import junit.framework.TestCase; + +import org.springframework.repeat.RepeatContext; + +public class RepeatContextCounterTests extends TestCase { + + RepeatContext parent = new RepeatContextSupport(null); + RepeatContext context = new RepeatContextSupport(parent); + + public void testAttributeCreated() { + new RepeatContextCounter(context, "FOO"); + assertTrue(context.hasAttribute("FOO")); + } + + public void testAttributeCreatedWithNullParent() { + new RepeatContextCounter(parent, "FOO", true); + assertTrue(parent.hasAttribute("FOO")); + } + + public void testVanillaIncrement() throws Exception { + RepeatContextCounter counter = new RepeatContextCounter(context, "FOO"); + assertEquals(0, counter.getCount()); + counter.increment(1); + assertEquals(1, counter.getCount()); + counter.increment(2); + assertEquals(3, counter.getCount()); + } + + public void testAttributeCreatedInParent() throws Exception { + new RepeatContextCounter(context, "FOO", true); + assertFalse(context.hasAttribute("FOO")); + assertTrue(parent.hasAttribute("FOO")); + } + + public void testParentIncrement() throws Exception { + RepeatContextCounter counter = new RepeatContextCounter(context, "FOO", true); + assertEquals(0, counter.getCount()); + counter.increment(1); + // now get new context with same parent + counter = new RepeatContextCounter(new RepeatContextSupport(parent), "FOO", true); + assertEquals(1, counter.getCount()); + counter.increment(2); + assertEquals(3, counter.getCount()); + } + +} diff --git a/src/test/java/org/springframework/repeat/context/RepeatContextSupportTests.java b/src/test/java/org/springframework/repeat/context/RepeatContextSupportTests.java new file mode 100644 index 0000000..d89d052 --- /dev/null +++ b/src/test/java/org/springframework/repeat/context/RepeatContextSupportTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.repeat.context; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +/** + * @author dsyer + * + */ +public class RepeatContextSupportTests extends TestCase { + + private List list = new ArrayList(); + + /** + * Test method for {@link org.springframework.repeat.context.RepeatContextSupport#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. + */ + public void testDestructionCallbackSunnyDay() throws Exception { + RepeatContextSupport context = new RepeatContextSupport(null); + context.setAttribute("foo", "FOO"); + context.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("bar"); + } + }); + context.close(); + assertEquals(1, list.size()); + assertEquals("bar", list.get(0)); + } + + /** + * Test method for {@link org.springframework.repeat.context.RepeatContextSupport#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. + */ + public void testDestructionCallbackMissingAttribute() throws Exception { + RepeatContextSupport context = new RepeatContextSupport(null); + context.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("bar"); + } + }); + context.close(); + // No check for the attribute before executing callback + assertEquals(1, list.size()); + } + + /** + * Test method for {@link org.springframework.repeat.context.RepeatContextSupport#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. + */ + public void testDestructionCallbackWithException() throws Exception { + RepeatContextSupport context = new RepeatContextSupport(null); + context.setAttribute("foo", "FOO"); + context.setAttribute("bar", "BAR"); + context.registerDestructionCallback("bar", new Runnable() { + public void run() { + list.add("spam"); + throw new RuntimeException("fail!"); + } + }); + context.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("bar"); + throw new RuntimeException("fail!"); + } + }); + try { + context.close(); + fail("Expected RuntimeException"); + } catch (RuntimeException e) { + // We don't care which one was thrown... + assertEquals("fail!", e.getMessage()); + } + // ...but we do care that both were executed: + assertEquals(2, list.size()); + assertTrue(list.contains("bar")); + assertTrue(list.contains("spam")); + } +} diff --git a/src/test/java/org/springframework/repeat/context/SynchronizedAttributeAccessorTests.java b/src/test/java/org/springframework/repeat/context/SynchronizedAttributeAccessorTests.java new file mode 100644 index 0000000..3a94bfd --- /dev/null +++ b/src/test/java/org/springframework/repeat/context/SynchronizedAttributeAccessorTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.context; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import junit.framework.TestCase; + +import org.springframework.core.AttributeAccessorSupport; + +public class SynchronizedAttributeAccessorTests extends TestCase { + + SynchronizedAttributeAccessor accessor = new SynchronizedAttributeAccessor(); + + public void testHashCode() { + SynchronizedAttributeAccessor another = new SynchronizedAttributeAccessor(); + accessor.setAttribute("foo", "bar"); + another.setAttribute("foo", "bar"); + assertEquals(accessor, another); + assertEquals("Object.hashCode() contract broken", accessor.hashCode(), another.hashCode()); + } + + public void testToStringWithNoAttributes() throws Exception { + assertNotNull(accessor.toString()); + } + + public void testToStringWithAttributes() throws Exception { + accessor.setAttribute("foo", "bar"); + accessor.setAttribute("spam", "bucket"); + assertNotNull(accessor.toString()); + } + + public void testAttributeNames() { + accessor.setAttribute("foo", "bar"); + accessor.setAttribute("spam", "bucket"); + List list = Arrays.asList(accessor.attributeNames()); + assertEquals(2, list.size()); + assertTrue(list.contains("foo")); + } + + public void testEqualsSameType() { + SynchronizedAttributeAccessor another = new SynchronizedAttributeAccessor(); + accessor.setAttribute("foo", "bar"); + another.setAttribute("foo", "bar"); + assertEquals(accessor, another); + } + + public void testEqualsSelf() { + accessor.setAttribute("foo", "bar"); + assertEquals(accessor, accessor); + } + + public void testEqualsWrongType() { + accessor.setAttribute("foo", "bar"); + Map another = Collections.singletonMap("foo", "bar"); + // Accessor and another are instances of unrelated classes, they should + // never be equal... + assertFalse(accessor.equals(another)); + } + + public void testEqualsSupport() { + AttributeAccessorSupport another = new AttributeAccessorSupport() { + }; + accessor.setAttribute("foo", "bar"); + another.setAttribute("foo", "bar"); + assertEquals(accessor, another); + } + + public void testGetAttribute() { + accessor.setAttribute("foo", "bar"); + assertEquals("bar", accessor.getAttribute("foo")); + } + + public void testSetAttributeIfAbsentWhenAlreadyPresent() { + accessor.setAttribute("foo", "bar"); + assertEquals("bar", accessor.setAttributeIfAbsent("foo", "spam")); + } + + public void testSetAttributeIfAbsentWhenNotAlreadyPresent() { + assertEquals(null, accessor.setAttributeIfAbsent("foo", "bar")); + assertEquals("bar", accessor.getAttribute("foo")); + } + + public void testHasAttribute() { + accessor.setAttribute("foo", "bar"); + assertEquals(true, accessor.hasAttribute("foo")); + } + + public void testRemoveAttribute() { + accessor.setAttribute("foo", "bar"); + assertEquals("bar", accessor.getAttribute("foo")); + accessor.removeAttribute("foo"); + assertEquals(null, accessor.getAttribute("foo")); + } + +} diff --git a/src/test/java/org/springframework/repeat/exception/CompositeExceptionHandlerTests.java b/src/test/java/org/springframework/repeat/exception/CompositeExceptionHandlerTests.java new file mode 100644 index 0000000..54cd8c4 --- /dev/null +++ b/src/test/java/org/springframework/repeat/exception/CompositeExceptionHandlerTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.exception; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.exception.CompositeExceptionHandler; +import org.springframework.repeat.exception.ExceptionHandler; + +public class CompositeExceptionHandlerTests extends TestCase { + + private CompositeExceptionHandler handler = new CompositeExceptionHandler(); + + public void testNewHandler() throws Throwable { + try { + handler.handleException(null, new RuntimeException()); + } + catch (RuntimeException e) { + fail("Unexpected RuntimeException"); + } + } + + public void testDelegation() throws Throwable { + final List list = new ArrayList(); + handler.setHandlers(new ExceptionHandler[] { + new ExceptionHandler() { + public void handleException(RepeatContext context, Throwable throwable) throws RuntimeException { + list.add("1"); + } + }, + new ExceptionHandler() { + public void handleException(RepeatContext context, Throwable throwable) throws RuntimeException { + list.add("2"); + } + } + }); + handler.handleException(null, new RuntimeException()); + assertEquals(2, list.size()); + assertEquals("1", list.get(0)); + assertEquals("2", list.get(1)); + } +} diff --git a/src/test/java/org/springframework/repeat/exception/DefaultExceptionHandlerTests.java b/src/test/java/org/springframework/repeat/exception/DefaultExceptionHandlerTests.java new file mode 100644 index 0000000..820e9f9 --- /dev/null +++ b/src/test/java/org/springframework/repeat/exception/DefaultExceptionHandlerTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.exception; + +import junit.framework.TestCase; + +import org.springframework.repeat.RepeatContext; + +public class DefaultExceptionHandlerTests extends TestCase { + + private DefaultExceptionHandler handler = new DefaultExceptionHandler(); + private RepeatContext context = null; + + public void testRuntimeException() throws Throwable { + try { + handler.handleException(context, new RuntimeException("Foo")); + fail("Expected RuntimeException"); + } catch (RuntimeException e) { + assertEquals("Foo", e.getMessage()); + } + } + + public void testError() throws Throwable { + try { + handler.handleException(context, new Error("Foo")); + fail("Expected Error"); + } catch (Error e) { + assertEquals("Foo", e.getMessage()); + } + } +} diff --git a/src/test/java/org/springframework/repeat/exception/LogOrRethrowExceptionHandlerTests.java b/src/test/java/org/springframework/repeat/exception/LogOrRethrowExceptionHandlerTests.java new file mode 100644 index 0000000..5d98969 --- /dev/null +++ b/src/test/java/org/springframework/repeat/exception/LogOrRethrowExceptionHandlerTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.exception; + +import java.io.StringWriter; + +import junit.framework.TestCase; + +import org.apache.log4j.Logger; +import org.apache.log4j.SimpleLayout; +import org.apache.log4j.WriterAppender; +import org.springframework.classify.ClassifierSupport; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.exception.LogOrRethrowExceptionHandler.Level; + +public class LogOrRethrowExceptionHandlerTests extends TestCase { + + private LogOrRethrowExceptionHandler handler = new LogOrRethrowExceptionHandler(); + + private StringWriter writer; + + private RepeatContext context = null; + + protected void setUp() throws Exception { + super.setUp(); + Logger logger = Logger.getLogger(LogOrRethrowExceptionHandler.class); + logger.setLevel(org.apache.log4j.Level.DEBUG); + writer = new StringWriter(); + logger.removeAllAppenders(); + logger.getParent().removeAllAppenders(); + logger.addAppender(new WriterAppender(new SimpleLayout(), writer)); + } + + public void testRuntimeException() throws Throwable { + try { + handler.handleException(context, new RuntimeException("Foo")); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Foo", e.getMessage()); + } + } + + public void testError() throws Throwable { + try { + handler.handleException(context, new Error("Foo")); + fail("Expected Error"); + } + catch (Error e) { + assertEquals("Foo", e.getMessage()); + } + } + + public void testNotRethrownErrorLevel() throws Throwable { + handler.setExceptionClassifier(new ClassifierSupport(Level.RETHROW) { + public Level classify(Throwable throwable) { + return Level.ERROR; + } + }); + // No exception... + handler.handleException(context, new Error("Foo")); + assertNotNull(writer.toString()); + } + + public void testNotRethrownWarnLevel() throws Throwable { + handler.setExceptionClassifier(new ClassifierSupport(Level.RETHROW) { + public Level classify(Throwable throwable) { + return Level.WARN; + } + }); + // No exception... + handler.handleException(context, new Error("Foo")); + assertNotNull(writer.toString()); + } + + public void testNotRethrownDebugLevel() throws Throwable { + handler.setExceptionClassifier(new ClassifierSupport(Level.RETHROW) { + public Level classify(Throwable throwable) { + return Level.DEBUG; + } + }); + // No exception... + handler.handleException(context, new Error("Foo")); + assertNotNull(writer.toString()); + } + +} diff --git a/src/test/java/org/springframework/repeat/exception/RethrowOnThresholdExceptionHandlerTests.java b/src/test/java/org/springframework/repeat/exception/RethrowOnThresholdExceptionHandlerTests.java new file mode 100644 index 0000000..0ab51c8 --- /dev/null +++ b/src/test/java/org/springframework/repeat/exception/RethrowOnThresholdExceptionHandlerTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.exception; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.context.RepeatContextSupport; + +public class RethrowOnThresholdExceptionHandlerTests { + + private RethrowOnThresholdExceptionHandler handler = new RethrowOnThresholdExceptionHandler(); + + private RepeatContext parent = new RepeatContextSupport(null); + + private RepeatContext context = new RepeatContextSupport(parent); + + @Test + public void testRuntimeException() throws Throwable { + try { + handler.handleException(context, new RuntimeException("Foo")); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Foo", e.getMessage()); + } + } + + @Test + public void testError() throws Throwable { + try { + handler.handleException(context, new Error("Foo")); + fail("Expected Error"); + } + catch (Error e) { + assertEquals("Foo", e.getMessage()); + } + } + + @Test + public void testNotRethrownWithThreshold() throws Throwable { + handler.setThresholds(Collections., Integer> singletonMap(Exception.class, 1)); + // No exception... + handler.handleException(context, new RuntimeException("Foo")); + AtomicInteger counter = (AtomicInteger) context.getAttribute(context.attributeNames()[0]); + assertNotNull(counter); + assertEquals(1, counter.get()); + } + + @Test + public void testRethrowOnThreshold() throws Throwable { + handler.setThresholds(Collections., Integer> singletonMap(Exception.class, 2)); + // No exception... + handler.handleException(context, new RuntimeException("Foo")); + handler.handleException(context, new RuntimeException("Foo")); + try { + handler.handleException(context, new RuntimeException("Foo")); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Foo", e.getMessage()); + } + } + + @Test + public void testNotUseParent() throws Throwable { + handler.setThresholds(Collections., Integer> singletonMap(Exception.class, 1)); + // No exception... + handler.handleException(context, new RuntimeException("Foo")); + context = new RepeatContextSupport(parent); + try { + // No exception again - context is changed... + handler.handleException(context, new RuntimeException("Foo")); + } + catch (RuntimeException e) { + fail("Unexpected Error"); + } + } + + @Test + public void testUseParent() throws Throwable { + handler.setThresholds(Collections., Integer> singletonMap(Exception.class, 1)); + handler.setUseParent(true); + // No exception... + handler.handleException(context, new RuntimeException("Foo")); + context = new RepeatContextSupport(parent); + try { + handler.handleException(context, new RuntimeException("Foo")); + fail("Expected Error"); + } + catch (RuntimeException e) { + assertEquals("Foo", e.getMessage()); + } + } + +} diff --git a/src/test/java/org/springframework/repeat/exception/SimpleLimitExceptionHandlerTests.java b/src/test/java/org/springframework/repeat/exception/SimpleLimitExceptionHandlerTests.java new file mode 100644 index 0000000..86bc3be --- /dev/null +++ b/src/test/java/org/springframework/repeat/exception/SimpleLimitExceptionHandlerTests.java @@ -0,0 +1,267 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.exception; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.repeat.context.RepeatContextSupport; + +/** + * Unit tests for {@link SimpleLimitExceptionHandler} + * + * @author Robert Kasanicky + * @author Dave Syer + */ +public class SimpleLimitExceptionHandlerTests { + + // object under test + private SimpleLimitExceptionHandler handler = new SimpleLimitExceptionHandler(); + + @Before + public void initializeHandler() throws Exception { + handler.afterPropertiesSet(); + } + + @Test + public void testInitializeWithNullContext() throws Throwable { + try { + handler.handleException(null, new RuntimeException("foo")); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testInitializeWithNullContextAndNullException() throws Throwable { + try { + handler.handleException(null, null); + } + catch (IllegalArgumentException e) { + // expected; + } + } + + @Test + public void testDefaultBehaviour() throws Throwable { + Throwable throwable = new RuntimeException("foo"); + try { + handler.handleException(new RepeatContextSupport(null), throwable); + fail("Exception was swallowed."); + } + catch (RuntimeException expected) { + assertTrue("Exception is rethrown, ignoring the exception limit", true); + assertSame(expected, throwable); + } + } + + /** + * Other than nominated exception type should be rethrown, ignoring the + * exception limit. + * + * @throws Exception + */ + @Test + public void testNormalExceptionThrown() throws Throwable { + Throwable throwable = new RuntimeException("foo"); + + final int MORE_THAN_ZERO = 1; + handler.setLimit(MORE_THAN_ZERO); + handler.setExceptionClasses(Collections.> singleton(IllegalArgumentException.class)); + handler.afterPropertiesSet(); + + try { + handler.handleException(new RepeatContextSupport(null), throwable); + fail("Exception was swallowed."); + } + catch (RuntimeException expected) { + assertTrue("Exception is rethrown, ignoring the exception limit", true); + assertSame(expected, throwable); + } + } + + /** + * TransactionInvalidException should only be rethrown below the exception + * limit. + * + * @throws Exception + */ + @Test + public void testLimitedExceptionTypeNotThrown() throws Throwable { + final int MORE_THAN_ZERO = 1; + handler.setLimit(MORE_THAN_ZERO); + handler.setExceptionClasses(Collections.> singleton(RuntimeException.class)); + handler.afterPropertiesSet(); + + try { + handler.handleException(new RepeatContextSupport(null), new RuntimeException("foo")); + } + catch (RuntimeException expected) { + fail("Unexpected exception."); + } + } + + /** + * TransactionInvalidException should only be rethrown below the exception + * limit. + * + * @throws Exception + */ + @Test + public void testLimitedExceptionNotThrownFromSiblings() throws Throwable { + Throwable throwable = new RuntimeException("foo"); + + final int MORE_THAN_ZERO = 1; + handler.setLimit(MORE_THAN_ZERO); + handler.setExceptionClasses(Collections.> singleton(RuntimeException.class)); + handler.afterPropertiesSet(); + + RepeatContextSupport parent = new RepeatContextSupport(null); + + try { + RepeatContextSupport context = new RepeatContextSupport(parent); + handler.handleException(context, throwable); + context = new RepeatContextSupport(parent); + handler.handleException(context, throwable); + } + catch (RuntimeException expected) { + fail("Unexpected exception."); + } + } + + /** + * TransactionInvalidException should only be rethrown below the exception + * limit. + * + * @throws Exception + */ + @Test + public void testLimitedExceptionThrownFromSiblingsWhenUsingParent() throws Throwable { + Throwable throwable = new RuntimeException("foo"); + + final int MORE_THAN_ZERO = 1; + handler.setLimit(MORE_THAN_ZERO); + handler.setExceptionClasses(Collections.> singleton(RuntimeException.class)); + handler.setUseParent(true); + handler.afterPropertiesSet(); + + RepeatContextSupport parent = new RepeatContextSupport(null); + + try { + RepeatContextSupport context = new RepeatContextSupport(parent); + handler.handleException(context, throwable); + context = new RepeatContextSupport(parent); + handler.handleException(context, throwable); + fail("Expected exception."); + } + catch (RuntimeException expected) { + assertSame(throwable, expected); + } + } + + /** + * Exceptions are swallowed until the exception limit is exceeded. After the + * limit is exceeded exceptions are rethrown + */ + @Test + public void testExceptionNotThrownBelowLimit() throws Throwable { + + final int EXCEPTION_LIMIT = 3; + handler.setLimit(EXCEPTION_LIMIT); + handler.afterPropertiesSet(); + + List throwables = new ArrayList() { + { + for (int i = 0; i < (EXCEPTION_LIMIT); i++) { + add(new RuntimeException("below exception limit")); + } + } + }; + + RepeatContextSupport context = new RepeatContextSupport(null); + + try { + for (Throwable throwable : throwables) { + + handler.handleException(context, throwable); + assertTrue("exceptions up to limit are swallowed", true); + + } + } + catch (RuntimeException unexpected) { + fail("exception rethrown although exception limit was not exceeded"); + } + + } + + /** + * TransactionInvalidExceptions are swallowed until the exception limit is + * exceeded. After the limit is exceeded exceptions are rethrown as + * BatchCriticalExceptions + */ + @Test + public void testExceptionThrownAboveLimit() throws Throwable { + + final int EXCEPTION_LIMIT = 3; + handler.setLimit(EXCEPTION_LIMIT); + handler.afterPropertiesSet(); + + List throwables = new ArrayList() { + { + for (int i = 0; i < (EXCEPTION_LIMIT); i++) { + add(new RuntimeException("below exception limit")); + } + } + }; + + throwables.add(new RuntimeException("above exception limit")); + + RepeatContextSupport context = new RepeatContextSupport(null); + + try { + for (Throwable throwable : throwables) { + + handler.handleException(context, throwable); + assertTrue("exceptions up to limit are swallowed", true); + + } + } + catch (RuntimeException expected) { + assertEquals("above exception limit", expected.getMessage()); + } + + // after reaching the limit, behaviour should be idempotent + try { + handler.handleException(context, new RuntimeException("foo")); + assertTrue("exceptions up to limit are swallowed", true); + + } + catch (RuntimeException expected) { + assertEquals("foo", expected.getMessage()); + } + } +} diff --git a/src/test/java/org/springframework/repeat/interceptor/RepeatOperationsInterceptorTests.java b/src/test/java/org/springframework/repeat/interceptor/RepeatOperationsInterceptorTests.java new file mode 100644 index 0000000..125ba55 --- /dev/null +++ b/src/test/java/org/springframework/repeat/interceptor/RepeatOperationsInterceptorTests.java @@ -0,0 +1,270 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.interceptor; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.repeat.RepeatCallback; +import org.springframework.repeat.RepeatException; +import org.springframework.repeat.RepeatOperations; +import org.springframework.repeat.RepeatStatus; +import org.springframework.repeat.policy.SimpleCompletionPolicy; +import org.springframework.repeat.support.RepeatTemplate; + +public class RepeatOperationsInterceptorTests extends TestCase { + + private RepeatOperationsInterceptor interceptor; + + private Service service; + + private ServiceImpl target; + + protected void setUp() throws Exception { + super.setUp(); + interceptor = new RepeatOperationsInterceptor(); + target = new ServiceImpl(); + ProxyFactory factory = new ProxyFactory(RepeatOperations.class.getClassLoader()); + factory.setInterfaces(new Class[] { Service.class }); + factory.setTarget(target); + service = (Service) factory.getProxy(); + } + + public void testDefaultInterceptorSunnyDay() throws Exception { + ((Advised) service).addAdvice(interceptor); + service.service(); + assertEquals(3, target.count); + } + + public void testCompleteOnFirstInvocation() throws Exception { + ((Advised) service).addAdvice(interceptor); + target.setMaxService(0); + service.service(); + assertEquals(1, target.count); + } + + public void testSetTemplate() throws Exception { + final List calls = new ArrayList(); + interceptor.setRepeatOperations(new RepeatOperations() { + public RepeatStatus iterate(RepeatCallback callback) { + try { + Object result = callback.doInIteration(null); + calls.add(result); + } + catch (Exception e) { + throw new RepeatException("Encountered exception in repeat.", e); + } + return RepeatStatus.CONTINUABLE; + } + }); + ((Advised) service).addAdvice(interceptor); + service.service(); + assertEquals(1, calls.size()); + } + + public void testCallbackNotExecuted() throws Exception { + final List calls = new ArrayList(); + interceptor.setRepeatOperations(new RepeatOperations() { + public RepeatStatus iterate(RepeatCallback callback) { + calls.add(null); + return RepeatStatus.FINISHED; + } + }); + ((Advised) service).addAdvice(interceptor); + try { + service.service(); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + String message = e.getMessage(); + assertTrue("Wrong exception message: "+message, message.toLowerCase().indexOf("no result available")>=0); + } + assertEquals(1, calls.size()); + } + + public void testVoidServiceSunnyDay() throws Exception { + ((Advised) service).addAdvice(interceptor); + RepeatTemplate template = new RepeatTemplate(); + // N.B. the default completion policy results in an infinite loop, so we + // need to set the chunk size. + template.setCompletionPolicy(new SimpleCompletionPolicy(2)); + interceptor.setRepeatOperations(template); + service.alternate(); + assertEquals(2, target.count); + } + + public void testCallbackWithException() throws Exception { + ((Advised) service).addAdvice(interceptor); + try { + service.exception(); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Duh", e.getMessage().substring(0, 3)); + } + } + + public void testCallbackWithThrowable() throws Exception { + ((Advised) service).addAdvice(interceptor); + try { + service.error(); + fail("Expected Error"); + } + catch (Error e) { + assertEquals("Duh", e.getMessage().substring(0, 3)); + } + } + + public void testCallbackWithBoolean() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + // N.B. the default completion policy results in an infinite loop, so we + // need to set the chunk size. + template.setCompletionPolicy(new SimpleCompletionPolicy(2)); + interceptor.setRepeatOperations(template); + ((Advised) service).addAdvice(interceptor); + assertTrue(service.isContinuable()); + assertEquals(2, target.count); + } + + public void testCallbackWithBooleanReturningFalseFirstTime() throws Exception { + target.setComplete(true); + ((Advised) service).addAdvice(interceptor); + // Complete without repeat when boolean return value is false + assertFalse(service.isContinuable()); + assertEquals(1, target.count); + } + + public void testInterceptorChainWithRetry() throws Exception { + ((Advised) service).addAdvice(interceptor); + final List list = new ArrayList(); + ((Advised) service).addAdvice(new MethodInterceptor() { + public Object invoke(MethodInvocation invocation) throws Throwable { + list.add("chain"); + return invocation.proceed(); + } + }); + RepeatTemplate template = new RepeatTemplate(); + template.setCompletionPolicy(new SimpleCompletionPolicy(2)); + interceptor.setRepeatOperations(template); + service.service(); + assertEquals(2, target.count); + assertEquals(2, list.size()); + } + + public void testIllegalMethodInvocationType() throws Throwable { + try { + interceptor.invoke(new MethodInvocation() { + public Method getMethod() { + try { + return Object.class.getMethod("toString", new Class[0]); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + public Object[] getArguments() { + return null; + } + + public AccessibleObject getStaticPart() { + return null; + } + + public Object getThis() { + return null; + } + + public Object proceed() throws Throwable { + return null; + } + }); + fail("IllegalStateException expected"); + } + catch (IllegalStateException e) { + assertTrue("Exception message should contain MethodInvocation: " + e.getMessage(), e.getMessage().indexOf( + "MethodInvocation") >= 0); + } + } + + private interface Service { + Object service() throws Exception; + + void alternate() throws Exception; + + Object exception() throws Exception; + + Object error() throws Exception; + + boolean isContinuable() throws Exception; + } + + private static class ServiceImpl implements Service { + private int count = 0; + + private boolean complete; + + private int maxService = 2; + + /** + * Public setter for the maximum number of times to call service(). + * @param maxService the maxService to set + */ + public void setMaxService(int maxService) { + this.maxService = maxService; + } + + public Object service() throws Exception { + count++; + if (count <= maxService) { + return Integer.valueOf(count); + } + else { + return null; + } + } + + public void setComplete(boolean complete) { + this.complete = complete; + } + + public void alternate() throws Exception { + count++; + } + + public Object exception() throws Exception { + throw new RuntimeException("Duh! Stupid."); + } + + public Object error() throws Exception { + throw new Error("Duh! Stupid error."); + } + + public boolean isContinuable() throws Exception { + count++; + return !complete; + } + } + +} diff --git a/src/test/java/org/springframework/repeat/listener/CompositeRepeatListenerTests.java b/src/test/java/org/springframework/repeat/listener/CompositeRepeatListenerTests.java new file mode 100644 index 0000000..430c34f --- /dev/null +++ b/src/test/java/org/springframework/repeat/listener/CompositeRepeatListenerTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.repeat.listener; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatListener; +import org.springframework.repeat.context.RepeatContextSupport; + +/** + * @author Dave Syer + * + */ +public class CompositeRepeatListenerTests extends TestCase { + + private CompositeRepeatListener listener = new CompositeRepeatListener(); + private RepeatContext context = new RepeatContextSupport(null); + + private List list = new ArrayList(); + + /** + * Test method for {@link CompositeRepeatListener#setListeners(RepeatListener[])}. + */ + public void testSetListeners() { + listener.setListeners(new RepeatListener[] { new RepeatListenerSupport() { + public void open(RepeatContext context) { + list.add("fail"); + } + }, new RepeatListenerSupport() { + public void open(RepeatContext context) { + list.add("continue"); + } + } }); + listener.open(context); + assertEquals(2, list.size()); + } + + /** + * Test method for + * {@link CompositeRepeatListener#register(RepeatListener)}. + */ + public void testSetListener() { + listener.register(new RepeatListenerSupport() { + public void before(RepeatContext context) { + list.add("fail"); + } + }); + listener.before(context); + assertEquals(1, list.size()); + } + + public void testClose() { + listener.register(new RepeatListenerSupport() { + public void close(RepeatContext context) { + list.add("foo"); + } + }); + listener.close(context); + assertEquals(1, list.size()); + } + + public void testOnError() { + listener.register(new RepeatListenerSupport() { + public void onError(RepeatContext context, Throwable e) { + list.add(e); + } + }); + listener.onError(context, new RuntimeException("foo")); + assertEquals(1, list.size()); + } + +} diff --git a/src/test/java/org/springframework/repeat/listener/RepeatListenerTests.java b/src/test/java/org/springframework/repeat/listener/RepeatListenerTests.java new file mode 100644 index 0000000..fc5be09 --- /dev/null +++ b/src/test/java/org/springframework/repeat/listener/RepeatListenerTests.java @@ -0,0 +1,263 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.listener; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.repeat.RepeatCallback; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatListener; +import org.springframework.repeat.RepeatStatus; +import org.springframework.repeat.support.RepeatTemplate; +import org.springframework.repeat.support.TaskExecutorRepeatTemplate; + +public class RepeatListenerTests extends TestCase { + + int count = 0; + + public void testBeforeInterceptors() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.setListeners(new RepeatListener[] { new RepeatListenerSupport() { + public void before(RepeatContext context) { + calls.add("1"); + } + }, new RepeatListenerSupport() { + public void before(RepeatContext context) { + calls.add("2"); + } + } }); + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + count++; + return RepeatStatus.continueIf(count <= 1); + } + }); + // 2 calls: the second time there is no processing + // (despite the fact that the callback returned null and batch was + // complete). Is this OK? + assertEquals(2, count); + // ... but the interceptor before() was called: + assertEquals("[1, 2, 1, 2]", calls.toString()); + } + + public void testBeforeInterceptorCanVeto() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.registerListener(new RepeatListenerSupport() { + public void before(RepeatContext context) { + calls.add("1"); + context.setCompleteOnly(); + } + }); + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + count++; + return RepeatStatus.FINISHED; + } + }); + assertEquals(0, count); + // ... but the interceptor before() was called: + assertEquals("[1]", calls.toString()); + } + + public void testAfterInterceptors() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.setListeners(new RepeatListener[] { new RepeatListenerSupport() { + public void after(RepeatContext context, RepeatStatus result) { + calls.add("1"); + } + }, new RepeatListenerSupport() { + public void after(RepeatContext context, RepeatStatus result) { + calls.add("2"); + } + } }); + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + count++; + return RepeatStatus.continueIf(count <= 1); + } + }); + // 2 calls to the callback, and the second one had no processing... + assertEquals(2, count); + // ... so the interceptor after() is not called: + assertEquals("[2, 1]", calls.toString()); + } + + public void testOpenInterceptors() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.setListeners(new RepeatListener[] { new RepeatListenerSupport() { + public void open(RepeatContext context) { + calls.add("1"); + } + }, new RepeatListenerSupport() { + public void open(RepeatContext context) { + calls.add("2"); + context.setCompleteOnly(); + } + } }); + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + count++; + return RepeatStatus.CONTINUABLE; + } + }); + assertEquals(0, count); + assertEquals("[1, 2]", calls.toString()); + } + + public void testSingleOpenInterceptor() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.registerListener(new RepeatListenerSupport() { + public void open(RepeatContext context) { + calls.add("1"); + } + }); + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + count++; + context.setCompleteOnly(); + return RepeatStatus.FINISHED; + } + }); + assertEquals(1, count); + assertEquals("[1]", calls.toString()); + } + + public void testCloseInterceptors() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.setListeners(new RepeatListener[] { new RepeatListenerSupport() { + public void close(RepeatContext context) { + calls.add("1"); + } + }, new RepeatListenerSupport() { + public void close(RepeatContext context) { + calls.add("2"); + } + } }); + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + count++; + return RepeatStatus.continueIf(count < 2); + } + }); + // Test that more than one call comes in to the callback... + assertEquals(2, count); + // ... but the interceptor is only called once. + assertEquals("[2, 1]", calls.toString()); + } + + + public void testOnErrorInterceptors() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.setListeners(new RepeatListener[] { new RepeatListenerSupport() { + public void onError(RepeatContext context, Throwable t) { + calls.add("1"); + } + }, new RepeatListenerSupport() { + public void onError(RepeatContext context, Throwable t) { + calls.add("2"); + } + } }); + try { + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + throw new IllegalStateException("Bogus"); + } + }); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + assertEquals(0, count); + assertEquals("[2, 1]", calls.toString()); + } + + public void testOnErrorInterceptorsPrecedence() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.setListeners(new RepeatListener[] { new RepeatListenerSupport() { + public void after(RepeatContext context, RepeatStatus result) { + calls.add("1"); + } + }, new RepeatListenerSupport() { + public void onError(RepeatContext context, Throwable t) { + calls.add("2"); + } + } }); + try { + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + throw new IllegalStateException("Bogus"); + } + }); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + assertEquals(0, count); + // The after is not executed, if there is an error... + assertEquals("[2]", calls.toString()); + } + + public void testAsynchronousOnErrorInterceptorsPrecedence() throws Exception { + TaskExecutorRepeatTemplate template = new TaskExecutorRepeatTemplate(); + template.setTaskExecutor(new SimpleAsyncTaskExecutor()); + final List calls = new ArrayList(); + final List fails = new ArrayList(); + template.setListeners(new RepeatListener[] { new RepeatListenerSupport() { + public void after(RepeatContext context, RepeatStatus result) { + calls.add("1"); + } + }, new RepeatListenerSupport() { + public void onError(RepeatContext context, Throwable t) { + calls.add("2"); + fails.add("2"); + } + } }); + try { + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + throw new IllegalStateException("Bogus"); + } + }); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + assertEquals("Bogus", e.getMessage()); + } + assertEquals(0, count); + System.err.println(calls); + // The after is not executed on error... + assertEquals("2", calls.get(0)); + assertEquals("2", calls.get(calls.size()-1)); + assertFalse(calls.contains("1")); + assertEquals(fails.size(), calls.size()); + } +} diff --git a/src/test/java/org/springframework/repeat/policy/CompositeCompletionPolicyTests.java b/src/test/java/org/springframework/repeat/policy/CompositeCompletionPolicyTests.java new file mode 100644 index 0000000..07fd6bf --- /dev/null +++ b/src/test/java/org/springframework/repeat/policy/CompositeCompletionPolicyTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.policy; + +import junit.framework.TestCase; + +import org.springframework.repeat.CompletionPolicy; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatStatus; + +public class CompositeCompletionPolicyTests extends TestCase { + + public void testEmptyPolicies() throws Exception { + CompositeCompletionPolicy policy = new CompositeCompletionPolicy(); + RepeatContext context = policy.start(null); + assertNotNull(context); + assertFalse(policy.isComplete(context)); + } + + public void testTrivialPolicies() throws Exception { + CompositeCompletionPolicy policy = new CompositeCompletionPolicy(); + policy.setPolicies(new CompletionPolicy[] { new MockCompletionPolicySupport(), + new MockCompletionPolicySupport() }); + RepeatContext context = policy.start(null); + assertEquals(0, context.getStartedCount()); + assertFalse(policy.isComplete(context)); + assertFalse(policy.isComplete(context, null)); + policy.update(context); + assertEquals(1, context.getStartedCount()); + } + + public void testNonTrivialPolicies() throws Exception { + CompositeCompletionPolicy policy = new CompositeCompletionPolicy(); + policy.setPolicies(new CompletionPolicy[] { new MockCompletionPolicySupport(), + new MockCompletionPolicySupport() { + public boolean isComplete(RepeatContext context) { + return true; + } + } }); + RepeatContext context = policy.start(null); + assertTrue(policy.isComplete(context)); + } + + public void testNonTrivialPoliciesWithResult() throws Exception { + CompositeCompletionPolicy policy = new CompositeCompletionPolicy(); + policy.setPolicies(new CompletionPolicy[] { new MockCompletionPolicySupport(), + new MockCompletionPolicySupport() { + public boolean isComplete(RepeatContext context, RepeatStatus result) { + return true; + } + } }); + RepeatContext context = policy.start(null); + assertTrue(policy.isComplete(context, null)); + } +} diff --git a/src/test/java/org/springframework/repeat/policy/CountingCompletionPolicyTests.java b/src/test/java/org/springframework/repeat/policy/CountingCompletionPolicyTests.java new file mode 100644 index 0000000..65a1c29 --- /dev/null +++ b/src/test/java/org/springframework/repeat/policy/CountingCompletionPolicyTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.policy; + +import junit.framework.TestCase; + +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatStatus; +import org.springframework.repeat.context.RepeatContextSupport; + +public class CountingCompletionPolicyTests extends TestCase { + + public void testDefaultBehaviour() throws Exception { + CountingCompletionPolicy policy = new CountingCompletionPolicy() { + protected int getCount(RepeatContext context) { + return 1; + }; + }; + RepeatContext context = policy.start(null); + assertTrue(policy.isComplete(context)); + } + + public void testNullResult() throws Exception { + CountingCompletionPolicy policy = new CountingCompletionPolicy() { + protected int getCount(RepeatContext context) { + return 1; + }; + }; + policy.setMaxCount(10); + RepeatContext context = policy.start(null); + assertTrue(policy.isComplete(context, null)); + } + + public void testFinishedResult() throws Exception { + CountingCompletionPolicy policy = new CountingCompletionPolicy() { + protected int getCount(RepeatContext context) { + return 1; + }; + }; + policy.setMaxCount(10); + RepeatContext context = policy.start(null); + assertTrue(policy.isComplete(context, RepeatStatus.FINISHED)); + } + + public void testDefaultBehaviourWithUpdate() throws Exception { + CountingCompletionPolicy policy = new CountingCompletionPolicy() { + int count = 0; + + protected int getCount(RepeatContext context) { + return count; + }; + + protected int doUpdate(RepeatContext context) { + count++; + return 1; + } + }; + policy.setMaxCount(2); + RepeatContext context = policy.start(null); + policy.update(context); + assertFalse(policy.isComplete(context)); + policy.update(context); + assertTrue(policy.isComplete(context)); + } + + public void testUpdateNotSavedAcrossSession() throws Exception { + CountingCompletionPolicy policy = new CountingCompletionPolicy() { + int count = 0; + + protected int getCount(RepeatContext context) { + return count; + }; + + protected int doUpdate(RepeatContext context) { + super.doUpdate(context); + count++; + return 1; + } + + public RepeatContext start(RepeatContext context) { + count = 0; + return super.start(context); + } + }; + policy.setMaxCount(2); + RepeatContextSupport session = new RepeatContextSupport(null); + RepeatContext context = policy.start(session); + policy.update(context); + assertFalse(policy.isComplete(context)); + context = policy.start(session); + policy.update(context); + assertFalse(policy.isComplete(context)); + } + + public void testUpdateSavedAcrossSession() throws Exception { + CountingCompletionPolicy policy = new CountingCompletionPolicy() { + int count = 0; + + protected int getCount(RepeatContext context) { + return count; + }; + + protected int doUpdate(RepeatContext context) { + super.doUpdate(context); + count++; + return 1; + } + + public RepeatContext start(RepeatContext context) { + count = 0; + return super.start(context); + } + }; + policy.setMaxCount(2); + policy.setUseParent(true); + RepeatContextSupport session = new RepeatContextSupport(null); + RepeatContext context = policy.start(session); + policy.update(context); + assertFalse(policy.isComplete(context)); + context = policy.start(session); + policy.update(context); + assertTrue(policy.isComplete(context)); + } +} diff --git a/src/test/java/org/springframework/repeat/policy/MockCompletionPolicySupport.java b/src/test/java/org/springframework/repeat/policy/MockCompletionPolicySupport.java new file mode 100644 index 0000000..359a548 --- /dev/null +++ b/src/test/java/org/springframework/repeat/policy/MockCompletionPolicySupport.java @@ -0,0 +1,27 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.policy; + +import org.springframework.repeat.RepeatContext; + +public class MockCompletionPolicySupport extends CompletionPolicySupport { + + public boolean isComplete(RepeatContext context) { + return false; + } + +} diff --git a/src/test/java/org/springframework/repeat/policy/SimpleCompletionPolicyTests.java b/src/test/java/org/springframework/repeat/policy/SimpleCompletionPolicyTests.java new file mode 100644 index 0000000..db9e1a1 --- /dev/null +++ b/src/test/java/org/springframework/repeat/policy/SimpleCompletionPolicyTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.policy; + +import junit.framework.TestCase; + +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatStatus; + +public class SimpleCompletionPolicyTests extends TestCase { + + SimpleCompletionPolicy policy = new SimpleCompletionPolicy(); + + RepeatContext context; + + RepeatStatus dummy = RepeatStatus.CONTINUABLE; + + protected void setUp() throws Exception { + super.setUp(); + context = policy.start(null); + } + + public void testTerminationAfterDefaultSize() throws Exception { + for (int i = 0; i < SimpleCompletionPolicy.DEFAULT_CHUNK_SIZE - 1; i++) { + policy.update(context); + assertFalse(policy.isComplete(context, dummy)); + } + policy.update(context); + assertTrue(policy.isComplete(context, dummy)); + } + + public void testTerminationAfterExplicitChunkSize() throws Exception { + int chunkSize = 2; + policy.setChunkSize(chunkSize); + for (int i = 0; i < chunkSize - 1; i++) { + policy.update(context); + assertFalse(policy.isComplete(context, dummy)); + } + policy.update(context); + assertTrue(policy.isComplete(context, dummy)); + } + + public void testTerminationAfterNullResult() throws Exception { + policy.update(context); + assertFalse(policy.isComplete(context, dummy)); + policy.update(context); + assertTrue(policy.isComplete(context, null)); + } + + public void testReset() throws Exception { + policy.setChunkSize(2); + policy.update(context); + assertFalse(policy.isComplete(context, dummy)); + policy.update(context); + assertTrue(policy.isComplete(context, dummy)); + context = policy.start(null); + policy.update(context); + assertFalse(policy.isComplete(context, dummy)); + } +} diff --git a/src/test/java/org/springframework/repeat/policy/TimeoutCompletionPolicyTests.java b/src/test/java/org/springframework/repeat/policy/TimeoutCompletionPolicyTests.java new file mode 100644 index 0000000..f4903b7 --- /dev/null +++ b/src/test/java/org/springframework/repeat/policy/TimeoutCompletionPolicyTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.policy; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatStatus; + +public class TimeoutCompletionPolicyTests { + + @Test + public void testSimpleTimeout() throws Exception { + TimeoutTerminationPolicy policy = new TimeoutTerminationPolicy(20L); + RepeatContext context = policy.start(null); + assertFalse(policy.isComplete(context)); + Thread.sleep(50L); + assertTrue(policy.isComplete(context)); + } + + @Test + public void testSuccessfulResult() throws Exception { + TimeoutTerminationPolicy policy = new TimeoutTerminationPolicy(); + RepeatContext context = policy.start(null); + assertFalse(policy.isComplete(context, null)); + } + + @Test + public void testNonContinuableResult() throws Exception { + TimeoutTerminationPolicy policy = new TimeoutTerminationPolicy(); + RepeatStatus result = RepeatStatus.FINISHED; + assertTrue(policy.isComplete(policy.start(null), result)); + } + + @Test + public void testUpdate() throws Exception { + TimeoutTerminationPolicy policy = new TimeoutTerminationPolicy(20L); + RepeatContext context = policy.start(null); + assertFalse(policy.isComplete(context)); + Thread.sleep(50L); + assertTrue(policy.isComplete(context)); + policy.update(context); + // update doesn't change completeness + assertTrue(policy.isComplete(context)); + } + +} diff --git a/src/test/java/org/springframework/repeat/support/AbstractTradeBatchTests.java b/src/test/java/org/springframework/repeat/support/AbstractTradeBatchTests.java new file mode 100644 index 0000000..7295200 --- /dev/null +++ b/src/test/java/org/springframework/repeat/support/AbstractTradeBatchTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import java.util.List; + +import org.junit.Before; + +/** + * Base class for simple tests with small trade data set. + * + * @author Dave Syer + * + */ +public abstract class AbstractTradeBatchTests { + + public static final int NUMBER_OF_ITEMS = 5; + + protected TradeWriter processor = new TradeWriter(); + + protected TradeReader provider; + + @Before + public void setUp() throws Exception { + provider = new TradeReader(); + } + + protected static class TradeReader { + + private Trade[] trades = new Trade[] { + new Trade("UK21341EAH45", 978, "98.34"), + new Trade("UK21341EAH46", 112, "18.12"), + new Trade("UK21341EAH47", 245, "12.78"), + new Trade("UK21341EAH48", 108, "109.25"), + new Trade("UK21341EAH49", 854, "123.39") }; + + private int count = 0; + + public Trade read() { + if (count < trades.length) { + return trades[count++]; + } + return null; + } + + } + + protected static class TradeWriter { + int count = 0; + + // This has to be synchronized because we are going to test the state + // (count) at the end of a concurrent batch run. + public synchronized void write(List data) { + count++; + System.out.println("Executing trade '" + data + "'"); + } + } + +} diff --git a/src/test/java/org/springframework/repeat/support/ChunkedRepeatTests.java b/src/test/java/org/springframework/repeat/support/ChunkedRepeatTests.java new file mode 100644 index 0000000..b7d1832 --- /dev/null +++ b/src/test/java/org/springframework/repeat/support/ChunkedRepeatTests.java @@ -0,0 +1,210 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; + +import org.junit.Test; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.repeat.RepeatCallback; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatStatus; +import org.springframework.repeat.callback.NestedRepeatCallback; +import org.springframework.repeat.policy.SimpleCompletionPolicy; + +/** + * Test various approaches to chunking of a batch. Not really a unit test, but + * it should be fast. + * + * @author Dave Syer + * + */ +public class ChunkedRepeatTests extends AbstractTradeBatchTests { + + private int count = 0; + + public static class TradeRepeatCallback implements RepeatCallback { + + private final TradeReader provider; + private final TradeWriter processor; + + public TradeRepeatCallback(TradeReader provider, TradeWriter processor) { + this.provider = provider; + this.processor = processor; + + } + + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + Trade item = provider.read(); + if (item == null) { + return RepeatStatus.FINISHED; + } + processor.write(Arrays.asList(item)); + return RepeatStatus.CONTINUABLE; + } + } + + + /** + * Chunking using a dedicated TerminationPolicy. Transactions would be laid + * on at the level of chunkTemplate.execute() or the surrounding callback. + * + * @throws Exception + */ + @Test + public void testChunkedBatchWithTerminationPolicy() throws Exception { + + RepeatTemplate repeatTemplate = new RepeatTemplate(); + final RepeatCallback callback = new TradeRepeatCallback(provider, processor); + + final RepeatTemplate chunkTemplate = new RepeatTemplate(); + // The policy is resettable so we only have to resolve this dependency + // once + chunkTemplate.setCompletionPolicy(new SimpleCompletionPolicy(2)); + + RepeatStatus result = repeatTemplate.iterate(new NestedRepeatCallback(chunkTemplate, callback) { + + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + count++; // for test assertion + return super.doInIteration(context); + } + + }); + + assertEquals(NUMBER_OF_ITEMS, processor.count); + // The chunk executes 3 times because the last one + // returns false. We terminate the main batch when + // we encounter a partially empty chunk. + assertEquals(3, count); + assertFalse(result.isContinuable()); + + } + + /** + * Chunking with an asynchronous taskExecutor in the chunks. Transactions + * have to be at the level of the business callback. + * + * @throws Exception + */ + @Test + public void testAsynchronousChunkedBatchWithCompletionPolicy() throws Exception { + + RepeatTemplate repeatTemplate = new RepeatTemplate(); + final RepeatCallback callback = new TradeRepeatCallback(provider, processor); + + final TaskExecutorRepeatTemplate chunkTemplate = new TaskExecutorRepeatTemplate(); + // The policy is resettable so we only have to resolve this dependency + // once + chunkTemplate.setCompletionPolicy(new SimpleCompletionPolicy(2)); + chunkTemplate.setTaskExecutor(new SimpleAsyncTaskExecutor()); + + RepeatStatus result = repeatTemplate.iterate(new NestedRepeatCallback(chunkTemplate, callback) { + + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + count++; // for test assertion + return super.doInIteration(context); + } + + }); + + assertEquals(NUMBER_OF_ITEMS, processor.count); + assertFalse(result.isContinuable()); + assertTrue("Expected at least 3 chunks but found: "+count, count>=3); + + } + + /** + * Explicit chunking of input data. Transactions would be laid on at the + * level of template.execute(). + * + * @throws Exception + */ + @Test + public void testChunksWithTruncatedItemProvider() throws Exception { + + RepeatTemplate template = new RepeatTemplate(); + + // This pattern would work with an asynchronous callback as well + // (but non-transactional in that case). + + class Chunker { + boolean ready = false; + + int count = 0; + + void set() { + ready = true; + } + + boolean ready() { + return ready; + } + + boolean first() { + return count == 0; + } + + void reset() { + count = 0; + ready = false; + } + + void increment() { + count++; + } + } + ; + + final Chunker chunker = new Chunker(); + + while (!chunker.ready()) { + + TradeReader truncated = new TradeReader() { + int count = 0; + + public Trade read() { + if (count++ < 2) + return provider.read(); + return null; + } + }; + chunker.reset(); + template.iterate(new TradeRepeatCallback(truncated, processor) { + + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + RepeatStatus result = super.doInIteration(context); + if (!result.isContinuable() && chunker.first()) { + chunker.set(); + } + chunker.increment(); + return result; + } + + }); + + } + + assertEquals(NUMBER_OF_ITEMS, processor.count); + + } + +} diff --git a/src/test/java/org/springframework/repeat/support/RepeatSynchronizationManagerTests.java b/src/test/java/org/springframework/repeat/support/RepeatSynchronizationManagerTests.java new file mode 100644 index 0000000..7845135 --- /dev/null +++ b/src/test/java/org/springframework/repeat/support/RepeatSynchronizationManagerTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import junit.framework.TestCase; + +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.context.RepeatContextSupport; + +public class RepeatSynchronizationManagerTests extends TestCase { + + private RepeatContext context = new RepeatContextSupport(null); + + protected void setUp() throws Exception { + RepeatSynchronizationManager.clear(); + } + + protected void tearDown() throws Exception { + RepeatSynchronizationManager.clear(); + } + + public void testGetContext() { + RepeatSynchronizationManager.register(context); + assertEquals(context, RepeatSynchronizationManager.getContext()); + } + + public void testSetSessionCompleteOnly() { + assertNull(RepeatSynchronizationManager.getContext()); + RepeatSynchronizationManager.register(context); + assertFalse(RepeatSynchronizationManager.getContext().isCompleteOnly()); + RepeatSynchronizationManager.setCompleteOnly(); + assertTrue(RepeatSynchronizationManager.getContext().isCompleteOnly()); + } + + public void testSetSessionCompleteOnlyWithParent() { + assertNull(RepeatSynchronizationManager.getContext()); + RepeatContext child = new RepeatContextSupport(context); + RepeatSynchronizationManager.register(child); + assertFalse(child.isCompleteOnly()); + RepeatSynchronizationManager.setAncestorsCompleteOnly(); + assertTrue(child.isCompleteOnly()); + assertTrue(context.isCompleteOnly()); + } + + public void testClear() { + RepeatSynchronizationManager.register(context); + assertEquals(context, RepeatSynchronizationManager.getContext()); + RepeatSynchronizationManager.clear(); + assertEquals(null, RepeatSynchronizationManager.getContext()); + } + +} diff --git a/src/test/java/org/springframework/repeat/support/ResultHolderResultQueueTests.java b/src/test/java/org/springframework/repeat/support/ResultHolderResultQueueTests.java new file mode 100644 index 0000000..c573ca6 --- /dev/null +++ b/src/test/java/org/springframework/repeat/support/ResultHolderResultQueueTests.java @@ -0,0 +1,61 @@ +package org.springframework.repeat.support; + + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatStatus; + +public class ResultHolderResultQueueTests { + + private ResultHolderResultQueue queue = new ResultHolderResultQueue(10); + + @Test + public void testPutTake() throws Exception { + queue.expect(); + assertTrue(queue.isExpecting()); + assertTrue(queue.isEmpty()); + queue.put(new TestResultHolder(RepeatStatus.CONTINUABLE)); + assertFalse(queue.isEmpty()); + assertTrue(queue.take().getResult().isContinuable()); + assertFalse(queue.isExpecting()); + } + + @Test + public void testOrdering() throws Exception { + queue.expect(); + queue.expect(); + queue.put(new TestResultHolder(RepeatStatus.FINISHED)); + queue.put(new TestResultHolder(RepeatStatus.CONTINUABLE)); + assertFalse(queue.isEmpty()); + assertTrue(queue.take().getResult().isContinuable()); + assertFalse(queue.take().getResult().isContinuable()); + } + + private static class TestResultHolder implements ResultHolder { + + private RepeatStatus result; + + private Throwable error; + + public TestResultHolder(RepeatStatus result) { + super(); + this.result = result; + } + + public RepeatContext getContext() { + return null; + } + + public Throwable getError() { + return error; + } + + public RepeatStatus getResult() { + return result; + } + } + +} diff --git a/src/test/java/org/springframework/repeat/support/SimpleRepeatTemplateTests.java b/src/test/java/org/springframework/repeat/support/SimpleRepeatTemplateTests.java new file mode 100644 index 0000000..1d90d68 --- /dev/null +++ b/src/test/java/org/springframework/repeat/support/SimpleRepeatTemplateTests.java @@ -0,0 +1,514 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.springframework.repeat.RepeatCallback; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatException; +import org.springframework.repeat.RepeatListener; +import org.springframework.repeat.RepeatStatus; +import org.springframework.repeat.callback.NestedRepeatCallback; +import org.springframework.repeat.context.RepeatContextSupport; +import org.springframework.repeat.exception.ExceptionHandler; +import org.springframework.repeat.listener.RepeatListenerSupport; +import org.springframework.repeat.policy.CompletionPolicySupport; +import org.springframework.repeat.policy.SimpleCompletionPolicy; + +/** + * @author Dave Syer + */ +public class SimpleRepeatTemplateTests extends AbstractTradeBatchTests { + + private RepeatTemplate template = getRepeatTemplate(); + + private int count = 0; + + public static class TradeRepeatCallback implements RepeatCallback { + + private final TradeReader provider; + private final TradeWriter processor; + + public TradeRepeatCallback(TradeReader provider, TradeWriter processor) { + this.provider = provider; + this.processor = processor; + + } + + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + Trade item = provider.read(); + if (item == null) { + return RepeatStatus.FINISHED; + } + processor.write(Arrays.asList(item)); + return RepeatStatus.CONTINUABLE; + } + } + + public RepeatTemplate getRepeatTemplate() { + template = new RepeatTemplate(); + // default stop after more items than exist in dataset + template.setCompletionPolicy(new SimpleCompletionPolicy(8)); + return template; + } + + @Test + public void testExecute() throws Exception { + template.iterate(new TradeRepeatCallback(provider, processor)); + assertEquals(NUMBER_OF_ITEMS, processor.count); + } + + /** + * Check that a dedicated TerminationPolicy can terminate the batch. + * + * @throws Exception + */ + @Test + public void testEarlyCompletionWithPolicy() throws Exception { + + template.setCompletionPolicy(new SimpleCompletionPolicy(2)); + + template.iterate(new TradeRepeatCallback(provider, processor)); + + assertEquals(2, processor.count); + + } + + /** + * Check that a dedicated TerminationPolicy can terminate the batch. + * + * @throws Exception + */ + @Test + public void testEarlyCompletionWithException() throws Exception { + + try { + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + count++; + throw new IllegalStateException("foo!"); + } + }); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + assertEquals("foo!", e.getMessage()); + } + + assertEquals(1, count); + assertTrue("Too many attempts: " + count, count <= 10); + + } + + /** + * Check that the context is closed. + * + * @throws Exception + */ + @Test + public void testContextClosedOnNormalCompletion() throws Exception { + + final List list = new ArrayList(); + + final RepeatContext context = new RepeatContextSupport(null) { + public void close() { + super.close(); + list.add("close"); + } + }; + template.setCompletionPolicy(new CompletionPolicySupport() { + public RepeatContext start(RepeatContext c) { + return context; + } + }); + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + count++; + return RepeatStatus.continueIf(count < 1); + } + }); + + assertEquals(1, count); + assertEquals(1, list.size()); + + } + + /** + * Check that the context is closed. + * + * @throws Exception + */ + @Test + public void testContextClosedOnAbnormalCompletion() throws Exception { + + final List list = new ArrayList(); + + final RepeatContext context = new RepeatContextSupport(null) { + public void close() { + super.close(); + list.add("close"); + } + }; + template.setCompletionPolicy(new CompletionPolicySupport() { + public RepeatContext start(RepeatContext c) { + return context; + } + }); + + try { + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + count++; + throw new RuntimeException("foo"); + } + }); + } catch (RuntimeException e) { + assertEquals("foo", e.getMessage()); + } + + assertEquals(1, count); + assertEquals(1, list.size()); + + } + + /** + * Check that the exception handler is called. + * + * @throws Exception + */ + @Test + public void testExceptionHandlerCalledOnAbnormalCompletion() + throws Exception { + + final List list = new ArrayList(); + + template.setExceptionHandler(new ExceptionHandler() { + public void handleException(RepeatContext context, + Throwable throwable) throws RuntimeException { + list.add(throwable); + throw (RuntimeException) throwable; + } + }); + + try { + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + count++; + throw new RuntimeException("foo"); + } + }); + } catch (RuntimeException e) { + assertEquals("foo", e.getMessage()); + } + + assertEquals(1, count); + assertEquals(1, list.size()); + + } + + /** + * Check that a the context can be used to signal early completion. + * + * @throws Exception + */ + @Test + public void testEarlyCompletionWithContext() throws Exception { + + RepeatStatus result = template.iterate(new TradeRepeatCallback( + provider, processor) { + + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + RepeatStatus result = super.doInIteration(context); + if (processor.count >= 2) { + context.setCompleteOnly(); + // If we return null the batch will terminate anyway + // without an exception... + } + return result; + } + }); + + // 2 items were processed before completion signalled + assertEquals(2, processor.count); + + // Not all items processed + assertTrue(result.isContinuable()); + + } + + /** + * Check that a the context can be used to signal early completion. + * + * @throws Exception + */ + @Test + public void testEarlyCompletionWithContextTerminated() throws Exception { + + RepeatStatus result = template.iterate(new TradeRepeatCallback( + provider, processor) { + + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + RepeatStatus result = super.doInIteration(context); + if (processor.count >= 2) { + context.setTerminateOnly(); + // If we return null the batch will terminate anyway + // without an exception... + } + return result; + } + }); + + // 2 items were processed before completion signalled + assertEquals(2, processor.count); + + // Not all items processed + assertTrue(result.isContinuable()); + + } + + @Test + public void testNestedSession() throws Exception { + RepeatTemplate outer = getRepeatTemplate(); + RepeatTemplate inner = getRepeatTemplate(); + outer.iterate(new NestedRepeatCallback(inner, new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + count++; + assertNotNull(context); + assertNotSame("Nested batch should have new session", context, + context.getParent()); + assertSame(context, RepeatSynchronizationManager.getContext()); + return RepeatStatus.FINISHED; + } + }) { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + count++; + assertSame(context, RepeatSynchronizationManager.getContext()); + return super.doInIteration(context); + } + }); + assertEquals(2, count); + } + + @Test + public void testNestedSessionTerminatesBeforeIteration() throws Exception { + RepeatTemplate outer = getRepeatTemplate(); + RepeatTemplate inner = getRepeatTemplate(); + outer.iterate(new NestedRepeatCallback(inner, new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + count++; + assertEquals(2, count); + fail("Nested batch should not have been executed"); + return RepeatStatus.FINISHED; + } + }) { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + count++; + context.setCompleteOnly(); + return super.doInIteration(context); + } + }); + assertEquals(1, count); + } + + @Test + public void testOuterContextPreserved() throws Exception { + RepeatTemplate outer = getRepeatTemplate(); + outer.setCompletionPolicy(new SimpleCompletionPolicy(2)); + RepeatTemplate inner = getRepeatTemplate(); + outer.iterate(new NestedRepeatCallback(inner, new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + count++; + assertNotNull(context); + assertNotSame("Nested batch should have new session", context, + context.getParent()); + assertSame(context, RepeatSynchronizationManager.getContext()); + return RepeatStatus.FINISHED; + } + }) { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + count++; + assertSame(context, RepeatSynchronizationManager.getContext()); + super.doInIteration(context); + return RepeatStatus.CONTINUABLE; + } + }); + assertEquals(4, count); + } + + /** + * Test that a result is returned from the batch. + * + * @throws Exception + */ + @Test + public void testResult() throws Exception { + RepeatStatus result = template.iterate(new TradeRepeatCallback( + provider, processor)); + assertEquals(NUMBER_OF_ITEMS, processor.count); + // We are complete - do not expect to be called again + assertFalse(result.isContinuable()); + } + + @Test + public void testExceptionThrownOnLastItem() throws Exception { + template.setCompletionPolicy(new SimpleCompletionPolicy(2)); + try { + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + count++; + if (count < 2) { + return RepeatStatus.CONTINUABLE; + } + throw new RuntimeException("Barf second try count=" + count); + } + }); + fail("Expected exception on last item in batch"); + } catch (Exception e) { + // expected + assertEquals("Barf second try count=2", e.getMessage()); + } + } + + /** + * Check that a the session can be used to signal early completion, but an + * exception takes precedence. + * + * @throws Exception + */ + @Test + public void testEarlyCompletionWithSessionAndException() throws Exception { + + template.setCompletionPolicy(new SimpleCompletionPolicy(4)); + + RepeatStatus result = RepeatStatus.FINISHED; + + try { + result = template.iterate(new TradeRepeatCallback(provider, + processor) { + + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + RepeatStatus result = super.doInIteration(context); + if (processor.count >= 2) { + context.setCompleteOnly(); + throw new RuntimeException("Barf second try count=" + + processor.count); + } + return result; + } + }); + fail("Expected exception on last item in batch"); + } catch (RuntimeException e) { + // expected + assertEquals("Barf second try count=2", e.getMessage()); + } + + // 2 items were processed before completion signalled + assertEquals(2, processor.count); + + System.err.println(result); + + // An exception was thrown by the template so result is still false + assertFalse(result.isContinuable()); + + } + + /** + * Checked exceptions are wrapped into runtime RepeatException. + * RepeatException should be unwrapped before before it is passed to + * listeners and exception handler. + */ + @Test + public void testExceptionUnwrapping() { + + class TestException extends Exception { + TestException(String msg) { + super(msg); + } + } + final TestException exception = new TestException("CRASH!"); + + class ExceptionHandlerStub implements ExceptionHandler { + boolean called = false; + + public void handleException(RepeatContext context, + Throwable throwable) throws Throwable { + called = true; + assertSame(exception, throwable); + throw throwable; // re-throw so that repeat template + // terminates iteration + } + } + ExceptionHandlerStub exHandler = new ExceptionHandlerStub(); + + class RepeatListenerStub extends RepeatListenerSupport { + boolean called = false; + + public void onError(RepeatContext context, Throwable throwable) { + called = true; + assertSame(exception, throwable); + } + } + RepeatListenerStub listener = new RepeatListenerStub(); + + template.setExceptionHandler(exHandler); + template.setListeners(new RepeatListener[] { listener }); + + try { + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + throw new RepeatException( + "typically thrown by nested repeat template", + exception); + } + }); + fail(); + } catch (RepeatException expected) { + assertSame(exception, expected.getCause()); + } + + assertTrue(listener.called); + assertTrue(exHandler.called); + + } +} diff --git a/src/test/java/org/springframework/repeat/support/TaskExecutorRepeatTemplateAsynchronousTests.java b/src/test/java/org/springframework/repeat/support/TaskExecutorRepeatTemplateAsynchronousTests.java new file mode 100644 index 0000000..e010269 --- /dev/null +++ b/src/test/java/org/springframework/repeat/support/TaskExecutorRepeatTemplateAsynchronousTests.java @@ -0,0 +1,270 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.Test; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.repeat.RepeatCallback; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatStatus; +import org.springframework.repeat.callback.NestedRepeatCallback; +import org.springframework.repeat.exception.ExceptionHandler; +import org.springframework.repeat.policy.SimpleCompletionPolicy; + +public class TaskExecutorRepeatTemplateAsynchronousTests extends + AbstractTradeBatchTests { + + RepeatTemplate template = getRepeatTemplate(); + + int count = 0; + + // @Override + public RepeatTemplate getRepeatTemplate() { + TaskExecutorRepeatTemplate template = new TaskExecutorRepeatTemplate(); + template.setTaskExecutor(new SimpleAsyncTaskExecutor()); + // Set default completion above number of items in input file + template.setCompletionPolicy(new SimpleCompletionPolicy(8)); + return template; + } + + @Test + public void testEarlyCompletionWithException() throws Exception { + + TaskExecutorRepeatTemplate template = new TaskExecutorRepeatTemplate(); + SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + template.setCompletionPolicy(new SimpleCompletionPolicy(20)); + taskExecutor.setConcurrencyLimit(2); + template.setTaskExecutor(taskExecutor); + try { + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + count++; + throw new IllegalStateException("foo!"); + } + }); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + assertEquals("foo!", e.getMessage()); + } + + assertTrue("Too few attempts: " + count, count >= 1); + assertTrue("Too many attempts: " + count, count <= 10); + + } + + @Test + public void testExceptionHandlerSwallowsException() throws Exception { + + TaskExecutorRepeatTemplate template = new TaskExecutorRepeatTemplate(); + SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + template.setCompletionPolicy(new SimpleCompletionPolicy(4)); + taskExecutor.setConcurrencyLimit(2); + template.setTaskExecutor(taskExecutor); + + template.setExceptionHandler(new ExceptionHandler() { + public void handleException(RepeatContext context, + Throwable throwable) throws Throwable { + count++; + } + }); + template.iterate(new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + throw new IllegalStateException("foo!"); + } + }); + + assertTrue("Too few attempts: " + count, count >= 1); + assertTrue("Too many attempts: " + count, count <= 10); + + } + + @Test + public void testNestedSession() throws Exception { + + RepeatTemplate outer = getRepeatTemplate(); + RepeatTemplate inner = new RepeatTemplate(); + + outer.iterate(new NestedRepeatCallback(inner, new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + count++; + assertNotNull(context); + assertNotSame("Nested batch should have new session", context, + context.getParent()); + assertSame(context, RepeatSynchronizationManager.getContext()); + return RepeatStatus.FINISHED; + } + }) { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + count++; + assertNotNull(context); + assertSame(context, RepeatSynchronizationManager.getContext()); + return super.doInIteration(context); + } + }); + + assertTrue("Too few attempts: " + count, count >= 1); + assertTrue("Too many attempts: " + count, count <= 10); + + } + + /** + * Run a batch with a single template that itself has an async task + * executor. The result is a batch that runs in multiple threads (up to the + * throttle limit of the template). + * + * @throws Exception + */ + @Test + public void testMultiThreadAsynchronousExecution() throws Exception { + + final String threadName = Thread.currentThread().getName(); + final Set threadNames = new HashSet(); + + final RepeatCallback callback = new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + assertNotSame(threadName, Thread.currentThread().getName()); + threadNames.add(Thread.currentThread().getName()); + Thread.sleep(100); + Trade item = provider.read(); + if (item != null) { + processor.write(Collections.singletonList(item)); + } + return RepeatStatus.continueIf(item != null); + } + }; + + template.iterate(callback); + // Shouldn't be necessary to wait: + // Thread.sleep(500); + assertEquals(NUMBER_OF_ITEMS, processor.count); + assertTrue(threadNames.size() > 1); + } + + @Test + public void testThrottleLimit() throws Exception { + + int throttleLimit = 600; + + TaskExecutorRepeatTemplate template = new TaskExecutorRepeatTemplate(); + SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + taskExecutor.setConcurrencyLimit(300); + template.setTaskExecutor(taskExecutor); + template.setThrottleLimit(throttleLimit); + + final String threadName = Thread.currentThread().getName(); + final Set threadNames = new HashSet(); + final List items = new ArrayList(); + + final RepeatCallback callback = new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + assertNotSame(threadName, Thread.currentThread().getName()); + Trade item = provider.read(); + threadNames + .add(Thread.currentThread().getName() + " : " + item); + items.add("" + item); + if (item != null) { + processor.write(Collections.singletonList(item)); + // Do some more I/O + for (int i = 0; i < 10; i++) { + TradeReader provider = new TradeReader(); + while (provider.read() != null) + continue; + } + } + return RepeatStatus.continueIf(item != null); + } + }; + + template.iterate(callback); + // Shouldn't be necessary to wait: + // Thread.sleep(500); + assertEquals(NUMBER_OF_ITEMS, processor.count); + assertTrue(threadNames.size() > 1); + int frequency = Collections.frequency(items, "null"); + // System.err.println("Frequency: "+frequency); + assertTrue(frequency <= throttleLimit); + } + + /** + * Wrap an otherwise synchronous batch in a callback to an asynchronous + * template. + * + * @throws Exception + */ + @Test + public void testSingleThreadAsynchronousExecution() throws Exception { + TaskExecutorRepeatTemplate jobTemplate = new TaskExecutorRepeatTemplate(); + final RepeatTemplate stepTemplate = new RepeatTemplate(); + SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + taskExecutor.setConcurrencyLimit(2); + jobTemplate.setTaskExecutor(taskExecutor); + + final String threadName = Thread.currentThread().getName(); + final Set threadNames = new HashSet(); + + final RepeatCallback stepCallback = new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + assertNotSame(threadName, Thread.currentThread().getName()); + threadNames.add(Thread.currentThread().getName()); + Thread.sleep(100); + Trade item = provider.read(); + if (item != null) { + processor.write(Collections.singletonList(item)); + } + return RepeatStatus.continueIf(item != null); + } + }; + RepeatCallback jobCallback = new RepeatCallback() { + public RepeatStatus doInIteration(RepeatContext context) + throws Exception { + stepTemplate.iterate(stepCallback); + return RepeatStatus.FINISHED; + } + }; + + jobTemplate.iterate(jobCallback); + // Shouldn't be necessary to wait: + // Thread.sleep(500); + assertEquals(NUMBER_OF_ITEMS, processor.count); + // Because of the throttling and queueing internally to a TaskExecutor, + // more than one thread will be used - the number used is the + // concurrency limit in the task executor, plus 1. + // System.err.println(threadNames); + assertTrue(threadNames.size() >= 1); + } + +} diff --git a/src/test/java/org/springframework/repeat/support/TaskExecutorRepeatTemplateBulkAsynchronousTests.java b/src/test/java/org/springframework/repeat/support/TaskExecutorRepeatTemplateBulkAsynchronousTests.java new file mode 100644 index 0000000..5a74a6f --- /dev/null +++ b/src/test/java/org/springframework/repeat/support/TaskExecutorRepeatTemplateBulkAsynchronousTests.java @@ -0,0 +1,201 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.repeat.RepeatCallback; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.RepeatStatus; +import org.springframework.repeat.policy.SimpleCompletionPolicy; + +/** + * Simple tests for concurrent behaviour in repeat template, in particular the + * barrier at the end of the iteration. N.B. these tests may fail if + * insufficient threads are available (e.g. on a single-core machine, or under + * load). They shouldn't deadlock though. + * + * @author Dave Syer + * + */ +public class TaskExecutorRepeatTemplateBulkAsynchronousTests { + + static Log logger = LogFactory.getLog(TaskExecutorRepeatTemplateBulkAsynchronousTests.class); + + private int total = 20; + + private int throttleLimit = 8; + + private volatile int early = Integer.MAX_VALUE; + + private TaskExecutorRepeatTemplate template; + + private RepeatCallback callback; + + private List items; + + @Before + public void setUp() { + + template = new TaskExecutorRepeatTemplate(); + SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + taskExecutor.setConcurrencyLimit(300); + template.setTaskExecutor(taskExecutor); + template.setThrottleLimit(throttleLimit); + + items = Collections.synchronizedList(new ArrayList()); + + callback = new RepeatCallback() { + + private volatile AtomicInteger count = new AtomicInteger(0); + + public RepeatStatus doInIteration(RepeatContext context) throws Exception { + int position = count.incrementAndGet(); + String item = position <= total ? "" + position : null; + items.add("" + item); + if (item != null) { + beBusy(); + } + /* + * In a multi-threaded task, one of the callbacks can call + * FINISHED early, while other threads are still working, and + * would do more work if the callback was called again. (This + * happens for instance if there is a failure and you want to + * retry the work.) + */ + RepeatStatus result = RepeatStatus.continueIf(position != early && item != null); + if (!result.isContinuable()) { + logger.debug("Returning " + result + " for count=" + position); + } + return result; + } + }; + + } + + @Test + public void testThrottleLimit() throws Exception { + + template.iterate(callback); + int frequency = Collections.frequency(items, "null"); + // System.err.println(items); + // System.err.println("Frequency: " + frequency); + assertEquals(total, items.size() - frequency); + assertTrue(frequency > 1); + assertTrue(frequency <= throttleLimit + 1); + + } + + @Test + public void testThrottleLimitEarlyFinish() throws Exception { + + early = 2; + + template.iterate(callback); + int frequency = Collections.frequency(items, "null"); + // System.err.println("Frequency: " + frequency); + // System.err.println("Items: " + items); + assertEquals(total, items.size() - frequency); + assertTrue(frequency > 1); + assertTrue(frequency <= throttleLimit + 1); + + } + + @Test + public void testThrottleLimitEarlyFinishThreadStarvation() throws Exception { + + early = 2; + SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + // Set the concurrency limit below the throttle limit for possible + // starvation condition + taskExecutor.setConcurrencyLimit(20); + template.setTaskExecutor(taskExecutor); + + template.iterate(callback); + int frequency = Collections.frequency(items, "null"); + // System.err.println("Frequency: " + frequency); + // System.err.println("Items: " + items); + // Extra tasks will be submitted before the termination is detected + assertEquals(total, items.size() - frequency); + assertTrue(frequency <= throttleLimit + 1); + + } + + @Test + public void testThrottleLimitEarlyFinishOneThread() throws Exception { + + early = 4; + SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + taskExecutor.setConcurrencyLimit(1); + + // This is kind of slow with only one thread, so reduce size: + throttleLimit = 4; + total = 10; + + template.setThrottleLimit(throttleLimit); + template.setTaskExecutor(taskExecutor); + + template.iterate(callback); + int frequency = Collections.frequency(items, "null"); + // System.err.println("Frequency: " + frequency); + // System.err.println("Items: " + items); + assertEquals(total, items.size() - frequency); + assertTrue(frequency <= throttleLimit + 1); + + } + + @Test + public void testThrottleLimitWithEarlyCompletion() throws Exception { + + early = 2; + template.setCompletionPolicy(new SimpleCompletionPolicy(10)); + + template.iterate(callback); + int frequency = Collections.frequency(items, "null"); + assertEquals(10, items.size() - frequency); + // System.err.println("Frequency: " + frequency); + assertEquals(0, frequency); + + } + + /** + * Slightly flakey convenience method. If this doesn't do something that + * lasts sufficiently long for another worker to be launched while it is + * busy, the early completion tests will fail. "Sufficiently long" is the + * problem so we try and block until we know someone else is busy? + * + * @throws Exception + */ + private void beBusy() throws Exception { + synchronized (this) { + wait(100L); + notifyAll(); + } + } + +} diff --git a/src/test/java/org/springframework/repeat/support/TaskExecutorRepeatTemplateTests.java b/src/test/java/org/springframework/repeat/support/TaskExecutorRepeatTemplateTests.java new file mode 100644 index 0000000..28289c0 --- /dev/null +++ b/src/test/java/org/springframework/repeat/support/TaskExecutorRepeatTemplateTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import static org.junit.Assert.fail; + +import org.junit.Test; + + +/** + * @author Dave Syer + */ +public class TaskExecutorRepeatTemplateTests extends SimpleRepeatTemplateTests { + + public RepeatTemplate getRepeatTemplate() { + return new TaskExecutorRepeatTemplate(); + } + + @Test + public void testSetThrottleLimit() throws Exception { + try { + new TaskExecutorRepeatTemplate().setThrottleLimit(-1); + } catch (Exception e) { + // unexpected - no check for illegal values + fail("Unexpected Exception setting throttle limit"); + } + } + +} diff --git a/src/test/java/org/springframework/repeat/support/ThrottleLimitResultQueueTests.java b/src/test/java/org/springframework/repeat/support/ThrottleLimitResultQueueTests.java new file mode 100644 index 0000000..7d43e0c --- /dev/null +++ b/src/test/java/org/springframework/repeat/support/ThrottleLimitResultQueueTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.repeat.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.NoSuchElementException; + +import org.junit.Test; + + +/** + * @author Dave Syer + * + */ +public class ThrottleLimitResultQueueTests { + + private ThrottleLimitResultQueue queue = new ThrottleLimitResultQueue(1); + + @Test + public void testPutTake() throws Exception { + queue.expect(); + assertTrue(queue.isExpecting()); + assertTrue(queue.isEmpty()); + queue.put("foo"); + assertFalse(queue.isEmpty()); + assertEquals("foo", queue.take()); + assertFalse(queue.isExpecting()); + } + + @Test + public void testPutWithoutExpecting() throws Exception { + assertFalse(queue.isExpecting()); + try { + queue.put("foo"); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testTakeWithoutExpecting() throws Exception { + assertFalse(queue.isExpecting()); + try { + queue.take(); + fail("Expected NoSuchElementException"); + } catch (NoSuchElementException e) { + // expected + } + } + + @Test + public void testThrottleLimit() throws Exception { + queue.expect(); + new Thread(new Runnable() { + public void run() { + try { + Thread.sleep(100L); + } + catch (InterruptedException e) { + e.printStackTrace(); + Thread.currentThread().interrupt(); + } + queue.put("foo"); + } + }).start(); + long t0 = System.currentTimeMillis(); + queue.expect(); + long t1 = System.currentTimeMillis(); + assertEquals("foo", queue.take()); + assertTrue(queue.isExpecting()); + assertTrue("Did not block on expect (throttle limit should have been hit): time taken="+(t1-t0), t1-t0>50); + } + +} diff --git a/src/test/java/org/springframework/repeat/support/Trade.java b/src/test/java/org/springframework/repeat/support/Trade.java new file mode 100644 index 0000000..66429bb --- /dev/null +++ b/src/test/java/org/springframework/repeat/support/Trade.java @@ -0,0 +1,53 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.repeat.support; + +import java.math.BigDecimal; + +/** + * @author Rob Harrop + */ +public class Trade { + + private String isin; + + private long quantity; + + private BigDecimal price; + + Trade(String isin, long quantity, String price) { + this.isin = isin; + this.quantity = quantity; + this.price = new BigDecimal(price); + } + + public String getIsin() { + return isin; + } + + public BigDecimal getPrice() { + return price; + } + + public long getQuantity() { + return quantity; + } + + public String toString() { + return "Trade: [isin=" + isin + ",quantity=" + quantity + ",price=" + price + "]"; + } +} diff --git a/src/test/java/org/springframework/retry/AbstractExceptionTests.java b/src/test/java/org/springframework/retry/AbstractExceptionTests.java new file mode 100644 index 0000000..8b2a61a --- /dev/null +++ b/src/test/java/org/springframework/retry/AbstractExceptionTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry; + +import junit.framework.TestCase; + +public abstract class AbstractExceptionTests extends TestCase { + + public void testExceptionString() throws Exception { + Exception exception = getException("foo"); + assertEquals("foo", exception.getMessage()); + } + + public void testExceptionStringThrowable() throws Exception { + Exception exception = getException("foo", new IllegalStateException()); + assertEquals("foo", exception.getMessage().substring(0, 3)); + } + + public abstract Exception getException(String msg) throws Exception; + + public abstract Exception getException(String msg, Throwable t) throws Exception; +} diff --git a/src/test/java/org/springframework/retry/BackOffInterruptedExceptionTests.java b/src/test/java/org/springframework/retry/BackOffInterruptedExceptionTests.java new file mode 100644 index 0000000..9910393 --- /dev/null +++ b/src/test/java/org/springframework/retry/BackOffInterruptedExceptionTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry; + +import org.springframework.retry.backoff.BackOffInterruptedException; + +public class BackOffInterruptedExceptionTests extends AbstractExceptionTests { + + public Exception getException(String msg) throws Exception { + return new BackOffInterruptedException(msg); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new BackOffInterruptedException(msg, t); + } + + public void testNothing() throws Exception { + // fool coverage tools... + } +} diff --git a/src/test/java/org/springframework/retry/ExhaustedRetryExceptionTests.java b/src/test/java/org/springframework/retry/ExhaustedRetryExceptionTests.java new file mode 100644 index 0000000..8674f9d --- /dev/null +++ b/src/test/java/org/springframework/retry/ExhaustedRetryExceptionTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry; + +import org.springframework.retry.ExhaustedRetryException; + +public class ExhaustedRetryExceptionTests extends AbstractExceptionTests { + + public Exception getException(String msg) throws Exception { + return new ExhaustedRetryException(msg); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new ExhaustedRetryException(msg, t); + } + + public void testNothing() throws Exception { + // fool coverage tools... + } +} diff --git a/src/test/java/org/springframework/retry/ResourcelessTransactionManager.java b/src/test/java/org/springframework/retry/ResourcelessTransactionManager.java new file mode 100644 index 0000000..ae50754 --- /dev/null +++ b/src/test/java/org/springframework/retry/ResourcelessTransactionManager.java @@ -0,0 +1,80 @@ +package org.springframework.retry; + +import java.util.Stack; + +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +public class ResourcelessTransactionManager extends AbstractPlatformTransactionManager { + + protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { + ((ResourcelessTransaction) transaction).begin(); + } + + protected void doCommit(DefaultTransactionStatus status) throws TransactionException { + logger.debug("Committing resourceless transaction on [" + status.getTransaction() + "]"); + } + + protected Object doGetTransaction() throws TransactionException { + Object transaction = new ResourcelessTransaction(); + Stack resources; + if (!TransactionSynchronizationManager.hasResource(this)) { + resources = new Stack(); + TransactionSynchronizationManager.bindResource(this, resources); + } + else { + @SuppressWarnings("unchecked") + Stack stack = (Stack) TransactionSynchronizationManager.getResource(this); + resources = stack; + } + resources.push(transaction); + return transaction; + } + + protected void doRollback(DefaultTransactionStatus status) throws TransactionException { + logger.debug("Rolling back resourceless transaction on [" + status.getTransaction() + "]"); + } + + protected boolean isExistingTransaction(Object transaction) throws TransactionException { + if (TransactionSynchronizationManager.hasResource(this)) { + @SuppressWarnings("unchecked") + Stack stack = (Stack) TransactionSynchronizationManager.getResource(this); + return stack.size() > 1; + } + return ((ResourcelessTransaction) transaction).isActive(); + } + + protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException { + } + + protected void doCleanupAfterCompletion(Object transaction) { + @SuppressWarnings("unchecked") + Stack list = (Stack) TransactionSynchronizationManager.getResource(this); + Stack resources = list; + resources.clear(); + TransactionSynchronizationManager.unbindResource(this); + ((ResourcelessTransaction) transaction).clear(); + } + + private static class ResourcelessTransaction { + + private boolean active = false; + + public boolean isActive() { + return active; + } + + public void begin() { + active = true; + } + + public void clear() { + active = false; + } + + } + +} diff --git a/src/test/java/org/springframework/retry/RetryExceptionTests.java b/src/test/java/org/springframework/retry/RetryExceptionTests.java new file mode 100644 index 0000000..eb1827f --- /dev/null +++ b/src/test/java/org/springframework/retry/RetryExceptionTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry; + +import org.springframework.retry.RetryException; + +public class RetryExceptionTests extends AbstractExceptionTests { + + public Exception getException(String msg) throws Exception { + return new RetryException(msg); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new RetryException(msg, t); + } + + public void testNothing() throws Exception { + // fool coverage tools... + } +} diff --git a/src/test/java/org/springframework/retry/TerminatedRetryExceptionTests.java b/src/test/java/org/springframework/retry/TerminatedRetryExceptionTests.java new file mode 100644 index 0000000..d5745c0 --- /dev/null +++ b/src/test/java/org/springframework/retry/TerminatedRetryExceptionTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry; + +import org.springframework.retry.TerminatedRetryException; + +public class TerminatedRetryExceptionTests extends AbstractExceptionTests { + + public Exception getException(String msg) throws Exception { + return new TerminatedRetryException(msg); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new TerminatedRetryException(msg, t); + } + + public void testNothing() throws Exception { + // fool coverage tools... + } +} diff --git a/src/test/java/org/springframework/retry/backoff/DummySleeper.java b/src/test/java/org/springframework/retry/backoff/DummySleeper.java new file mode 100644 index 0000000..9e56251 --- /dev/null +++ b/src/test/java/org/springframework/retry/backoff/DummySleeper.java @@ -0,0 +1,58 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.retry.backoff; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Simple {@link Sleeper} implementation that just waits on a local Object. + * + * @author Dave Syer + * + */ +public class DummySleeper implements Sleeper { + + private List backOffs = new ArrayList(); + + /** + * Public getter for the long. + * @return the lastBackOff + */ + public long getLastBackOff() { + return backOffs.get(backOffs.size()-1).longValue(); + } + + public long[] getBackOffs() { + long[] result = new long[backOffs.size()]; + int i = 0; + for (Iterator iterator = backOffs.iterator(); iterator.hasNext();) { + Long value = iterator.next(); + result[i++] =value.longValue(); + } + return result ; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.retry.backoff.Sleeper#sleep(long) + */ + public void sleep(long backOffPeriod) throws InterruptedException { + this.backOffs.add(Long.valueOf(backOffPeriod)); + } + +} diff --git a/src/test/java/org/springframework/retry/backoff/ExponentialBackOffPolicyTests.java b/src/test/java/org/springframework/retry/backoff/ExponentialBackOffPolicyTests.java new file mode 100644 index 0000000..5c280f9 --- /dev/null +++ b/src/test/java/org/springframework/retry/backoff/ExponentialBackOffPolicyTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.backoff; + +import junit.framework.TestCase; + +/** + * @author Rob Harrop + * @author Dave Syer + */ +public class ExponentialBackOffPolicyTests extends TestCase { + + private DummySleeper sleeper = new DummySleeper(); + + public void testSetMaxInterval() throws Exception { + ExponentialBackOffPolicy strategy = new ExponentialBackOffPolicy(); + strategy.setMaxInterval(1000); + assertTrue(strategy.toString().indexOf("maxInterval=1000") >= 0); + strategy.setMaxInterval(0); + // The minimum value for the max interval is 1 + assertTrue(strategy.toString().indexOf("maxInterval=1") >= 0); + } + + public void testSetInitialInterval() throws Exception { + ExponentialBackOffPolicy strategy = new ExponentialBackOffPolicy(); + strategy.setInitialInterval(10000); + assertTrue(strategy.toString().indexOf("initialInterval=10000,") >= 0); + strategy.setInitialInterval(0); + assertTrue(strategy.toString().indexOf("initialInterval=1,") >= 0); + } + + public void testSetMultiplier() throws Exception { + ExponentialBackOffPolicy strategy = new ExponentialBackOffPolicy(); + strategy.setMultiplier(3.); + assertTrue(strategy.toString().indexOf("multiplier=3.") >= 0); + strategy.setMultiplier(.5); + assertTrue(strategy.toString().indexOf("multiplier=1.") >= 0); + } + + public void testSingleBackOff() throws Exception { + ExponentialBackOffPolicy strategy = new ExponentialBackOffPolicy(); + strategy.setSleeper(sleeper); + BackOffContext context = strategy.start(null); + strategy.backOff(context); + assertEquals(ExponentialBackOffPolicy.DEFAULT_INITIAL_INTERVAL, sleeper.getLastBackOff()); + } + + public void testMaximumBackOff() throws Exception { + ExponentialBackOffPolicy strategy = new ExponentialBackOffPolicy(); + strategy.setMaxInterval(50); + strategy.setSleeper(sleeper); + BackOffContext context = strategy.start(null); + strategy.backOff(context); + assertEquals(50, sleeper.getLastBackOff()); + } + + public void testMultiBackOff() throws Exception { + ExponentialBackOffPolicy strategy = new ExponentialBackOffPolicy(); + long seed = 40; + double multiplier = 1.2; + strategy.setInitialInterval(seed); + strategy.setMultiplier(multiplier); + strategy.setSleeper(sleeper); + BackOffContext context = strategy.start(null); + for (int x = 0; x < 5; x++) { + strategy.backOff(context); + assertEquals(seed, sleeper.getLastBackOff()); + seed *= multiplier; + } + } + +} diff --git a/src/test/java/org/springframework/retry/backoff/FixedBackOffPolicyTests.java b/src/test/java/org/springframework/retry/backoff/FixedBackOffPolicyTests.java new file mode 100644 index 0000000..c71dc2b --- /dev/null +++ b/src/test/java/org/springframework/retry/backoff/FixedBackOffPolicyTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.backoff; + +import junit.framework.TestCase; + +/** + * @author Rob Harrop + * @author Dave Syer + * @since 2.1 + */ +public class FixedBackOffPolicyTests extends TestCase { + + private DummySleeper sleeper = new DummySleeper(); + + public void testSetBackoffPeriodNegative() throws Exception { + FixedBackOffPolicy strategy = new FixedBackOffPolicy(); + strategy.setBackOffPeriod(-1000L); + strategy.setSleeper(sleeper); + strategy.backOff(null); + // We should see a zero backoff if we try to set it negative + assertEquals(1, sleeper.getBackOffs().length); + assertEquals(1, sleeper.getLastBackOff()); + } + + public void testSingleBackOff() throws Exception { + int backOffPeriod = 50; + FixedBackOffPolicy strategy = new FixedBackOffPolicy(); + strategy.setBackOffPeriod(backOffPeriod); + strategy.setSleeper(sleeper); + strategy.backOff(null); + assertEquals(1, sleeper.getBackOffs().length); + assertEquals(backOffPeriod, sleeper.getLastBackOff()); + } + + public void testManyBackOffCalls() throws Exception { + int backOffPeriod = 50; + FixedBackOffPolicy strategy = new FixedBackOffPolicy(); + strategy.setBackOffPeriod(backOffPeriod); + strategy.setSleeper(sleeper); + for (int x = 0; x < 10; x++) { + strategy.backOff(null); + assertEquals(backOffPeriod, sleeper.getLastBackOff()); + } + assertEquals(10, sleeper.getBackOffs().length); + } + +} diff --git a/src/test/java/org/springframework/retry/backoff/ObjectWaitSleeperTests.java b/src/test/java/org/springframework/retry/backoff/ObjectWaitSleeperTests.java new file mode 100644 index 0000000..edd7914 --- /dev/null +++ b/src/test/java/org/springframework/retry/backoff/ObjectWaitSleeperTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.backoff; + +import junit.framework.TestCase; + +/** + * @author Dave Syer + */ +public class ObjectWaitSleeperTests extends TestCase { + + public void testSingleBackOff() throws Exception { + long backOffPeriod = 50; + ObjectWaitSleeper strategy = new ObjectWaitSleeper(); + long before = System.currentTimeMillis(); + strategy.sleep(backOffPeriod); + long after = System.currentTimeMillis(); + assertEqualsApprox(backOffPeriod, after - before, 25); + } + + private void assertEqualsApprox(long desired, long actual, long variance) { + long lower = desired - variance; + long upper = desired + 2 * variance; + assertTrue("Expected value to be between '" + lower + "' and '" + upper + "' but was '" + actual + "'", + lower <= actual); + } + +} diff --git a/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java b/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java new file mode 100644 index 0000000..d629e0a --- /dev/null +++ b/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java @@ -0,0 +1,178 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.interceptor; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import junit.framework.TestCase; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.target.SingletonTargetSource; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.retry.policy.NeverRetryPolicy; +import org.springframework.retry.policy.SimpleRetryPolicy; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.ClassUtils; + +public class RetryOperationsInterceptorTests extends TestCase { + + private RetryOperationsInterceptor interceptor; + + private Service service; + + private ServiceImpl target; + + private static int count; + + private static int transactionCount; + + protected void setUp() throws Exception { + super.setUp(); + interceptor = new RetryOperationsInterceptor(); + target = new ServiceImpl(); + service = (Service) ProxyFactory.getProxy(Service.class, new SingletonTargetSource(target)); + count = 0; + transactionCount = 0; + } + + public void testDefaultInterceptorSunnyDay() throws Exception { + ((Advised) service).addAdvice(interceptor); + service.service(); + assertEquals(2, count); + } + + public void testInterceptorChainWithRetry() throws Exception { + ((Advised) service).addAdvice(interceptor); + final List list = new ArrayList(); + ((Advised) service).addAdvice(new MethodInterceptor() { + public Object invoke(MethodInvocation invocation) throws Throwable { + list.add("chain"); + return invocation.proceed(); + } + }); + RetryTemplate template = new RetryTemplate(); + template.setRetryPolicy(new SimpleRetryPolicy(2, Collections + ., Boolean> singletonMap(Exception.class, true))); + interceptor.setRetryOperations(template); + service.service(); + assertEquals(2, count); + assertEquals(2, list.size()); + } + + public void testRetryExceptionAfterTooManyAttempts() throws Exception { + ((Advised) service).addAdvice(interceptor); + RetryTemplate template = new RetryTemplate(); + template.setRetryPolicy(new NeverRetryPolicy()); + interceptor.setRetryOperations(template); + try { + service.service(); + fail("Expected Exception."); + } + catch (Exception e) { + assertTrue(e.getMessage().startsWith("Not enough calls")); + } + assertEquals(1, count); + } + + public void testOutsideTransaction() throws Exception { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(ClassUtils + .addResourcePathToPackagePath(getClass(), "retry-transaction-test.xml")); + Object object = context.getBean("bean"); + assertNotNull(object); + assertTrue(object instanceof Service); + Service bean = (Service) object; + bean.doTansactional(); + assertEquals(2, count); + // Expect 2 separate transactions... + assertEquals(2, transactionCount); + } + + public void testIllegalMethodInvocationType() throws Throwable { + try { + interceptor.invoke(new MethodInvocation() { + public Method getMethod() { + return null; + } + + public Object[] getArguments() { + return null; + } + + public AccessibleObject getStaticPart() { + return null; + } + + public Object getThis() { + return null; + } + + public Object proceed() throws Throwable { + return null; + } + }); + fail("IllegalStateException expected"); + } + catch (IllegalStateException e) { + assertTrue("Exception message should contain MethodInvocation: " + e.getMessage(), e.getMessage().indexOf( + "MethodInvocation") >= 0); + } + } + + public static interface Service { + void service() throws Exception; + + void doTansactional() throws Exception; + } + + public static class ServiceImpl implements Service { + + private boolean enteredTransaction = false; + + public void service() throws Exception { + count++; + if (count < 2) { + throw new Exception("Not enough calls: " + count); + } + } + + public void doTansactional() throws Exception { + if (TransactionSynchronizationManager.isActualTransactionActive() && !enteredTransaction) { + transactionCount++; + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + public void beforeCompletion() { + enteredTransaction = false; + } + }); + enteredTransaction = true; + } + count++; + if (count == 1) { + throw new RuntimeException("Rollback please"); + } + } + + } +} diff --git a/src/test/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptorTests.java b/src/test/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptorTests.java new file mode 100644 index 0000000..ec31f59 --- /dev/null +++ b/src/test/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptorTests.java @@ -0,0 +1,248 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.retry.interceptor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import junit.framework.TestCase; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.target.SingletonTargetSource; +import org.springframework.retry.ExhaustedRetryException; +import org.springframework.retry.policy.AlwaysRetryPolicy; +import org.springframework.retry.policy.NeverRetryPolicy; +import org.springframework.retry.policy.SimpleRetryPolicy; +import org.springframework.retry.support.RetryTemplate; + +/** + * @author Dave Syer + * + */ +public class StatefulRetryOperationsInterceptorTests extends TestCase { + + private StatefulRetryOperationsInterceptor interceptor; + + private RetryTemplate retryTemplate = new RetryTemplate(); + + private Service service; + + private Transformer transformer; + + private static int count; + + public void setUp() throws Exception { + interceptor = new StatefulRetryOperationsInterceptor(); + service = (Service) ProxyFactory.getProxy(Service.class, new SingletonTargetSource(new ServiceImpl())); + transformer = (Transformer) ProxyFactory.getProxy(Transformer.class, new SingletonTargetSource( + new TransformerImpl())); + count = 0; + } + + public void testDefaultInterceptorSunnyDay() throws Exception { + ((Advised) service).addAdvice(interceptor); + try { + service.service("foo"); + fail("Expected Exception."); + } + catch (Exception e) { + String message = e.getMessage(); + assertTrue("Wrong message: " + message, message.startsWith("Not enough calls")); + } + assertEquals(1, count); + } + + public void testDefaultTransformerInterceptorSunnyDay() throws Exception { + ((Advised) transformer).addAdvice(interceptor); + try { + transformer.transform("foo"); + fail("Expected Exception."); + } + catch (Exception e) { + String message = e.getMessage(); + assertTrue("Wrong message: " + message, message.startsWith("Not enough calls")); + } + assertEquals(1, count); + } + + public void testDefaultInterceptorAlwaysRetry() throws Exception { + retryTemplate.setRetryPolicy(new AlwaysRetryPolicy()); + interceptor.setRetryOperations(retryTemplate); + ((Advised) service).addAdvice(interceptor); + try { + service.service("foo"); + fail("Expected Exception."); + } + catch (Exception e) { + String message = e.getMessage(); + assertTrue("Wrong message: " + message, message.startsWith("Not enough calls")); + } + assertEquals(1, count); + } + + public void testInterceptorChainWithRetry() throws Exception { + ((Advised) service).addAdvice(interceptor); + final List list = new ArrayList(); + ((Advised) service).addAdvice(new MethodInterceptor() { + public Object invoke(MethodInvocation invocation) throws Throwable { + list.add("chain"); + return invocation.proceed(); + } + }); + interceptor.setRetryOperations(retryTemplate); + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(2, Collections + ., Boolean> singletonMap(Exception.class, true))); + try { + service.service("foo"); + fail("Expected Exception."); + } + catch (Exception e) { + String message = e.getMessage(); + assertTrue("Wrong message: " + message, message.startsWith("Not enough calls")); + } + assertEquals(1, count); + service.service("foo"); + assertEquals(2, count); + assertEquals(2, list.size()); + } + + public void testTransformerWithSuccessfulRetry() throws Exception { + ((Advised) transformer).addAdvice(interceptor); + interceptor.setRetryOperations(retryTemplate); + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(2, Collections + ., Boolean> singletonMap(Exception.class, true))); + try { + transformer.transform("foo"); + fail("Expected Exception."); + } + catch (Exception e) { + String message = e.getMessage(); + assertTrue("Wrong message: " + message, message.startsWith("Not enough calls")); + } + assertEquals(1, count); + Collection result = transformer.transform("foo"); + assertEquals(2, count); + assertEquals(1, result.size()); + } + + public void testRetryExceptionAfterTooManyAttemptsWithNoRecovery() throws Exception { + ((Advised) service).addAdvice(interceptor); + interceptor.setRetryOperations(retryTemplate); + retryTemplate.setRetryPolicy(new NeverRetryPolicy()); + try { + service.service("foo"); + fail("Expected Exception."); + } + catch (Exception e) { + String message = e.getMessage(); + assertTrue("Wrong message: " + message, message.startsWith("Not enough calls")); + } + assertEquals(1, count); + try { + service.service("foo"); + fail("Expected ExhaustedRetryException"); + } + catch (ExhaustedRetryException e) { + // expected + String message = e.getMessage(); + assertTrue("Wrong message: " + message, message.startsWith("Retry was exhausted but there was no recover")); + } + assertEquals(1, count); + } + + public void testRecoveryAfterTooManyAttempts() throws Exception { + ((Advised) service).addAdvice(interceptor); + interceptor.setRetryOperations(retryTemplate); + retryTemplate.setRetryPolicy(new NeverRetryPolicy()); + try { + service.service("foo"); + fail("Expected Exception."); + } + catch (Exception e) { + String message = e.getMessage(); + assertTrue("Wrong message: " + message, message.startsWith("Not enough calls")); + } + assertEquals(1, count); + interceptor.setRecoverer(new MethodInvocationRecoverer() { + public Object recover(Object[] data, Throwable cause) { + count++; + return null; + } + }); + service.service("foo"); + assertEquals(2, count); + } + + public void testTransformerRecoveryAfterTooManyAttempts() throws Exception { + ((Advised) transformer).addAdvice(interceptor); + interceptor.setRetryOperations(retryTemplate); + retryTemplate.setRetryPolicy(new NeverRetryPolicy()); + try { + transformer.transform("foo"); + fail("Expected Exception."); + } + catch (Exception e) { + String message = e.getMessage(); + assertTrue("Wrong message: " + message, message.startsWith("Not enough calls")); + } + assertEquals(1, count); + interceptor.setRecoverer(new MethodInvocationRecoverer>() { + public Collection recover(Object[] data, Throwable cause) { + count++; + return Collections.singleton((String) data[0]); + } + }); + Collection result = transformer.transform("foo"); + assertEquals(2, count); + assertEquals(1, result.size()); + } + + public static interface Service { + void service(String in) throws Exception; + } + + public static class ServiceImpl implements Service { + + public void service(String in) throws Exception { + count++; + if (count < 2) { + throw new Exception("Not enough calls: " + count); + } + } + + } + + public static interface Transformer { + Collection transform(String in) throws Exception; + } + + public static class TransformerImpl implements Transformer { + + public Collection transform(String in) throws Exception { + count++; + if (count < 2) { + throw new Exception("Not enough calls: " + count); + } + return Collections.singleton(in + ":" + count); + } + + } +} diff --git a/src/test/java/org/springframework/retry/listener/RetryListenerSupportTests.java b/src/test/java/org/springframework/retry/listener/RetryListenerSupportTests.java new file mode 100644 index 0000000..0b7c5f8 --- /dev/null +++ b/src/test/java/org/springframework/retry/listener/RetryListenerSupportTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.listener; + +import junit.framework.TestCase; + +public class RetryListenerSupportTests extends TestCase { + + public void testClose() { + RetryListenerSupport support = new RetryListenerSupport(); + try { + support.close(null, null, null); + } + catch (Exception e) { + fail("Unexpected exception"); + } + } + + public void testOnError() { + RetryListenerSupport support = new RetryListenerSupport(); + try { + support.onError(null, null, null); + } + catch (Exception e) { + fail("Unexpected exception"); + } + } + + public void testOpen() { + RetryListenerSupport support = new RetryListenerSupport(); + assertTrue(support.open(null, null)); + } + +} diff --git a/src/test/java/org/springframework/retry/listener/RetryListenerTests.java b/src/test/java/org/springframework/retry/listener/RetryListenerTests.java new file mode 100644 index 0000000..bc6d3f8 --- /dev/null +++ b/src/test/java/org/springframework/retry/listener/RetryListenerTests.java @@ -0,0 +1,162 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.listener; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryListener; +import org.springframework.retry.TerminatedRetryException; +import org.springframework.retry.policy.NeverRetryPolicy; +import org.springframework.retry.support.RetryTemplate; + +public class RetryListenerTests extends TestCase { + + RetryTemplate template = new RetryTemplate(); + + int count = 0; + + List list = new ArrayList(); + + public void testOpenInterceptors() throws Exception { + template.setListeners(new RetryListener[] { new RetryListenerSupport() { + public boolean open(RetryContext context, RetryCallback callback) { + count++; + list.add("1:" + count); + return true; + } + }, new RetryListenerSupport() { + public boolean open(RetryContext context, RetryCallback callback) { + count++; + list.add("2:" + count); + return true; + } + } }); + template.execute(new RetryCallback() { + public String doWithRetry(RetryContext context) throws Exception { + return null; + } + }); + assertEquals(2, count); + assertEquals(2, list.size()); + assertEquals("1:1", list.get(0)); + } + + public void testOpenCanVetoRetry() throws Exception { + template.registerListener(new RetryListenerSupport() { + public boolean open(RetryContext context, RetryCallback callback) { + list.add("1"); + return false; + } + }); + try { + template.execute(new RetryCallback() { + public String doWithRetry(RetryContext context) throws Exception { + count++; + return null; + } + }); + fail("Expected TerminatedRetryException"); + } + catch (TerminatedRetryException e) { + // expected + } + assertEquals(0, count); + assertEquals(1, list.size()); + assertEquals("1", list.get(0)); + } + + public void testCloseInterceptors() throws Exception { + template.setListeners(new RetryListener[] { new RetryListenerSupport() { + public void close(RetryContext context, RetryCallback callback, Throwable t) { + count++; + list.add("1:" + count); + } + }, new RetryListenerSupport() { + public void close(RetryContext context, RetryCallback callback, Throwable t) { + count++; + list.add("2:" + count); + } + } }); + template.execute(new RetryCallback() { + public String doWithRetry(RetryContext context) throws Exception { + return null; + } + }); + assertEquals(2, count); + assertEquals(2, list.size()); + // interceptors are called in reverse order on close... + assertEquals("2:1", list.get(0)); + } + + public void testOnError() throws Exception { + template.setRetryPolicy(new NeverRetryPolicy()); + template.setListeners(new RetryListener[] { new RetryListenerSupport() { + public void onError(RetryContext context, RetryCallback callback, Throwable throwable) { + list.add("1"); + } + }, new RetryListenerSupport() { + public void onError(RetryContext context, RetryCallback callback, Throwable throwable) { + list.add("2"); + } + } }); + try { + template.execute(new RetryCallback() { + public String doWithRetry(RetryContext context) throws Exception { + count++; + throw new IllegalStateException("foo"); + } + }); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + assertEquals("foo", e.getMessage()); + } + // never retry so callback is executed once + assertEquals(1, count); + assertEquals(2, list.size()); + // interceptors are called in reverse order on error... + assertEquals("2", list.get(0)); + + } + + public void testCloseInterceptorsAfterRetry() throws Exception { + template.registerListener(new RetryListenerSupport() { + public void close(RetryContext context, RetryCallback callback, Throwable t) { + list.add("" + count); + // The last attempt should have been successful: + assertNull(t); + } + }); + template.execute(new RetryCallback() { + public String doWithRetry(RetryContext context) throws Exception { + if (count++ < 1) + throw new RuntimeException("Retry!"); + return null; + } + }); + assertEquals(2, count); + // The close interceptor was only called once: + assertEquals(1, list.size()); + // We succeeded on the second try: + assertEquals("2", list.get(0)); + } +} diff --git a/src/test/java/org/springframework/retry/policy/AlwaysRetryPolicyTests.java b/src/test/java/org/springframework/retry/policy/AlwaysRetryPolicyTests.java new file mode 100644 index 0000000..a83118a --- /dev/null +++ b/src/test/java/org/springframework/retry/policy/AlwaysRetryPolicyTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.policy; + +import junit.framework.TestCase; + +import org.springframework.retry.RetryContext; + +public class AlwaysRetryPolicyTests extends TestCase { + + public void testSimpleOperations() throws Exception { + AlwaysRetryPolicy policy = new AlwaysRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, null); + assertTrue(policy.canRetry(context)); + policy.close(context); + assertTrue(policy.canRetry(context)); + } + + public void testRetryCount() throws Exception { + AlwaysRetryPolicy policy = new AlwaysRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + policy.registerThrowable(context, new RuntimeException("foo")); + assertEquals(1, context.getRetryCount()); + assertEquals("foo", context.getLastThrowable().getMessage()); + } + + public void testParent() throws Exception { + AlwaysRetryPolicy policy = new AlwaysRetryPolicy(); + RetryContext context = policy.open(null); + RetryContext child = policy.open(context); + assertNotSame(child, context); + assertSame(context, child.getParent()); + } +} diff --git a/src/test/java/org/springframework/retry/policy/CompositeRetryPolicyTests.java b/src/test/java/org/springframework/retry/policy/CompositeRetryPolicyTests.java new file mode 100644 index 0000000..1fbb6ac --- /dev/null +++ b/src/test/java/org/springframework/retry/policy/CompositeRetryPolicyTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.policy; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryPolicy; + +public class CompositeRetryPolicyTests extends TestCase { + + public void testEmptyPolicies() throws Exception { + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + assertTrue(policy.canRetry(context)); + } + + public void testTrivialPolicies() throws Exception { + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + policy.setPolicies(new RetryPolicy[] { new MockRetryPolicySupport(), new MockRetryPolicySupport() }); + RetryContext context = policy.open(null); + assertNotNull(context); + assertTrue(policy.canRetry(context)); + } + + public void testNonTrivialPolicies() throws Exception { + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + policy.setPolicies(new RetryPolicy[] { new MockRetryPolicySupport(), new MockRetryPolicySupport() { + public boolean canRetry(RetryContext context) { + return false; + } + } }); + RetryContext context = policy.open(null); + assertNotNull(context); + assertFalse(policy.canRetry(context)); + } + + public void testNonTrivialPoliciesWithThrowable() throws Exception { + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + policy.setPolicies(new RetryPolicy[] { new MockRetryPolicySupport(), new MockRetryPolicySupport() { + boolean errorRegistered = false; + + public boolean canRetry(RetryContext context) { + return !errorRegistered; + } + + public void registerThrowable(RetryContext context, Throwable throwable) { + errorRegistered = true; + } + } }); + RetryContext context = policy.open(null); + assertNotNull(context); + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, null); + assertFalse("Should be still able to retry", policy.canRetry(context)); + } + + public void testNonTrivialPoliciesClose() throws Exception { + final List list = new ArrayList(); + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + policy.setPolicies(new RetryPolicy[] { new MockRetryPolicySupport() { + public void close(RetryContext context) { + list.add("1"); + } + }, new MockRetryPolicySupport() { + public void close(RetryContext context) { + list.add("2"); + } + } }); + RetryContext context = policy.open(null); + assertNotNull(context); + policy.close(context); + assertEquals(2, list.size()); + } + + public void testExceptionOnPoliciesClose() throws Exception { + final List list = new ArrayList(); + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + policy.setPolicies(new RetryPolicy[] { new MockRetryPolicySupport() { + public void close(RetryContext context) { + list.add("1"); + throw new RuntimeException("Pah!"); + } + }, new MockRetryPolicySupport() { + public void close(RetryContext context) { + list.add("2"); + } + } }); + RetryContext context = policy.open(null); + assertNotNull(context); + try { + policy.close(context); + fail("Expected RuntimeException"); + } catch (RuntimeException e) { + assertEquals("Pah!", e.getMessage()); + } + assertEquals(2, list.size()); + } + + public void testRetryCount() throws Exception { + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + policy.setPolicies(new RetryPolicy[] { new MockRetryPolicySupport(), new MockRetryPolicySupport() }); + RetryContext context = policy.open(null); + assertNotNull(context); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + policy.registerThrowable(context, new RuntimeException("foo")); + assertEquals(1, context.getRetryCount()); + assertEquals("foo", context.getLastThrowable().getMessage()); + } + + public void testParent() throws Exception { + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + RetryContext context = policy.open(null); + RetryContext child = policy.open(context); + assertNotSame(child, context); + assertSame(context, child.getParent()); + } + +} diff --git a/src/test/java/org/springframework/retry/policy/ExceptionClassifierRetryPolicyTests.java b/src/test/java/org/springframework/retry/policy/ExceptionClassifierRetryPolicyTests.java new file mode 100644 index 0000000..bba9e19 --- /dev/null +++ b/src/test/java/org/springframework/retry/policy/ExceptionClassifierRetryPolicyTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.policy; + +import java.util.Collections; +import java.util.HashMap; +import junit.framework.TestCase; + +import org.springframework.classify.Classifier; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryPolicy; + +public class ExceptionClassifierRetryPolicyTests extends TestCase { + + private ExceptionClassifierRetryPolicy policy = new ExceptionClassifierRetryPolicy(); + + public void testDefaultPolicies() throws Exception { + RetryContext context = policy.open(null); + assertNotNull(context); + } + + public void testTrivialPolicies() throws Exception { + policy.setPolicyMap(Collections., RetryPolicy> singletonMap(Exception.class, + new MockRetryPolicySupport())); + RetryContext context = policy.open(null); + assertNotNull(context); + assertTrue(policy.canRetry(context)); + } + + public void testNullPolicies() throws Exception { + policy.setPolicyMap(new HashMap, RetryPolicy>()); + RetryContext context = policy.open(null); + assertNotNull(context); + } + + public void testNullContext() throws Exception { + policy.setPolicyMap(Collections., RetryPolicy> singletonMap(Exception.class, + new NeverRetryPolicy())); + + RetryContext context = policy.open(null); + assertNotNull(context); + + assertTrue(policy.canRetry(context)); + } + + public void testClassifierOperates() throws Exception { + + RetryContext context = policy.open(null); + assertNotNull(context); + + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, new IllegalArgumentException()); + assertFalse(policy.canRetry(context)); // NeverRetryPolicy is the + // default + + policy.setExceptionClassifier(new Classifier() { + public RetryPolicy classify(Throwable throwable) { + if (throwable != null) { + return new AlwaysRetryPolicy(); + } + return new NeverRetryPolicy(); + } + }); + + // The context saves the classifier, so changing it now has no effect + assertFalse(policy.canRetry(context)); + policy.registerThrowable(context, new IllegalArgumentException()); + assertFalse(policy.canRetry(context)); + + // But now the classifier will be active in the new context... + context = policy.open(null); + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, new IllegalArgumentException()); + assertTrue(policy.canRetry(context)); + + } + + int count = 0; + + public void testClose() throws Exception { + policy.setExceptionClassifier(new Classifier() { + public RetryPolicy classify(Throwable throwable) { + return new MockRetryPolicySupport() { + public void close(RetryContext context) { + count++; + } + }; + } + }); + RetryContext context = policy.open(null); + + // The mapped (child) policy hasn't been used yet, so if we close now + // we don't incur the possible expense of creating the child context. + policy.close(context); + assertEquals(0, count); // not classified yet + // This forces a child context to be created and the child policy is + // then closed + policy.registerThrowable(context, new IllegalStateException()); + policy.close(context); + assertEquals(1, count); // now classified + } + + public void testRetryCount() throws Exception { + ExceptionClassifierRetryPolicy policy = new ExceptionClassifierRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + policy.registerThrowable(context, new RuntimeException("foo")); + assertEquals(1, context.getRetryCount()); + assertEquals("foo", context.getLastThrowable().getMessage()); + } + + public void testParent() throws Exception { + ExceptionClassifierRetryPolicy policy = new ExceptionClassifierRetryPolicy(); + RetryContext context = policy.open(null); + RetryContext child = policy.open(context); + assertNotSame(child, context); + assertSame(context, child.getParent()); + } + +} diff --git a/src/test/java/org/springframework/retry/policy/FatalExceptionRetryPolicyTests.java b/src/test/java/org/springframework/retry/policy/FatalExceptionRetryPolicyTests.java new file mode 100644 index 0000000..2700d3c --- /dev/null +++ b/src/test/java/org/springframework/retry/policy/FatalExceptionRetryPolicyTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.policy; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import org.springframework.retry.RecoveryCallback; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.support.DefaultRetryState; +import org.springframework.retry.support.RetryTemplate; + +public class FatalExceptionRetryPolicyTests extends TestCase { + + public void testFatalExceptionWithoutState() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + callback.setExceptionToThrow(new IllegalArgumentException()); + + RetryTemplate retryTemplate = new RetryTemplate(); + + // Make sure certain exceptions are fatal... + Map, Boolean> map = new HashMap, Boolean>(); + map.put(IllegalArgumentException.class, false); + map.put(IllegalStateException.class, false); + + // ... and allow multiple attempts + SimpleRetryPolicy policy = new SimpleRetryPolicy(3, map); + retryTemplate.setRetryPolicy(policy); + RecoveryCallback recoveryCallback = new RecoveryCallback() { + public String recover(RetryContext context) throws Exception { + return "bar"; + } + }; + + Object result = null; + try { + result = retryTemplate.execute(callback, recoveryCallback); + } + catch (IllegalArgumentException e) { + // We should swallow the exception when recovery is possible + fail("Did not expect IllegalArgumentException"); + } + // Callback is called once: the recovery path should also be called + assertEquals(1, callback.attempts); + assertEquals("bar", result); + } + + public void testFatalExceptionWithState() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + callback.setExceptionToThrow(new IllegalArgumentException()); + + RetryTemplate retryTemplate = new RetryTemplate(); + + Map, Boolean> map = new HashMap, Boolean>(); + map.put(IllegalArgumentException.class, false); + map.put(IllegalStateException.class, false); + + SimpleRetryPolicy policy = new SimpleRetryPolicy(3, map); + retryTemplate.setRetryPolicy(policy); + + RecoveryCallback recoveryCallback = new RecoveryCallback() { + public String recover(RetryContext context) throws Exception { + return "bar"; + } + }; + + Object result = null; + try { + retryTemplate.execute(callback, recoveryCallback, new DefaultRetryState("foo")); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // If stateful we have to always rethrow. Clients who want special + // cases have to implement them in the callback + } + result = retryTemplate.execute(callback, recoveryCallback, new DefaultRetryState("foo")); + // Callback is called once: the recovery path should also be called + assertEquals(1, callback.attempts); + assertEquals("bar", result); + } + + private static class MockRetryCallback implements RetryCallback { + + private int attempts; + + private Exception exceptionToThrow = new Exception(); + + public String doWithRetry(RetryContext context) throws Exception { + this.attempts++; + // Just barf... + throw this.exceptionToThrow; + } + + public void setExceptionToThrow(Exception exceptionToThrow) { + this.exceptionToThrow = exceptionToThrow; + } + } + +} diff --git a/src/test/java/org/springframework/retry/policy/MapRetryContextCacheTests.java b/src/test/java/org/springframework/retry/policy/MapRetryContextCacheTests.java new file mode 100644 index 0000000..99548d3 --- /dev/null +++ b/src/test/java/org/springframework/retry/policy/MapRetryContextCacheTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.policy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.springframework.retry.context.RetryContextSupport; + +public class MapRetryContextCacheTests { + + MapRetryContextCache cache = new MapRetryContextCache(); + + @Test + public void testPut() { + RetryContextSupport context = new RetryContextSupport(null); + cache.put("foo", context); + assertEquals(context, cache.get("foo")); + } + + @Test(expected=RetryCacheCapacityExceededException.class) + public void testPutOverLimit() { + RetryContextSupport context = new RetryContextSupport(null); + cache.setCapacity(1); + cache.put("foo", context); + cache.put("foo", context); + } + + @Test + public void testRemove() { + assertFalse(cache.containsKey("foo")); + RetryContextSupport context = new RetryContextSupport(null); + cache.put("foo", context); + assertTrue(cache.containsKey("foo")); + cache.remove("foo"); + assertFalse(cache.containsKey("foo")); + } + +} diff --git a/src/test/java/org/springframework/retry/policy/MockRetryPolicySupport.java b/src/test/java/org/springframework/retry/policy/MockRetryPolicySupport.java new file mode 100644 index 0000000..b0e3ca6 --- /dev/null +++ b/src/test/java/org/springframework/retry/policy/MockRetryPolicySupport.java @@ -0,0 +1,21 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.policy; + +public class MockRetryPolicySupport extends AlwaysRetryPolicy { + +} diff --git a/src/test/java/org/springframework/retry/policy/NeverRetryPolicyTests.java b/src/test/java/org/springframework/retry/policy/NeverRetryPolicyTests.java new file mode 100644 index 0000000..a7ca895 --- /dev/null +++ b/src/test/java/org/springframework/retry/policy/NeverRetryPolicyTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.policy; + +import junit.framework.TestCase; + +import org.springframework.retry.RetryContext; + +public class NeverRetryPolicyTests extends TestCase { + + public void testSimpleOperations() throws Exception { + NeverRetryPolicy policy = new NeverRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + // We can retry until the first exception is registered... + assertTrue(policy.canRetry(context)); + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, null); + assertFalse(policy.canRetry(context)); + policy.close(context); + assertFalse(policy.canRetry(context)); + } + + public void testRetryCount() throws Exception { + NeverRetryPolicy policy = new NeverRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + policy.registerThrowable(context, new RuntimeException("foo")); + assertEquals(1, context.getRetryCount()); + assertEquals("foo", context.getLastThrowable().getMessage()); + } + + public void testParent() throws Exception { + NeverRetryPolicy policy = new NeverRetryPolicy(); + RetryContext context = policy.open(null); + RetryContext child = policy.open(context); + assertNotSame(child, context); + assertSame(context, child.getParent()); + } + +} diff --git a/src/test/java/org/springframework/retry/policy/SimpleRetryPolicyTests.java b/src/test/java/org/springframework/retry/policy/SimpleRetryPolicyTests.java new file mode 100644 index 0000000..e48cd94 --- /dev/null +++ b/src/test/java/org/springframework/retry/policy/SimpleRetryPolicyTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.policy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.springframework.retry.RetryContext; + +public class SimpleRetryPolicyTests { + + @Test + public void testCanRetryIfNoException() throws Exception { + SimpleRetryPolicy policy = new SimpleRetryPolicy(); + RetryContext context = policy.open(null); + assertTrue(policy.canRetry(context)); + } + + @Test + public void testEmptyExceptionsNeverRetry() throws Exception { + + // We can't retry any exceptions... + SimpleRetryPolicy policy = new SimpleRetryPolicy(3, Collections + ., Boolean> emptyMap()); + RetryContext context = policy.open(null); + + // ...so we can't retry this one... + policy.registerThrowable(context, new IllegalStateException()); + assertFalse(policy.canRetry(context)); + } + + @Test + public void testRetryLimitInitialState() throws Exception { + SimpleRetryPolicy policy = new SimpleRetryPolicy(); + RetryContext context = policy.open(null); + assertTrue(policy.canRetry(context)); + policy.setMaxAttempts(0); + context = policy.open(null); + assertFalse(policy.canRetry(context)); + } + + @Test + public void testRetryLimitSubsequentState() throws Exception { + SimpleRetryPolicy policy = new SimpleRetryPolicy(); + RetryContext context = policy.open(null); + policy.setMaxAttempts(2); + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, new Exception()); + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, new Exception()); + assertFalse(policy.canRetry(context)); + } + + @Test + public void testRetryCount() throws Exception { + SimpleRetryPolicy policy = new SimpleRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + policy.registerThrowable(context, new RuntimeException("foo")); + assertEquals(1, context.getRetryCount()); + assertEquals("foo", context.getLastThrowable().getMessage()); + } + + @Test + public void testFatalOverridesRetryable() throws Exception { + Map, Boolean> map = new HashMap, Boolean>(); + map.put(Exception.class, false); + map.put(RuntimeException.class, true); + SimpleRetryPolicy policy = new SimpleRetryPolicy(3, map); + RetryContext context = policy.open(null); + assertNotNull(context); + policy.registerThrowable(context, new RuntimeException("foo")); + assertTrue(policy.canRetry(context)); + } + + @Test + public void testParent() throws Exception { + SimpleRetryPolicy policy = new SimpleRetryPolicy(); + RetryContext context = policy.open(null); + RetryContext child = policy.open(context); + assertNotSame(child, context); + assertSame(context, child.getParent()); + } + +} diff --git a/src/test/java/org/springframework/retry/policy/SoftReferenceMapRetryContextCacheTests.java b/src/test/java/org/springframework/retry/policy/SoftReferenceMapRetryContextCacheTests.java new file mode 100644 index 0000000..8d85c8b --- /dev/null +++ b/src/test/java/org/springframework/retry/policy/SoftReferenceMapRetryContextCacheTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.policy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.springframework.retry.context.RetryContextSupport; + +public class SoftReferenceMapRetryContextCacheTests { + + SoftReferenceMapRetryContextCache cache = new SoftReferenceMapRetryContextCache(); + + @Test + public void testPut() { + RetryContextSupport context = new RetryContextSupport(null); + cache.put("foo", context); + assertEquals(context, cache.get("foo")); + } + + @Test(expected=RetryCacheCapacityExceededException.class) + public void testPutOverLimit() { + RetryContextSupport context = new RetryContextSupport(null); + cache.setCapacity(1); + cache.put("foo", context); + cache.put("foo", context); + } + + @Test + public void testRemove() { + assertFalse(cache.containsKey("foo")); + RetryContextSupport context = new RetryContextSupport(null); + cache.put("foo", context); + assertTrue(cache.containsKey("foo")); + cache.remove("foo"); + assertFalse(cache.containsKey("foo")); + } + +} diff --git a/src/test/java/org/springframework/retry/policy/StatefulRetryIntegrationTests.java b/src/test/java/org/springframework/retry/policy/StatefulRetryIntegrationTests.java new file mode 100644 index 0000000..9113f92 --- /dev/null +++ b/src/test/java/org/springframework/retry/policy/StatefulRetryIntegrationTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.retry.policy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Collections; + +import org.junit.Test; +import org.springframework.retry.ExhaustedRetryException; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryState; +import org.springframework.retry.support.DefaultRetryState; +import org.springframework.retry.support.RetryTemplate; + +/** + * @author Dave Syer + * + */ +public class StatefulRetryIntegrationTests { + + @Test + public void testExternalRetryWithFailAndNoRetry() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + + RetryState retryState = new DefaultRetryState("foo"); + + RetryTemplate retryTemplate = new RetryTemplate(); + MapRetryContextCache cache = new MapRetryContextCache(); + retryTemplate.setRetryContextCache(cache); + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(1, Collections + ., Boolean> singletonMap(Exception.class, true))); + + assertFalse(cache.containsKey("foo")); + + try { + retryTemplate.execute(callback, retryState); + // The first failed attempt we expect to retry... + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals(null, e.getMessage()); + } + + assertTrue(cache.containsKey("foo")); + + try { + retryTemplate.execute(callback, retryState); + // We don't get a second attempt... + fail("Expected ExhaustedRetryException"); + } + catch (ExhaustedRetryException e) { + // This is now the "exhausted" message: + assertNotNull(e.getMessage()); + } + + assertFalse(cache.containsKey("foo")); + + // Callback is called once: the recovery path should be called in + // handleRetryExhausted (so not in this test)... + assertEquals(1, callback.attempts); + } + + @Test + public void testExternalRetryWithSuccessOnRetry() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + + RetryState retryState = new DefaultRetryState("foo"); + + RetryTemplate retryTemplate = new RetryTemplate(); + MapRetryContextCache cache = new MapRetryContextCache(); + retryTemplate.setRetryContextCache(cache); + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(2, Collections + ., Boolean> singletonMap(Exception.class, true))); + + assertFalse(cache.containsKey("foo")); + + Object result = "start_foo"; + try { + result = retryTemplate.execute(callback, retryState); + // The first failed attempt we expect to retry... + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertNull(e.getMessage()); + } + + assertTrue(cache.containsKey("foo")); + + result = retryTemplate.execute(callback, retryState); + + assertFalse(cache.containsKey("foo")); + + assertEquals(2, callback.attempts); + assertEquals("bar", result); + } + + /** + * @author Dave Syer + * + */ + private static final class MockRetryCallback implements RetryCallback { + int attempts = 0; + + public String doWithRetry(RetryContext context) throws Exception { + attempts++; + if (attempts < 2) { + throw new RuntimeException(); + } + return "bar"; + } + } + +} diff --git a/src/test/java/org/springframework/retry/policy/TimeoutRetryPolicyTests.java b/src/test/java/org/springframework/retry/policy/TimeoutRetryPolicyTests.java new file mode 100644 index 0000000..c378dac --- /dev/null +++ b/src/test/java/org/springframework/retry/policy/TimeoutRetryPolicyTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.policy; + +import junit.framework.TestCase; + +import org.springframework.retry.RetryContext; + +public class TimeoutRetryPolicyTests extends TestCase { + + public void testTimeoutPreventsRetry() throws Exception { + TimeoutRetryPolicy policy = new TimeoutRetryPolicy(); + policy.setTimeout(100); + RetryContext context = policy.open(null); + policy.registerThrowable(context, new Exception()); + assertTrue(policy.canRetry(context)); + Thread.sleep(200); + assertFalse(policy.canRetry(context)); + policy.close(context); + } + + public void testRetryCount() throws Exception { + TimeoutRetryPolicy policy = new TimeoutRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + policy.registerThrowable(context, new RuntimeException("foo")); + assertEquals(1, context.getRetryCount()); + assertEquals("foo", context.getLastThrowable().getMessage()); + } + + public void testParent() throws Exception { + TimeoutRetryPolicy policy = new TimeoutRetryPolicy(); + RetryContext context = policy.open(null); + RetryContext child = policy.open(context); + assertNotSame(child, context); + assertSame(context, child.getParent()); + } + +} diff --git a/src/test/java/org/springframework/retry/support/DefaultRetryStateTests.java b/src/test/java/org/springframework/retry/support/DefaultRetryStateTests.java new file mode 100644 index 0000000..99cb613 --- /dev/null +++ b/src/test/java/org/springframework/retry/support/DefaultRetryStateTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.retry.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.springframework.classify.Classifier; + +/** + * @author Dave Syer + * + */ +public class DefaultRetryStateTests { + + /** + * Test method for + * {@link org.springframework.retry.support.DefaultRetryState#DefaultRetryState(java.lang.Object, boolean, org.springframework.classify.Classifier)}. + */ + @Test + public void testDefaultRetryStateObjectBooleanClassifierOfQsuperThrowableBoolean() { + DefaultRetryState state = new DefaultRetryState("foo", true, new Classifier() { + public Boolean classify(Throwable classifiable) { + return false; + } + }); + assertEquals("foo", state.getKey()); + assertTrue(state.isForceRefresh()); + assertFalse(state.rollbackFor(null)); + } + + /** + * Test method for + * {@link org.springframework.retry.support.DefaultRetryState#DefaultRetryState(java.lang.Object, org.springframework.classify.Classifier)}. + */ + @Test + public void testDefaultRetryStateObjectClassifierOfQsuperThrowableBoolean() { + DefaultRetryState state = new DefaultRetryState("foo", new Classifier() { + public Boolean classify(Throwable classifiable) { + return false; + } + }); + assertEquals("foo", state.getKey()); + assertFalse(state.isForceRefresh()); + assertFalse(state.rollbackFor(null)); + } + + /** + * Test method for + * {@link org.springframework.retry.support.DefaultRetryState#DefaultRetryState(java.lang.Object, boolean)}. + */ + @Test + public void testDefaultRetryStateObjectBoolean() { + DefaultRetryState state = new DefaultRetryState("foo", true); + assertEquals("foo", state.getKey()); + assertTrue(state.isForceRefresh()); + assertTrue(state.rollbackFor(null)); + } + + /** + * Test method for + * {@link org.springframework.retry.support.DefaultRetryState#DefaultRetryState(java.lang.Object)}. + */ + @Test + public void testDefaultRetryStateObject() { + DefaultRetryState state = new DefaultRetryState("foo"); + assertEquals("foo", state.getKey()); + assertFalse(state.isForceRefresh()); + assertTrue(state.rollbackFor(null)); + } + +} diff --git a/src/test/java/org/springframework/retry/support/RetrySynchronizationManagerTests.java b/src/test/java/org/springframework/retry/support/RetrySynchronizationManagerTests.java new file mode 100644 index 0000000..638004a --- /dev/null +++ b/src/test/java/org/springframework/retry/support/RetrySynchronizationManagerTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.support; + +import junit.framework.TestCase; + +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.context.RetryContextSupport; + +/** + * @author Dave Syer + */ +public class RetrySynchronizationManagerTests extends TestCase { + + RetryTemplate template = new RetryTemplate(); + + protected void setUp() throws Exception { + super.setUp(); + RetrySynchronizationManagerTests.clearAll(); + RetryContext status = RetrySynchronizationManager.getContext(); + assertNull(status); + } + + public void testStatusIsStoredByTemplate() throws Exception { + + RetryContext status = RetrySynchronizationManager.getContext(); + assertNull(status); + + template.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Exception { + RetryContext global = RetrySynchronizationManager.getContext(); + assertNotNull(status); + assertEquals(global, status); + return null; + } + }); + + status = RetrySynchronizationManager.getContext(); + assertNull(status); + } + + public void testStatusRegistration() throws Exception { + RetryContext status = new RetryContextSupport(null); + RetryContext value = RetrySynchronizationManager.register(status); + assertNull(value); + value = RetrySynchronizationManager.register(status); + assertEquals(status, value); + } + + public void testClear() throws Exception { + RetryContext status = new RetryContextSupport(null); + RetryContext value = RetrySynchronizationManager.register(status); + assertNull(value); + RetrySynchronizationManager.clear(); + value = RetrySynchronizationManager.register(status); + assertNull(value); + } + + public void testParent() throws Exception { + RetryContext parent = new RetryContextSupport(null); + RetryContext child = new RetryContextSupport(parent); + assertSame(parent, child.getParent()); + } + + /** + * Clear all contexts starting with the current one and continuing until + * {@link RetrySynchronizationManager#clear()} returns null. + */ + public static RetryContext clearAll() { + RetryContext result = null; + RetryContext context = RetrySynchronizationManager.clear(); + while (context != null) { + result = context; + context = RetrySynchronizationManager.clear(); + } + return result; + } +} diff --git a/src/test/java/org/springframework/retry/support/RetryTemplateTests.java b/src/test/java/org/springframework/retry/support/RetryTemplateTests.java new file mode 100644 index 0000000..5d691b0 --- /dev/null +++ b/src/test/java/org/springframework/retry/support/RetryTemplateTests.java @@ -0,0 +1,353 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.support; + +import static org.easymock.EasyMock.createStrictMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.isA; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import java.util.Collections; + +import org.junit.Test; +import org.springframework.classify.BinaryExceptionClassifier; +import org.springframework.retry.ExhaustedRetryException; +import org.springframework.retry.RecoveryCallback; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.backoff.BackOffContext; +import org.springframework.retry.backoff.BackOffInterruptedException; +import org.springframework.retry.backoff.BackOffPolicy; +import org.springframework.retry.backoff.StatelessBackOffPolicy; +import org.springframework.retry.policy.NeverRetryPolicy; +import org.springframework.retry.policy.SimpleRetryPolicy; + +/** + * @author Rob Harrop + * @author Dave Syer + */ +public class RetryTemplateTests { + + RetryContext context; + + int count = 0; + + @Test + public void testSuccessfulRetry() throws Exception { + for (int x = 1; x <= 10; x++) { + MockRetryCallback callback = new MockRetryCallback(); + callback.setAttemptsBeforeSuccess(x); + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(x, Collections + ., Boolean> singletonMap(Exception.class, true))); + retryTemplate.execute(callback); + assertEquals(x, callback.attempts); + } + } + + @Test + public void testSuccessfulRecovery() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + callback.setAttemptsBeforeSuccess(3); + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(2, Collections + ., Boolean> singletonMap(Exception.class, true))); + final Object value = new Object(); + Object result = retryTemplate.execute(callback, new RecoveryCallback() { + public Object recover(RetryContext context) throws Exception { + return value; + } + }); + assertEquals(2, callback.attempts); + assertEquals(value, result); + } + + @Test + public void testAlwaysTryAtLeastOnce() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new NeverRetryPolicy()); + retryTemplate.execute(callback); + assertEquals(1, callback.attempts); + } + + @Test + public void testNoSuccessRetry() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + // Something that won't be thrown by JUnit... + callback.setExceptionToThrow(new IllegalArgumentException()); + callback.setAttemptsBeforeSuccess(Integer.MAX_VALUE); + RetryTemplate retryTemplate = new RetryTemplate(); + int retryAttempts = 2; + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(retryAttempts, Collections + ., Boolean> singletonMap(Exception.class, true))); + try { + retryTemplate.execute(callback); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + assertNotNull(e); + assertEquals(retryAttempts, callback.attempts); + return; + } + fail("Expected IllegalArgumentException"); + } + + @Test + public void testDefaultConfigWithExceptionSubclass() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + int attempts = 3; + callback.setAttemptsBeforeSuccess(attempts); + callback.setExceptionToThrow(new IllegalArgumentException()); + + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(attempts, Collections + ., Boolean> singletonMap(Exception.class, true))); + retryTemplate.execute(callback); + assertEquals(attempts, callback.attempts); + } + + @Test + public void testRollbackClassifierOverridesRetryPolicy() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + int attempts = 3; + callback.setAttemptsBeforeSuccess(attempts); + callback.setExceptionToThrow(new IllegalArgumentException()); + + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(attempts, Collections + ., Boolean> singletonMap(Exception.class, true))); + BinaryExceptionClassifier classifier = new BinaryExceptionClassifier(Collections + .> singleton(IllegalArgumentException.class), false); + retryTemplate.execute(callback, new DefaultRetryState("foo", classifier)); + assertEquals(attempts, callback.attempts); + } + + @Test + public void testSetExceptions() throws Exception { + RetryTemplate template = new RetryTemplate(); + SimpleRetryPolicy policy = new SimpleRetryPolicy(3, Collections + ., Boolean> singletonMap(RuntimeException.class, true)); + template.setRetryPolicy(policy); + + int attempts = 3; + + MockRetryCallback callback = new MockRetryCallback(); + callback.setAttemptsBeforeSuccess(attempts); + + try { + template.execute(callback); + } + catch (Exception e) { + assertNotNull(e); + assertEquals(1, callback.attempts); + } + callback.setExceptionToThrow(new RuntimeException()); + + template.execute(callback); + assertEquals(attempts, callback.attempts); + } + + @Test + public void testBackOffInvoked() throws Exception { + for (int x = 1; x <= 10; x++) { + MockRetryCallback callback = new MockRetryCallback(); + MockBackOffStrategy backOff = new MockBackOffStrategy(); + callback.setAttemptsBeforeSuccess(x); + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setBackOffPolicy(backOff); + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(x, Collections + ., Boolean> singletonMap(Exception.class, true))); + retryTemplate.execute(callback); + assertEquals(x, callback.attempts); + assertEquals(1, backOff.startCalls); + assertEquals(x - 1, backOff.backOffCalls); + } + } + + @Test + public void testEarlyTermination() throws Exception { + try { + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Exception { + status.setExhaustedOnly(); + throw new IllegalStateException("Retry this operation"); + } + }); + fail("Expected ExhaustedRetryException"); + } + catch (ExhaustedRetryException ex) { + // Expected for internal retry policy (external would recover + // gracefully) + assertEquals("Retry this operation", ex.getCause().getMessage()); + } + } + + @Test + public void testNestedContexts() throws Exception { + RetryTemplate outer = new RetryTemplate(); + final RetryTemplate inner = new RetryTemplate(); + outer.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Exception { + context = status; + count++; + Object result = inner.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Exception { + count++; + assertNotNull(context); + assertNotSame(status, context); + assertSame(context, status.getParent()); + assertSame("The context should be the child", status, RetrySynchronizationManager.getContext()); + return null; + } + }); + assertSame("The context should be restored", status, RetrySynchronizationManager.getContext()); + return result; + } + }); + assertEquals(2, count); + } + + @Test + public void testRethrowError() throws Exception { + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new NeverRetryPolicy()); + try { + retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext context) throws Exception { + throw new Error("Realllly bad!"); + } + }); + fail("Expected Error"); + } + catch (Error e) { + assertEquals("Realllly bad!", e.getMessage()); + } + } + + @Test + public void testBackOffInterrupted() throws Exception { + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setBackOffPolicy(new StatelessBackOffPolicy() { + protected void doBackOff() throws BackOffInterruptedException { + throw new BackOffInterruptedException("foo"); + } + }); + try { + retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext context) throws Exception { + throw new RuntimeException("Bad!"); + } + }); + fail("Expected RuntimeException"); + } + catch (BackOffInterruptedException e) { + assertEquals("foo", e.getMessage()); + } + } + + /** + * {@link BackOffPolicy} should apply also for exceptions that are + * re-thrown. + */ + @Test + public void testNoBackOffForRethrownException() throws Exception { + + RetryTemplate tested = new RetryTemplate(); + tested.setRetryPolicy(new SimpleRetryPolicy(1, Collections., Boolean> singletonMap( + Exception.class, true))); + + BackOffPolicy bop = createStrictMock(BackOffPolicy.class); + BackOffContext backOffContext = new BackOffContext() { + }; + tested.setBackOffPolicy(bop); + + expect(bop.start(isA(RetryContext.class))).andReturn(backOffContext); + replay(bop); + + try { + tested.execute(new RetryCallback() { + + public Object doWithRetry(RetryContext context) throws Exception { + throw new Exception("maybe next time!"); + } + + }, null, new DefaultRetryState(tested) { + + @Override + public boolean rollbackFor(Throwable exception) { + return true; + } + + }); + fail(); + } + catch (Exception expected) { + assertEquals("maybe next time!", expected.getMessage()); + } + + verify(bop); + } + + private static class MockRetryCallback implements RetryCallback { + + private int attempts; + + private int attemptsBeforeSuccess; + + private Exception exceptionToThrow = new Exception(); + + public Object doWithRetry(RetryContext status) throws Exception { + this.attempts++; + if (attempts < attemptsBeforeSuccess) { + throw this.exceptionToThrow; + } + return null; + } + + public void setAttemptsBeforeSuccess(int attemptsBeforeSuccess) { + this.attemptsBeforeSuccess = attemptsBeforeSuccess; + } + + public void setExceptionToThrow(Exception exceptionToThrow) { + this.exceptionToThrow = exceptionToThrow; + } + } + + private static class MockBackOffStrategy implements BackOffPolicy { + + public int backOffCalls; + + public int startCalls; + + public BackOffContext start(RetryContext status) { + startCalls++; + return null; + } + + public void backOff(BackOffContext backOffContext) throws BackOffInterruptedException { + backOffCalls++; + } + } +} diff --git a/src/test/java/org/springframework/retry/support/StatefulRecoveryRetryTests.java b/src/test/java/org/springframework/retry/support/StatefulRecoveryRetryTests.java new file mode 100644 index 0000000..919bef9 --- /dev/null +++ b/src/test/java/org/springframework/retry/support/StatefulRecoveryRetryTests.java @@ -0,0 +1,333 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; +import org.springframework.classify.BinaryExceptionClassifier; +import org.springframework.dao.DataAccessException; +import org.springframework.repeat.RepeatContext; +import org.springframework.repeat.context.RepeatContextSupport; +import org.springframework.repeat.support.RepeatSynchronizationManager; +import org.springframework.retry.ExhaustedRetryException; +import org.springframework.retry.RecoveryCallback; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryException; +import org.springframework.retry.RetryPolicy; +import org.springframework.retry.RetryState; +import org.springframework.retry.policy.MapRetryContextCache; +import org.springframework.retry.policy.NeverRetryPolicy; +import org.springframework.retry.policy.SimpleRetryPolicy; + +public class StatefulRecoveryRetryTests { + + private RetryTemplate retryTemplate = new RetryTemplate(); + + private int count = 0; + + private List list = new ArrayList(); + + @Test + public void testOpenSunnyDay() throws Exception { + RetryContext context = retryTemplate.open(new NeverRetryPolicy(), new DefaultRetryState("foo")); + assertNotNull(context); + // we haven't called the processor yet... + assertEquals(0, count); + } + + @Test + public void testRegisterThrowable() { + NeverRetryPolicy retryPolicy = new NeverRetryPolicy(); + RetryState state = new DefaultRetryState("foo"); + RetryContext context = retryTemplate.open(retryPolicy, state); + assertNotNull(context); + retryTemplate.registerThrowable(retryPolicy, state, context, new Exception()); + assertFalse(retryPolicy.canRetry(context)); + } + + @Test + public void testClose() throws Exception { + NeverRetryPolicy retryPolicy = new NeverRetryPolicy(); + RetryState state = new DefaultRetryState("foo"); + RetryContext context = retryTemplate.open(retryPolicy, state); + assertNotNull(context); + retryTemplate.registerThrowable(retryPolicy, state, context, new Exception()); + assertFalse(retryPolicy.canRetry(context)); + retryTemplate.close(retryPolicy, context, state, true); + // still can't retry, even if policy is closed + // (not that this would happen in practice)... + assertFalse(retryPolicy.canRetry(context)); + } + + @Test + public void testRecoverWithParent() throws Exception { + RepeatContext parent = new RepeatContextSupport(null); + RepeatSynchronizationManager.register(new RepeatContextSupport(parent)); + testRecover(); + assertFalse(parent.isCompleteOnly()); + RepeatSynchronizationManager.clear(); + } + + @Test + public void testRecover() throws Exception { + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(1, Collections + ., Boolean> singletonMap(Exception.class, true))); + final String input = "foo"; + RetryState state = new DefaultRetryState(input); + RetryCallback callback = new RetryCallback() { + public String doWithRetry(RetryContext context) throws Exception { + throw new RuntimeException("Barf!"); + } + }; + RecoveryCallback recoveryCallback = new RecoveryCallback() { + public String recover(RetryContext context) { + count++; + list.add(input); + return input; + } + }; + Object result = null; + try { + result = retryTemplate.execute(callback, recoveryCallback, state); + fail("Expected exception on first try"); + } + catch (Exception e) { + // expected... + } + // On the second retry, the recovery path is taken... + result = retryTemplate.execute(callback, recoveryCallback, state); + assertEquals(input, result); // default result is the item + assertEquals(1, count); + assertEquals(input, list.get(0)); + } + + @Test + public void testSwitchToStatelessForNoRollback() throws Exception { + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(1, Collections + ., Boolean> singletonMap(Exception.class, true))); + // Roll back for these: + BinaryExceptionClassifier classifier = new BinaryExceptionClassifier(Collections + .> singleton(DataAccessException.class)); + // ...but not these: + assertFalse(classifier.classify(new RuntimeException())); + final String input = "foo"; + RetryState state = new DefaultRetryState(input, classifier); + RetryCallback callback = new RetryCallback() { + public String doWithRetry(RetryContext context) throws Exception { + throw new RuntimeException("Barf!"); + } + }; + RecoveryCallback recoveryCallback = new RecoveryCallback() { + public String recover(RetryContext context) { + count++; + list.add(input); + return input; + } + }; + Object result = null; + // On the second retry, the recovery path is taken... + result = retryTemplate.execute(callback, recoveryCallback, state); + assertEquals(input, result); // default result is the item + assertEquals(1, count); + assertEquals(input, list.get(0)); + } + + @Test + public void testExhaustedClearsHistoryAfterLastAttempt() throws Exception { + RetryPolicy retryPolicy = new SimpleRetryPolicy(1, Collections + ., Boolean> singletonMap(Exception.class, true)); + retryTemplate.setRetryPolicy(retryPolicy); + + final String input = "foo"; + RetryState state = new DefaultRetryState(input); + RetryCallback callback = new RetryCallback() { + public String doWithRetry(RetryContext context) throws Exception { + throw new RuntimeException("Barf!"); + } + }; + + try { + retryTemplate.execute(callback, state); + fail("Expected ExhaustedRetryException"); + } + catch (RuntimeException e) { + assertEquals("Barf!", e.getMessage()); + } + + try { + retryTemplate.execute(callback, state); + fail("Expected ExhaustedRetryException"); + } + catch (ExhaustedRetryException e) { + // expected + } + + RetryContext context = retryTemplate.open(retryPolicy, state); + // True after exhausted - the history is reset... + assertTrue(retryPolicy.canRetry(context)); + } + + @Test + public void testKeyGeneratorNotConsistentAfterFailure() throws Throwable { + + RetryPolicy retryPolicy = new SimpleRetryPolicy(3, Collections + ., Boolean> singletonMap(Exception.class, true)); + retryTemplate.setRetryPolicy(retryPolicy); + final StringHolder item = new StringHolder("bar"); + RetryState state = new DefaultRetryState(item); + + RetryCallback callback = new RetryCallback() { + public StringHolder doWithRetry(RetryContext context) throws Exception { + // This simulates what happens if someone uses a primary key + // for hashCode and equals and then relies on default key + // generator + ((StringHolder) item).string = ((StringHolder) item).string + (count++); + throw new RuntimeException("Barf!"); + } + }; + + try { + retryTemplate.execute(callback, state); + fail("Expected RuntimeException"); + } + catch (RuntimeException ex) { + String message = ex.getMessage(); + assertEquals("Barf!", message); + } + // Only fails second attempt because the algorithm to detect + // inconsistent has codes relies on the cache having been used for this + // item already... + try { + retryTemplate.execute(callback, state); + fail("Expected RetryException"); + } + catch (RetryException ex) { + String message = ex.getMessage(); + assertTrue("Message doesn't contain 'inconsistent': " + message, message.contains("inconsistent")); + } + + RetryContext context = retryTemplate.open(retryPolicy, state); + // True after exhausted - the history is reset... + assertEquals(0, context.getRetryCount()); + + } + + @Test + public void testCacheCapacity() throws Exception { + + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(1, Collections + ., Boolean> singletonMap(Exception.class, true))); + retryTemplate.setRetryContextCache(new MapRetryContextCache(1)); + + RetryCallback callback = new RetryCallback() { + public Object doWithRetry(RetryContext context) throws Exception { + count++; + throw new RuntimeException("Barf!"); + } + }; + + try { + retryTemplate.execute(callback, new DefaultRetryState("foo")); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Barf!", e.getMessage()); + } + + try { + retryTemplate.execute(callback, new DefaultRetryState("bar")); + fail("Expected RetryException"); + } + catch (RetryException e) { + String message = e.getMessage(); + assertTrue("Message does not contain 'capacity': " + message, message.indexOf("capacity") >= 0); + } + } + + @Test + public void testCacheCapacityNotReachedIfRecovered() throws Exception { + + SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(1, Collections + ., Boolean> singletonMap(Exception.class, true)); + retryTemplate.setRetryPolicy(retryPolicy); + retryTemplate.setRetryContextCache(new MapRetryContextCache(2)); + final StringHolder item = new StringHolder("foo"); + RetryState state = new DefaultRetryState(item); + + RetryCallback callback = new RetryCallback() { + public Object doWithRetry(RetryContext context) throws Exception { + count++; + throw new RuntimeException("Barf!"); + } + }; + RecoveryCallback recoveryCallback = new RecoveryCallback() { + public Object recover(RetryContext context) throws Exception { + return null; + } + }; + + try { + retryTemplate.execute(callback, recoveryCallback, state); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Barf!", e.getMessage()); + } + retryTemplate.execute(callback, recoveryCallback, state); + + RetryContext context = retryTemplate.open(retryPolicy, state); + // True after exhausted - the history is reset... + assertEquals(0, context.getRetryCount()); + + } + + private static class StringHolder { + + private String string; + + public StringHolder(String string) { + this.string = string; + } + + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof StringHolder)) { + return false; + } + return string.equals(((StringHolder) obj).string); + } + + public int hashCode() { + return string.hashCode(); + } + + public String toString() { + return "String: " + string + " (hash = " + hashCode() + ")"; + } + + } + +} diff --git a/src/test/resources/org/springframework/repeat/support/trades.csv b/src/test/resources/org/springframework/repeat/support/trades.csv new file mode 100644 index 0000000..fe3d2ec --- /dev/null +++ b/src/test/resources/org/springframework/repeat/support/trades.csv @@ -0,0 +1,5 @@ +UK21341EAH45,978,98.34 +UK21341EAH46,112,18.12 +UK21341EAH47,245,12.78 +UK21341EAH48,108,109.25 +UK21341EAH49,854,123.39 \ No newline at end of file diff --git a/src/test/resources/org/springframework/retry/interceptor/retry-transaction-test.xml b/src/test/resources/org/springframework/retry/interceptor/retry-transaction-test.xml new file mode 100644 index 0000000..65298cd --- /dev/null +++ b/src/test/resources/org/springframework/retry/interceptor/retry-transaction-test.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + +