001/* 002 * Copyright 2016-2019 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 */ 016package org.springframework.batch.item.file.builder; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.List; 021import java.util.Locale; 022 023import org.apache.commons.logging.Log; 024import org.apache.commons.logging.LogFactory; 025 026import org.springframework.batch.item.file.FlatFileFooterCallback; 027import org.springframework.batch.item.file.FlatFileHeaderCallback; 028import org.springframework.batch.item.file.FlatFileItemWriter; 029import org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor; 030import org.springframework.batch.item.file.transform.DelimitedLineAggregator; 031import org.springframework.batch.item.file.transform.FieldExtractor; 032import org.springframework.batch.item.file.transform.FormatterLineAggregator; 033import org.springframework.batch.item.file.transform.LineAggregator; 034import org.springframework.core.io.Resource; 035import org.springframework.util.Assert; 036 037/** 038 * A builder implementation for the {@link FlatFileItemWriter} 039 * 040 * @author Michael Minella 041 * @author Glenn Renfro 042 * @author Mahmoud Ben Hassine 043 * @since 4.0 044 * @see FlatFileItemWriter 045 */ 046public class FlatFileItemWriterBuilder<T> { 047 048 protected Log logger = LogFactory.getLog(getClass()); 049 050 private Resource resource; 051 052 private boolean forceSync = false; 053 054 private String lineSeparator = FlatFileItemWriter.DEFAULT_LINE_SEPARATOR; 055 056 private LineAggregator<T> lineAggregator; 057 058 private String encoding = FlatFileItemWriter.DEFAULT_CHARSET; 059 060 private boolean shouldDeleteIfExists = true; 061 062 private boolean append = false; 063 064 private boolean shouldDeleteIfEmpty = false; 065 066 private FlatFileHeaderCallback headerCallback; 067 068 private FlatFileFooterCallback footerCallback; 069 070 private boolean transactional = FlatFileItemWriter.DEFAULT_TRANSACTIONAL; 071 072 private boolean saveState = true; 073 074 private String name; 075 076 private DelimitedBuilder<T> delimitedBuilder; 077 078 private FormattedBuilder<T> formattedBuilder; 079 080 /** 081 * Configure if the state of the {@link org.springframework.batch.item.ItemStreamSupport} 082 * should be persisted within the {@link org.springframework.batch.item.ExecutionContext} 083 * for restart purposes. 084 * 085 * @param saveState defaults to true 086 * @return The current instance of the builder. 087 */ 088 public FlatFileItemWriterBuilder<T> saveState(boolean saveState) { 089 this.saveState = saveState; 090 091 return this; 092 } 093 094 /** 095 * The name used to calculate the key within the 096 * {@link org.springframework.batch.item.ExecutionContext}. Required if 097 * {@link #saveState(boolean)} is set to true. 098 * 099 * @param name name of the reader instance 100 * @return The current instance of the builder. 101 * @see org.springframework.batch.item.ItemStreamSupport#setName(String) 102 */ 103 public FlatFileItemWriterBuilder<T> name(String name) { 104 this.name = name; 105 106 return this; 107 } 108 109 /** 110 * The {@link Resource} to be used as output. 111 * 112 * @param resource the output of the writer. 113 * @return The current instance of the builder. 114 * @see FlatFileItemWriter#setResource(Resource) 115 */ 116 public FlatFileItemWriterBuilder<T> resource(Resource resource) { 117 this.resource = resource; 118 119 return this; 120 } 121 122 /** 123 * A flag indicating that changes should be force-synced to disk on flush. Defaults 124 * to false. 125 * 126 * @param forceSync value to set the flag to 127 * @return The current instance of the builder. 128 * @see FlatFileItemWriter#setForceSync(boolean) 129 */ 130 public FlatFileItemWriterBuilder<T> forceSync(boolean forceSync) { 131 this.forceSync = forceSync; 132 133 return this; 134 } 135 136 /** 137 * String used to separate lines in output. Defaults to the System property 138 * line.separator. 139 * 140 * @param lineSeparator value to use for a line separator 141 * @return The current instance of the builder. 142 * @see FlatFileItemWriter#setLineSeparator(String) 143 */ 144 public FlatFileItemWriterBuilder<T> lineSeparator(String lineSeparator) { 145 this.lineSeparator = lineSeparator; 146 147 return this; 148 } 149 150 /** 151 * Line aggregator used to build the String version of each item. 152 * 153 * @param lineAggregator {@link LineAggregator} implementation 154 * @return The current instance of the builder. 155 * @see FlatFileItemWriter#setLineAggregator(LineAggregator) 156 */ 157 public FlatFileItemWriterBuilder<T> lineAggregator(LineAggregator<T> lineAggregator) { 158 this.lineAggregator = lineAggregator; 159 160 return this; 161 } 162 163 /** 164 * Encoding used for output. 165 * 166 * @param encoding encoding type. 167 * @return The current instance of the builder. 168 * @see FlatFileItemWriter#setEncoding(String) 169 */ 170 public FlatFileItemWriterBuilder<T> encoding(String encoding) { 171 this.encoding = encoding; 172 173 return this; 174 } 175 176 /** 177 * If set to true, once the step is complete, if the resource previously provided is 178 * empty, it will be deleted. 179 * 180 * @param shouldDelete defaults to false 181 * @return The current instance of the builder 182 * @see FlatFileItemWriter#setShouldDeleteIfEmpty(boolean) 183 */ 184 public FlatFileItemWriterBuilder<T> shouldDeleteIfEmpty(boolean shouldDelete) { 185 this.shouldDeleteIfEmpty = shouldDelete; 186 187 return this; 188 } 189 190 /** 191 * If set to true, upon the start of the step, if the resource already exists, it will 192 * be deleted and recreated. 193 * 194 * @param shouldDelete defaults to true 195 * @return The current instance of the builder 196 * @see FlatFileItemWriter#setShouldDeleteIfExists(boolean) 197 */ 198 public FlatFileItemWriterBuilder<T> shouldDeleteIfExists(boolean shouldDelete) { 199 this.shouldDeleteIfExists = shouldDelete; 200 201 return this; 202 } 203 204 /** 205 * If set to true and the file exists, the output will be appended to the existing 206 * file. 207 * 208 * @param append defaults to false 209 * @return The current instance of the builder 210 * @see FlatFileItemWriter#setAppendAllowed(boolean) 211 */ 212 public FlatFileItemWriterBuilder<T> append(boolean append) { 213 this.append = append; 214 215 return this; 216 } 217 218 /** 219 * A callback for header processing. 220 * 221 * @param callback {@link FlatFileHeaderCallback} impl 222 * @return The current instance of the builder 223 * @see FlatFileItemWriter#setHeaderCallback(FlatFileHeaderCallback) 224 */ 225 public FlatFileItemWriterBuilder<T> headerCallback(FlatFileHeaderCallback callback) { 226 this.headerCallback = callback; 227 228 return this; 229 } 230 231 /** 232 * A callback for footer processing 233 * @param callback {@link FlatFileFooterCallback} impl 234 * @return The current instance of the builder 235 * @see FlatFileItemWriter#setFooterCallback(FlatFileFooterCallback) 236 */ 237 public FlatFileItemWriterBuilder<T> footerCallback(FlatFileFooterCallback callback) { 238 this.footerCallback = callback; 239 240 return this; 241 } 242 243 /** 244 * If set to true, the flushing of the buffer is delayed while a transaction is active. 245 * 246 * @param transactional defaults to true 247 * @return The current instance of the builder 248 * @see FlatFileItemWriter#setTransactional(boolean) 249 */ 250 public FlatFileItemWriterBuilder<T> transactional(boolean transactional) { 251 this.transactional = transactional; 252 253 return this; 254 } 255 256 /** 257 * Returns an instance of a {@link DelimitedBuilder} for building a 258 * {@link DelimitedLineAggregator}. The {@link DelimitedLineAggregator} configured by 259 * this builder will only be used if one is not explicitly configured via 260 * {@link FlatFileItemWriterBuilder#lineAggregator} 261 * 262 * @return a {@link DelimitedBuilder} 263 * 264 */ 265 public DelimitedBuilder<T> delimited() { 266 this.delimitedBuilder = new DelimitedBuilder<>(this); 267 return this.delimitedBuilder; 268 } 269 270 /** 271 * Returns an instance of a {@link FormattedBuilder} for building a 272 * {@link FormatterLineAggregator}. The {@link FormatterLineAggregator} configured by 273 * this builder will only be used if one is not explicitly configured via 274 * {@link FlatFileItemWriterBuilder#lineAggregator} 275 * 276 * @return a {@link FormattedBuilder} 277 * 278 */ 279 public FormattedBuilder<T> formatted() { 280 this.formattedBuilder = new FormattedBuilder<>(this); 281 return this.formattedBuilder; 282 } 283 284 /** 285 * A builder for constructing a {@link FormatterLineAggregator}. 286 * 287 * @param <T> the type of the parent {@link FlatFileItemWriterBuilder} 288 */ 289 public static class FormattedBuilder<T> { 290 291 private FlatFileItemWriterBuilder<T> parent; 292 293 private String format; 294 295 private Locale locale = Locale.getDefault(); 296 297 private int maximumLength = 0; 298 299 private int minimumLength = 0; 300 301 private FieldExtractor<T> fieldExtractor; 302 303 private List<String> names = new ArrayList<>(); 304 305 protected FormattedBuilder(FlatFileItemWriterBuilder<T> parent) { 306 this.parent = parent; 307 } 308 309 /** 310 * Set the format string used to aggregate items 311 * @param format used to aggregate items 312 * @return The instance of the builder for chaining. 313 */ 314 public FormattedBuilder<T> format(String format) { 315 this.format = format; 316 return this; 317 } 318 319 /** 320 * Set the locale. 321 * @param locale to use 322 * @return The instance of the builder for chaining. 323 */ 324 public FormattedBuilder<T> locale(Locale locale) { 325 this.locale = locale; 326 return this; 327 } 328 329 /** 330 * Set the minimum length of the formatted string. If this is not set 331 * the default is to allow any length. 332 * @param minimumLength of the formatted string 333 * @return The instance of the builder for chaining. 334 */ 335 public FormattedBuilder<T> minimumLength(int minimumLength) { 336 this.minimumLength = minimumLength; 337 return this; 338 } 339 340 /** 341 * Set the maximum length of the formatted string. If this is not set 342 * the default is to allow any length. 343 * @param maximumLength of the formatted string 344 * @return The instance of the builder for chaining. 345 */ 346 public FormattedBuilder<T> maximumLength(int maximumLength) { 347 this.maximumLength = maximumLength; 348 return this; 349 } 350 351 /** 352 * Set the {@link FieldExtractor} to use to extract fields from each item. 353 * @param fieldExtractor to use to extract fields from each item 354 * @return The current instance of the builder 355 */ 356 public FlatFileItemWriterBuilder<T> fieldExtractor(FieldExtractor<T> fieldExtractor) { 357 this.fieldExtractor = fieldExtractor; 358 return this.parent; 359 } 360 361 /** 362 * Names of each of the fields within the fields that are returned in the order 363 * they occur within the formatted file. These names will be used to create 364 * a {@link BeanWrapperFieldExtractor} only if no explicit field extractor 365 * is set via {@link FormattedBuilder#fieldExtractor(FieldExtractor)}. 366 * 367 * @param names names of each field 368 * @return The parent {@link FlatFileItemWriterBuilder} 369 * @see BeanWrapperFieldExtractor#setNames(String[]) 370 */ 371 public FlatFileItemWriterBuilder<T> names(String[] names) { 372 this.names.addAll(Arrays.asList(names)); 373 return this.parent; 374 } 375 376 public FormatterLineAggregator<T> build() { 377 Assert.notNull(this.format, "A format is required"); 378 Assert.isTrue((this.names != null && !this.names.isEmpty()) || this.fieldExtractor != null, 379 "A list of field names or a field extractor is required"); 380 381 FormatterLineAggregator<T> formatterLineAggregator = new FormatterLineAggregator<>(); 382 formatterLineAggregator.setFormat(this.format); 383 formatterLineAggregator.setLocale(this.locale); 384 formatterLineAggregator.setMinimumLength(this.minimumLength); 385 formatterLineAggregator.setMaximumLength(this.maximumLength); 386 387 if (this.fieldExtractor == null) { 388 BeanWrapperFieldExtractor<T> beanWrapperFieldExtractor = new BeanWrapperFieldExtractor<>(); 389 beanWrapperFieldExtractor.setNames(this.names.toArray(new String[this.names.size()])); 390 try { 391 beanWrapperFieldExtractor.afterPropertiesSet(); 392 } 393 catch (Exception e) { 394 throw new IllegalStateException("Unable to initialize FormatterLineAggregator", e); 395 } 396 this.fieldExtractor = beanWrapperFieldExtractor; 397 } 398 399 formatterLineAggregator.setFieldExtractor(this.fieldExtractor); 400 return formatterLineAggregator; 401 } 402 } 403 404 /** 405 * A builder for constructing a {@link DelimitedLineAggregator} 406 * 407 * @param <T> the type of the parent {@link FlatFileItemWriterBuilder} 408 */ 409 public static class DelimitedBuilder<T> { 410 411 private FlatFileItemWriterBuilder<T> parent; 412 413 private List<String> names = new ArrayList<>(); 414 415 private String delimiter = ","; 416 417 private FieldExtractor<T> fieldExtractor; 418 419 protected DelimitedBuilder(FlatFileItemWriterBuilder<T> parent) { 420 this.parent = parent; 421 } 422 423 /** 424 * Define the delimiter for the file. 425 * 426 * @param delimiter String used as a delimiter between fields. 427 * @return The instance of the builder for chaining. 428 * @see DelimitedLineAggregator#setDelimiter(String) 429 */ 430 public DelimitedBuilder<T> delimiter(String delimiter) { 431 this.delimiter = delimiter; 432 return this; 433 } 434 435 /** 436 * Names of each of the fields within the fields that are returned in the order 437 * they occur within the delimited file. These names will be used to create 438 * a {@link BeanWrapperFieldExtractor} only if no explicit field extractor 439 * is set via {@link DelimitedBuilder#fieldExtractor(FieldExtractor)}. 440 * 441 * @param names names of each field 442 * @return The parent {@link FlatFileItemWriterBuilder} 443 * @see BeanWrapperFieldExtractor#setNames(String[]) 444 */ 445 public FlatFileItemWriterBuilder<T> names(String[] names) { 446 this.names.addAll(Arrays.asList(names)); 447 return this.parent; 448 } 449 450 /** 451 * Set the {@link FieldExtractor} to use to extract fields from each item. 452 * @param fieldExtractor to use to extract fields from each item 453 * @return The parent {@link FlatFileItemWriterBuilder} 454 */ 455 public FlatFileItemWriterBuilder<T> fieldExtractor(FieldExtractor<T> fieldExtractor) { 456 this.fieldExtractor = fieldExtractor; 457 return this.parent; 458 } 459 460 public DelimitedLineAggregator<T> build() { 461 Assert.isTrue((this.names != null && !this.names.isEmpty()) || this.fieldExtractor != null, 462 "A list of field names or a field extractor is required"); 463 464 DelimitedLineAggregator<T> delimitedLineAggregator = new DelimitedLineAggregator<>(); 465 if (this.delimiter != null) { 466 delimitedLineAggregator.setDelimiter(this.delimiter); 467 } 468 469 if (this.fieldExtractor == null) { 470 BeanWrapperFieldExtractor<T> beanWrapperFieldExtractor = new BeanWrapperFieldExtractor<>(); 471 beanWrapperFieldExtractor.setNames(this.names.toArray(new String[this.names.size()])); 472 try { 473 beanWrapperFieldExtractor.afterPropertiesSet(); 474 } 475 catch (Exception e) { 476 throw new IllegalStateException("Unable to initialize DelimitedLineAggregator", e); 477 } 478 this.fieldExtractor = beanWrapperFieldExtractor; 479 } 480 481 delimitedLineAggregator.setFieldExtractor(this.fieldExtractor); 482 return delimitedLineAggregator; 483 } 484 } 485 486 /** 487 * Validates and builds a {@link FlatFileItemWriter}. 488 * 489 * @return a {@link FlatFileItemWriter} 490 */ 491 public FlatFileItemWriter<T> build() { 492 493 Assert.isTrue(this.lineAggregator != null || this.delimitedBuilder != null || this.formattedBuilder != null, 494 "A LineAggregator or a DelimitedBuilder or a FormattedBuilder is required"); 495 496 if(this.saveState) { 497 Assert.hasText(this.name, "A name is required when saveState is true"); 498 } 499 500 if(this.resource == null) { 501 logger.debug("The resource is null. This is only a valid scenario when " + 502 "injecting it later as in when using the MultiResourceItemWriter"); 503 } 504 505 FlatFileItemWriter<T> writer = new FlatFileItemWriter<>(); 506 507 writer.setName(this.name); 508 writer.setAppendAllowed(this.append); 509 writer.setEncoding(this.encoding); 510 writer.setFooterCallback(this.footerCallback); 511 writer.setForceSync(this.forceSync); 512 writer.setHeaderCallback(this.headerCallback); 513 if (this.lineAggregator == null) { 514 Assert.state(this.delimitedBuilder == null || this.formattedBuilder == null, 515 "Either a DelimitedLineAggregator or a FormatterLineAggregator should be provided, but not both"); 516 if (this.delimitedBuilder != null) { 517 this.lineAggregator = this.delimitedBuilder.build(); 518 } 519 else { 520 this.lineAggregator = this.formattedBuilder.build(); 521 } 522 } 523 writer.setLineAggregator(this.lineAggregator); 524 writer.setLineSeparator(this.lineSeparator); 525 writer.setResource(this.resource); 526 writer.setSaveState(this.saveState); 527 writer.setShouldDeleteIfEmpty(this.shouldDeleteIfEmpty); 528 writer.setShouldDeleteIfExists(this.shouldDeleteIfExists); 529 writer.setTransactional(this.transactional); 530 531 return writer; 532 } 533}