001/*
002 * Copyright 2006-2007 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.batch.repeat.interceptor;
018
019import org.aopalliance.intercept.MethodInterceptor;
020import org.aopalliance.intercept.MethodInvocation;
021import org.springframework.aop.ProxyMethodInvocation;
022import org.springframework.batch.repeat.RepeatStatus;
023import org.springframework.batch.repeat.RepeatCallback;
024import org.springframework.batch.repeat.RepeatContext;
025import org.springframework.batch.repeat.RepeatException;
026import org.springframework.batch.repeat.RepeatOperations;
027import org.springframework.batch.repeat.support.RepeatTemplate;
028import org.springframework.util.Assert;
029
030/**
031 * A {@link MethodInterceptor} that can be used to automatically repeat calls to
032 * a method on a service. The injected {@link RepeatOperations} is used to
033 * control the completion of the loop. Independent of the completion policy in
034 * the {@link RepeatOperations} the loop will repeat until the target method
035 * returns null or false. Be careful when injecting a bespoke
036 * {@link RepeatOperations} that the loop will actually terminate, because the
037 * default policy for a vanilla {@link RepeatTemplate} will never complete if
038 * the return type of the target method is void (the value returned is always
039 * not-null, representing the {@link Void#TYPE}).
040 * 
041 * @author Dave Syer
042 */
043public class RepeatOperationsInterceptor implements MethodInterceptor {
044
045        private RepeatOperations repeatOperations = new RepeatTemplate();
046
047        /**
048         * Setter for the {@link RepeatOperations}.
049         * 
050         * @param batchTemplate template to be used
051         * @throws IllegalArgumentException if the argument is null.
052         */
053        public void setRepeatOperations(RepeatOperations batchTemplate) {
054                Assert.notNull(batchTemplate, "'repeatOperations' cannot be null.");
055                this.repeatOperations = batchTemplate;
056        }
057
058        /**
059         * Invoke the proceeding method call repeatedly, according to the properties
060         * of the injected {@link RepeatOperations}.
061         * 
062         * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
063         */
064    @Override
065        public Object invoke(final MethodInvocation invocation) throws Throwable {
066
067                final ResultHolder result = new ResultHolder();
068                // Cache void return value if intercepted method returns void
069                final boolean voidReturnType = Void.TYPE.equals(invocation.getMethod().getReturnType());
070                if (voidReturnType) {
071                        // This will be ignored anyway, but we want it to be non-null for
072                        // convenience of checking that there is a result.
073                        result.setValue(new Object());
074                }
075
076                try {
077                        repeatOperations.iterate(new RepeatCallback() {
078
079                @Override
080                                public RepeatStatus doInIteration(RepeatContext context) throws Exception {
081                                        try {
082
083                                                MethodInvocation clone = invocation;
084                                                if (invocation instanceof ProxyMethodInvocation) {
085                                                        clone = ((ProxyMethodInvocation) invocation).invocableClone();
086                                                }
087                                                else {
088                                                        throw new IllegalStateException(
089                                                                        "MethodInvocation of the wrong type detected - this should not happen with Spring AOP, so please raise an issue if you see this exception");
090                                                }
091
092                                                Object value = clone.proceed();
093                                                if (voidReturnType) {
094                                                        return RepeatStatus.CONTINUABLE;
095                                                }
096                                                if (!isComplete(value)) {
097                                                        // Save the last result
098                                                        result.setValue(value);
099                                                        return RepeatStatus.CONTINUABLE;
100                                                }
101                                                else {
102                                                        result.setFinalValue(value);
103                                                        return RepeatStatus.FINISHED;
104                                                }
105                                        }
106                                        catch (Throwable e) {
107                                                if (e instanceof Exception) {
108                                                        throw (Exception) e;
109                                                }
110                                                else {
111                                                        throw new RepeatOperationsInterceptorException("Unexpected error in batch interceptor", e);
112                                                }
113                                        }
114                                }
115
116                        });
117                }
118                catch (Throwable t) {
119                        // The repeat exception should be unwrapped by the template
120                        throw t;
121                }
122
123                if (result.isReady()) {
124                        return result.getValue();
125                }
126
127                // No result means something weird happened
128                throw new IllegalStateException("No result available for attempted repeat call to " + invocation
129                                + ".  The invocation was never called, so maybe there is a problem with the completion policy?");
130        }
131
132        /**
133         * @param result
134         * @return
135         */
136        private boolean isComplete(Object result) {
137                return result == null || (result instanceof Boolean) && !((Boolean) result).booleanValue();
138        }
139
140        /**
141         * Simple wrapper exception class to enable nasty errors to be passed out of
142         * the scope of the repeat operations and handled by the caller.
143         * 
144         * @author Dave Syer
145         * 
146         */
147        @SuppressWarnings("serial")
148        private static class RepeatOperationsInterceptorException extends RepeatException {
149                /**
150                 * @param message
151                 * @param e
152                 */
153                public RepeatOperationsInterceptorException(String message, Throwable e) {
154                        super(message, e);
155                }
156        }
157
158        /**
159         * Simple wrapper object for the result from a method invocation.
160         * 
161         * @author Dave Syer
162         * 
163         */
164        private static class ResultHolder {
165                private Object value = null;
166
167                private boolean ready = false;
168
169                /**
170                 * Public setter for the Object.
171                 * @param value the value to set
172                 */
173                public void setValue(Object value) {
174                        this.ready = true;
175                        this.value = value;
176                }
177
178                /**
179                 * @param value
180                 */
181                public void setFinalValue(Object value) {
182                        if (ready) {
183                                // Only set the value the last time if the last time was also
184                                // the first time
185                                return;
186                        }
187                        setValue(value);
188                }
189
190                /**
191                 * Public getter for the Object.
192                 * @return the value
193                 */
194                public Object getValue() {
195                        return value;
196                }
197
198                /**
199                 * @return true if a value has been set
200                 */
201                public boolean isReady() {
202                        return ready;
203                }
204        }
205
206}