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}