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}