001/*
002 * Copyright 2017-2018 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.item.data.builder;
018
019import java.lang.reflect.Method;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.List;
023import java.util.Map;
024
025import org.springframework.batch.item.data.RepositoryItemReader;
026import org.springframework.cglib.proxy.Enhancer;
027import org.springframework.cglib.proxy.MethodInterceptor;
028import org.springframework.cglib.proxy.MethodProxy;
029import org.springframework.data.domain.Sort;
030import org.springframework.data.repository.PagingAndSortingRepository;
031import org.springframework.util.Assert;
032import org.springframework.util.CollectionUtils;
033import org.springframework.util.StringUtils;
034
035/**
036 * A builder implementation for the {@link RepositoryItemReader}.
037 *
038 * @author Glenn Renfro
039 * @author Mahmoud Ben Hassine
040 * @since 4.0
041 * @see RepositoryItemReader
042 */
043
044public class RepositoryItemReaderBuilder<T> {
045
046        private PagingAndSortingRepository<?, ?> repository;
047
048        private Map<String, Sort.Direction> sorts;
049
050        private List<?> arguments;
051
052        private int pageSize = 10;
053
054        private String methodName;
055
056        private RepositoryMethodReference repositoryMethodReference;
057
058        private boolean saveState = true;
059
060        private String name;
061
062        private int maxItemCount = Integer.MAX_VALUE;
063
064        private int currentItemCount;
065
066        /**
067         * Configure if the state of the {@link org.springframework.batch.item.ItemStreamSupport}
068         * should be persisted within the {@link org.springframework.batch.item.ExecutionContext}
069         * for restart purposes.
070         *
071         * @param saveState defaults to true
072         * @return The current instance of the builder.
073         */
074        public RepositoryItemReaderBuilder<T> saveState(boolean saveState) {
075                this.saveState = saveState;
076
077                return this;
078        }
079
080        /**
081         * The name used to calculate the key within the
082         * {@link org.springframework.batch.item.ExecutionContext}. Required if
083         * {@link #saveState(boolean)} is set to true.
084         *
085         * @param name name of the reader instance
086         * @return The current instance of the builder.
087         * @see org.springframework.batch.item.ItemStreamSupport#setName(String)
088         */
089        public RepositoryItemReaderBuilder<T> name(String name) {
090                this.name = name;
091
092                return this;
093        }
094
095        /**
096         * Configure the max number of items to be read.
097         *
098         * @param maxItemCount the max items to be read
099         * @return The current instance of the builder.
100         * @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setMaxItemCount(int)
101         */
102        public RepositoryItemReaderBuilder<T> maxItemCount(int maxItemCount) {
103                this.maxItemCount = maxItemCount;
104
105                return this;
106        }
107
108        /**
109         * Index for the current item. Used on restarts to indicate where to start from.
110         *
111         * @param currentItemCount current index
112         * @return this instance for method chaining
113         * @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setCurrentItemCount(int)
114         */
115        public RepositoryItemReaderBuilder<T> currentItemCount(int currentItemCount) {
116                this.currentItemCount = currentItemCount;
117
118                return this;
119        }
120
121        /**
122         * Arguments to be passed to the data providing method.
123         *
124         * @param arguments list of method arguments to be passed to the repository.
125         * @return The current instance of the builder.
126         * @see RepositoryItemReader#setArguments(List)
127         */
128        public RepositoryItemReaderBuilder<T> arguments(List<?> arguments) {
129                this.arguments = arguments;
130
131                return this;
132        }
133
134        /**
135         * Provides ordering of the results so that order is maintained between paged queries.
136         *
137         * @param sorts the fields to sort by and the directions.
138         * @return The current instance of the builder.
139         * @see RepositoryItemReader#setSort(Map)
140         */
141        public RepositoryItemReaderBuilder<T> sorts(Map<String, Sort.Direction> sorts) {
142                this.sorts = sorts;
143
144                return this;
145        }
146
147        /**
148         * Establish the pageSize for the generated RepositoryItemReader.
149         *
150         * @param pageSize The number of items to retrieve per page.
151         * @return The current instance of the builder.
152         * @see RepositoryItemReader#setPageSize(int)
153         */
154        public RepositoryItemReaderBuilder<T> pageSize(int pageSize) {
155                this.pageSize = pageSize;
156
157                return this;
158        }
159
160        /**
161         * The {@link org.springframework.data.repository.PagingAndSortingRepository}
162         * implementation used to read input from.
163         *
164         * @param repository underlying repository for input to be read from.
165         * @return The current instance of the builder.
166         * @see RepositoryItemReader#setRepository(PagingAndSortingRepository)
167         */
168        public RepositoryItemReaderBuilder<T> repository(PagingAndSortingRepository<?, ?> repository) {
169                this.repository = repository;
170
171                return this;
172        }
173
174        /**
175         * Specifies what method on the repository to call. This method must take
176         * {@link org.springframework.data.domain.Pageable} as the <em>last</em> argument.
177         *
178         * @param methodName name of the method to invoke.
179         * @return The current instance of the builder.
180         * @see RepositoryItemReader#setMethodName(String)
181         */
182        public RepositoryItemReaderBuilder<T> methodName(String methodName) {
183                this.methodName = methodName;
184
185                return this;
186        }
187
188        /**
189         * Specifies a repository and the type-safe method to call for the reader. The method
190         * configured via this mechanism must take
191         * {@link org.springframework.data.domain.Pageable} as the <em>last</em>
192         * argument. This method can be used in place of {@link #repository(PagingAndSortingRepository)},
193         * {@link #methodName(String)}, and {@link #arguments(List)}.
194         *
195         * Note: The repository that is used by the repositoryMethodReference must be
196         * non-final.
197         *
198         * @param repositoryMethodReference of the used to get a repository and type-safe
199         * method for use by the reader.
200         * @return The current instance of the builder.
201         * @see RepositoryItemReader#setMethodName(String)
202         * @see RepositoryItemReader#setRepository(PagingAndSortingRepository)
203         *
204         */
205        public RepositoryItemReaderBuilder<T> repository(RepositoryMethodReference repositoryMethodReference) {
206                this.repositoryMethodReference = repositoryMethodReference;
207
208                return this;
209        }
210
211        /**
212         * Builds the {@link RepositoryItemReader}.
213         *
214         * @return a {@link RepositoryItemReader}
215         */
216        public RepositoryItemReader<T> build() {
217                if (this.repositoryMethodReference != null) {
218                        this.methodName = this.repositoryMethodReference.getMethodName();
219                        this.repository = this.repositoryMethodReference.getRepository();
220
221                        if(CollectionUtils.isEmpty(this.arguments)) {
222                                this.arguments = this.repositoryMethodReference.getArguments();
223                        }
224                }
225
226                Assert.notNull(this.sorts, "sorts map is required.");
227                Assert.notNull(this.repository, "repository is required.");
228                Assert.hasText(this.methodName, "methodName is required.");
229                if (this.saveState) {
230                        Assert.state(StringUtils.hasText(this.name), "A name is required when saveState is set to true.");
231                }
232
233                RepositoryItemReader<T> reader = new RepositoryItemReader<>();
234                reader.setArguments(this.arguments);
235                reader.setRepository(this.repository);
236                reader.setMethodName(this.methodName);
237                reader.setPageSize(this.pageSize);
238                reader.setCurrentItemCount(this.currentItemCount);
239                reader.setMaxItemCount(this.maxItemCount);
240                reader.setSaveState(this.saveState);
241                reader.setSort(this.sorts);
242                reader.setName(this.name);
243                return reader;
244        }
245
246        /**
247         * Establishes a proxy that will capture a the Repository and the associated
248         * methodName that will be used by the reader.
249         * @param <T> The type of repository that will be used by the reader.  The class must
250         * not be final.
251         */
252        public static class RepositoryMethodReference<T> {
253                private RepositoryMethodInterceptor repositoryInvocationHandler;
254
255                private PagingAndSortingRepository<?, ?> repository;
256
257                public RepositoryMethodReference(PagingAndSortingRepository<?, ?> repository) {
258                        this.repository = repository;
259                        this.repositoryInvocationHandler = new RepositoryMethodInterceptor();
260                }
261
262                /**
263                 * The proxy returned prevents actual method execution and is only used to gather,
264                 * information about the method.
265                 * @return T is a proxy of the object passed in in the constructor
266                 */
267                public T methodIs() {
268                        Enhancer enhancer = new Enhancer();
269                        enhancer.setSuperclass(this.repository.getClass());
270                        enhancer.setCallback(this.repositoryInvocationHandler);
271                        return (T) enhancer.create();
272                }
273
274                PagingAndSortingRepository<?, ?> getRepository() {
275                        return this.repository;
276                }
277
278                String getMethodName() {
279                        return this.repositoryInvocationHandler.getMethodName();
280                }
281
282                List<Object> getArguments() {
283                        return this.repositoryInvocationHandler.getArguments();
284                }
285        }
286
287        private static class RepositoryMethodInterceptor implements MethodInterceptor {
288                private String methodName;
289
290                private List<Object> arguments;
291
292                @Override
293                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
294                        this.methodName = method.getName();
295                        if (objects != null && objects.length > 1) {
296                                arguments = new ArrayList<>(Arrays.asList(objects));
297                                // remove last entry because that will be provided by the
298                                // RepositoryItemReader
299                                arguments.remove(objects.length - 1);
300                        }
301                        return null;
302                }
303
304                String getMethodName() {
305                        return this.methodName;
306                }
307
308                List<Object> getArguments() {
309                        return arguments;
310                }
311        }
312}