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.item.file.transform;
018
019import java.math.BigDecimal;
020import java.text.DateFormat;
021import java.text.DecimalFormat;
022import java.text.NumberFormat;
023import java.text.ParseException;
024import java.text.SimpleDateFormat;
025import java.util.Arrays;
026import java.util.Date;
027import java.util.List;
028import java.util.Locale;
029import java.util.Properties;
030
031import org.springframework.util.Assert;
032import org.springframework.util.StringUtils;
033
034/**
035 * Default implementation of {@link FieldSet} using Java using Java primitive
036 * and standard types and utilities. Strings are trimmed before parsing by
037 * default, and so are plain String values.
038 * 
039 * @author Rob Harrop
040 * @author Dave Syer
041 */
042public class DefaultFieldSet implements FieldSet {
043
044        private final static String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";
045
046        private DateFormat dateFormat = new SimpleDateFormat(DEFAULT_DATE_PATTERN);
047        {
048                dateFormat.setLenient(false);
049        }
050
051        private NumberFormat numberFormat = NumberFormat.getInstance(Locale.US);
052
053        private String grouping = ",";
054
055        private String decimal = ".";
056
057        /**
058         * The fields wrapped by this '<code>FieldSet</code>' instance.
059         */
060        private String[] tokens;
061
062        private List<String> names;
063
064        /**
065         * The {@link NumberFormat} to use for parsing numbers. If unset the US
066         * locale will be used ('.' as decimal place).
067         * @param numberFormat the {@link NumberFormat} to use for number parsing
068         */
069        public final void setNumberFormat(NumberFormat numberFormat) {
070                this.numberFormat = numberFormat;
071                if (numberFormat instanceof DecimalFormat) {
072                        grouping = "" + ((DecimalFormat) numberFormat).getDecimalFormatSymbols().getGroupingSeparator();
073                        decimal = "" + ((DecimalFormat) numberFormat).getDecimalFormatSymbols().getDecimalSeparator();
074                }
075        }
076
077        /**
078         * The {@link DateFormat} to use for parsing numbers. If unset the default
079         * pattern is ISO standard <code>yyyy/MM/dd</code>.
080         * @param dateFormat the {@link DateFormat} to use for date parsing
081         */
082        public void setDateFormat(DateFormat dateFormat) {
083                this.dateFormat = dateFormat;
084        }
085
086        /**
087         * Create a FieldSet with anonymous tokens. They can only be retrieved by
088         * column number.
089         * @param tokens the token values
090         * @see FieldSet#readString(int)
091         */
092        public DefaultFieldSet(String[] tokens) {
093                this.tokens = tokens == null ? null : tokens.clone();
094                setNumberFormat(NumberFormat.getInstance(Locale.US));
095        }
096
097        /**
098         * Create a FieldSet with named tokens. The token values can then be
099         * retrieved either by name or by column number.
100         * @param tokens the token values
101         * @param names the names of the tokens
102         * @see FieldSet#readString(String)
103         */
104        public DefaultFieldSet(String[] tokens, String[] names) {
105                Assert.notNull(tokens, "Tokens must not be null");
106                Assert.notNull(names, "Names must not be null");
107                if (tokens.length != names.length) {
108                        throw new IllegalArgumentException("Field names must be same length as values: names="
109                                        + Arrays.asList(names) + ", values=" + Arrays.asList(tokens));
110                }
111                this.tokens = tokens.clone();
112                this.names = Arrays.asList(names);
113                setNumberFormat(NumberFormat.getInstance(Locale.US));
114        }
115
116        /*
117         * (non-Javadoc)
118         * 
119         * @see org.springframework.batch.item.file.mapping.IFieldSet#getNames()
120         */
121    @Override
122        public String[] getNames() {
123                if (names == null) {
124                        throw new IllegalStateException("Field names are not known");
125                }
126                return names.toArray(new String[names.size()]);
127        }
128
129        /*
130         * (non-Javadoc)
131         * 
132         * @see org.springframework.batch.item.file.mapping.FieldSet#hasNames()
133         */
134    @Override
135        public boolean hasNames() {
136                return names != null;
137        }
138
139        /*
140         * (non-Javadoc)
141         * 
142         * @see org.springframework.batch.item.file.mapping.IFieldSet#getValues()
143         */
144    @Override
145        public String[] getValues() {
146                return tokens.clone();
147        }
148
149        /*
150         * (non-Javadoc)
151         * 
152         * @see
153         * org.springframework.batch.item.file.mapping.IFieldSet#readString(int)
154         */
155    @Override
156        public String readString(int index) {
157                return readAndTrim(index);
158        }
159
160        /*
161         * (non-Javadoc)
162         * 
163         * @see
164         * org.springframework.batch.item.file.mapping.IFieldSet#readString(java
165         * .lang.String)
166         */
167    @Override
168        public String readString(String name) {
169                return readString(indexOf(name));
170        }
171
172        /*
173         * (non-Javadoc)
174         * 
175         * @see
176         * org.springframework.batch.item.file.mapping.IFieldSet#readRawString(int)
177         */
178    @Override
179        public String readRawString(int index) {
180                return tokens[index];
181        }
182
183        /*
184         * (non-Javadoc)
185         * 
186         * @see
187         * org.springframework.batch.item.file.mapping.IFieldSet#readRawString(java
188         * .lang.String)
189         */
190    @Override
191        public String readRawString(String name) {
192                return readRawString(indexOf(name));
193        }
194
195        /*
196         * (non-Javadoc)
197         * 
198         * @see
199         * org.springframework.batch.item.file.mapping.IFieldSet#readBoolean(int)
200         */
201    @Override
202        public boolean readBoolean(int index) {
203                return readBoolean(index, "true");
204        }
205
206        /*
207         * (non-Javadoc)
208         * 
209         * @see
210         * org.springframework.batch.item.file.mapping.IFieldSet#readBoolean(java
211         * .lang.String)
212         */
213    @Override
214        public boolean readBoolean(String name) {
215                return readBoolean(indexOf(name));
216        }
217
218        /*
219         * (non-Javadoc)
220         * 
221         * @see
222         * org.springframework.batch.item.file.mapping.IFieldSet#readBoolean(int,
223         * java.lang.String)
224         */
225    @Override
226        public boolean readBoolean(int index, String trueValue) {
227                Assert.notNull(trueValue, "'trueValue' cannot be null.");
228
229                String value = readAndTrim(index);
230
231                return trueValue.equals(value);
232        }
233
234        /*
235         * (non-Javadoc)
236         * 
237         * @see
238         * org.springframework.batch.item.file.mapping.IFieldSet#readBoolean(java
239         * .lang.String, java.lang.String)
240         */
241    @Override
242        public boolean readBoolean(String name, String trueValue) {
243                return readBoolean(indexOf(name), trueValue);
244        }
245
246        /*
247         * (non-Javadoc)
248         * 
249         * @see org.springframework.batch.item.file.mapping.IFieldSet#readChar(int)
250         */
251    @Override
252        public char readChar(int index) {
253                String value = readAndTrim(index);
254
255                Assert.isTrue(value.length() == 1, "Cannot convert field value '" + value + "' to char.");
256
257                return value.charAt(0);
258        }
259
260        /*
261         * (non-Javadoc)
262         * 
263         * @see
264         * org.springframework.batch.item.file.mapping.IFieldSet#readChar(java.lang
265         * .String)
266         */
267    @Override
268        public char readChar(String name) {
269                return readChar(indexOf(name));
270        }
271
272        /*
273         * (non-Javadoc)
274         * 
275         * @see org.springframework.batch.item.file.mapping.IFieldSet#readByte(int)
276         */
277    @Override
278        public byte readByte(int index) {
279                return Byte.parseByte(readAndTrim(index));
280        }
281
282        /*
283         * (non-Javadoc)
284         * 
285         * @see
286         * org.springframework.batch.item.file.mapping.IFieldSet#readByte(java.lang
287         * .String)
288         */
289    @Override
290        public byte readByte(String name) {
291                return readByte(indexOf(name));
292        }
293
294        /*
295         * (non-Javadoc)
296         * 
297         * @see org.springframework.batch.item.file.mapping.IFieldSet#readShort(int)
298         */
299    @Override
300        public short readShort(int index) {
301                return Short.parseShort(readAndTrim(index));
302        }
303
304        /*
305         * (non-Javadoc)
306         * 
307         * @see
308         * org.springframework.batch.item.file.mapping.IFieldSet#readShort(java.
309         * lang.String)
310         */
311    @Override
312        public short readShort(String name) {
313                return readShort(indexOf(name));
314        }
315
316        /*
317         * (non-Javadoc)
318         * 
319         * @see org.springframework.batch.item.file.mapping.IFieldSet#readInt(int)
320         */
321    @Override
322        public int readInt(int index) {
323                return parseNumber(readAndTrim(index)).intValue();
324        }
325
326        /*
327         * (non-Javadoc)
328         * 
329         * @see
330         * org.springframework.batch.item.file.mapping.IFieldSet#readInt(java.lang
331         * .String)
332         */
333    @Override
334        public int readInt(String name) {
335                return readInt(indexOf(name));
336        }
337
338        /*
339         * (non-Javadoc)
340         * 
341         * @see org.springframework.batch.item.file.mapping.IFieldSet#readInt(int,
342         * int)
343         */
344    @Override
345        public int readInt(int index, int defaultValue) {
346                String value = readAndTrim(index);
347
348                return StringUtils.hasLength(value) ? Integer.parseInt(value) : defaultValue;
349        }
350
351        /*
352         * (non-Javadoc)
353         * 
354         * @see
355         * org.springframework.batch.item.file.mapping.IFieldSet#readInt(java.lang
356         * .String, int)
357         */
358    @Override
359        public int readInt(String name, int defaultValue) {
360                return readInt(indexOf(name), defaultValue);
361        }
362
363        /*
364         * (non-Javadoc)
365         * 
366         * @see org.springframework.batch.item.file.mapping.IFieldSet#readLong(int)
367         */
368    @Override
369        public long readLong(int index) {
370                return parseNumber(readAndTrim(index)).longValue();
371        }
372
373        /*
374         * (non-Javadoc)
375         * 
376         * @see
377         * org.springframework.batch.item.file.mapping.IFieldSet#readLong(java.lang
378         * .String)
379         */
380    @Override
381        public long readLong(String name) {
382                return readLong(indexOf(name));
383        }
384
385        /*
386         * (non-Javadoc)
387         * 
388         * @see org.springframework.batch.item.file.mapping.IFieldSet#readLong(int,
389         * long)
390         */
391    @Override
392        public long readLong(int index, long defaultValue) {
393                String value = readAndTrim(index);
394
395                return StringUtils.hasLength(value) ? Long.parseLong(value) : defaultValue;
396        }
397
398        /*
399         * (non-Javadoc)
400         * 
401         * @see
402         * org.springframework.batch.item.file.mapping.IFieldSet#readLong(java.lang
403         * .String, long)
404         */
405    @Override
406        public long readLong(String name, long defaultValue) {
407                return readLong(indexOf(name), defaultValue);
408        }
409
410        /*
411         * (non-Javadoc)
412         * 
413         * @see org.springframework.batch.item.file.mapping.IFieldSet#readFloat(int)
414         */
415    @Override
416        public float readFloat(int index) {
417                return parseNumber(readAndTrim(index)).floatValue();
418        }
419
420        /*
421         * (non-Javadoc)
422         * 
423         * @see
424         * org.springframework.batch.item.file.mapping.IFieldSet#readFloat(java.
425         * lang.String)
426         */
427    @Override
428        public float readFloat(String name) {
429                return readFloat(indexOf(name));
430        }
431
432        /*
433         * (non-Javadoc)
434         * 
435         * @see
436         * org.springframework.batch.item.file.mapping.IFieldSet#readDouble(int)
437         */
438    @Override
439        public double readDouble(int index) {
440                return parseNumber(readAndTrim(index)).doubleValue();
441        }
442
443        /*
444         * (non-Javadoc)
445         * 
446         * @see
447         * org.springframework.batch.item.file.mapping.IFieldSet#readDouble(java
448         * .lang.String)
449         */
450    @Override
451        public double readDouble(String name) {
452                return readDouble(indexOf(name));
453        }
454
455        /*
456         * (non-Javadoc)
457         * 
458         * @see
459         * org.springframework.batch.item.file.mapping.IFieldSet#readBigDecimal(int)
460         */
461    @Override
462        public BigDecimal readBigDecimal(int index) {
463                return readBigDecimal(index, null);
464        }
465
466        /*
467         * (non-Javadoc)
468         * 
469         * @see
470         * org.springframework.batch.item.file.mapping.IFieldSet#readBigDecimal(
471         * java.lang.String)
472         */
473    @Override
474        public BigDecimal readBigDecimal(String name) {
475                return readBigDecimal(name, null);
476        }
477
478        /*
479         * (non-Javadoc)
480         * 
481         * @see
482         * org.springframework.batch.item.file.mapping.IFieldSet#readBigDecimal(int,
483         * java.math.BigDecimal)
484         */
485    @Override
486        public BigDecimal readBigDecimal(int index, BigDecimal defaultValue) {
487                String candidate = readAndTrim(index);
488
489                if (!StringUtils.hasText(candidate)) {
490                        return defaultValue;
491                }
492
493                try {
494                        String result = removeSeparators(candidate);
495                        return new BigDecimal(result);
496                }
497                catch (NumberFormatException e) {
498                        throw new NumberFormatException("Unparseable number: " + candidate);
499                }
500        }
501
502        private String removeSeparators(String candidate) {
503                return candidate.replace(grouping, "").replace(decimal, ".");
504        }
505
506        /*
507         * (non-Javadoc)
508         * 
509         * @see
510         * org.springframework.batch.item.file.mapping.IFieldSet#readBigDecimal(
511         * java.lang.String, java.math.BigDecimal)
512         */
513    @Override
514        public BigDecimal readBigDecimal(String name, BigDecimal defaultValue) {
515                try {
516                        return readBigDecimal(indexOf(name), defaultValue);
517                }
518                catch (NumberFormatException e) {
519                        throw new NumberFormatException(e.getMessage() + ", name: [" + name + "]");
520                }
521                catch (IllegalArgumentException e) {
522                        throw new IllegalArgumentException(e.getMessage() + ", name: [" + name + "]");
523                }
524        }
525
526        /*
527         * (non-Javadoc)
528         * 
529         * @see org.springframework.batch.item.file.mapping.IFieldSet#readDate(int)
530         */
531    @Override
532        public Date readDate(int index) {
533                return parseDate(readAndTrim(index), dateFormat);
534        }
535
536        /*
537         * (non-Javadoc)
538         * 
539         * @see org.springframework.batch.item.file.transform.FieldSet#readDate(int,
540         * java.util.Date)
541         */
542    @Override
543        public Date readDate(int index, Date defaultValue) {
544                String candidate = readAndTrim(index);
545                return StringUtils.hasText(candidate) ? parseDate(candidate, dateFormat) : defaultValue;
546        }
547
548        /*
549         * (non-Javadoc)
550         * 
551         * @see
552         * org.springframework.batch.item.file.mapping.IFieldSet#readDate(java.lang
553         * .String)
554         */
555    @Override
556        public Date readDate(String name) {
557                try {
558                        return readDate(indexOf(name));
559                }
560                catch (IllegalArgumentException e) {
561                        throw new IllegalArgumentException(e.getMessage() + ", name: [" + name + "]");
562                }
563        }
564
565        /*
566         * (non-Javadoc)
567         * 
568         * @see org.springframework.batch.item.file.transform.FieldSet#readDate(int,
569         * java.util.Date)
570         */
571    @Override
572        public Date readDate(String name, Date defaultValue) {
573                try {
574                        return readDate(indexOf(name), defaultValue);
575                }
576                catch (IllegalArgumentException e) {
577                        throw new IllegalArgumentException(e.getMessage() + ", name: [" + name + "]");
578                }
579        }
580
581        /*
582         * (non-Javadoc)
583         * 
584         * @see org.springframework.batch.item.file.mapping.IFieldSet#readDate(int,
585         * java.lang.String)
586         */
587    @Override
588        public Date readDate(int index, String pattern) {
589                SimpleDateFormat sdf = new SimpleDateFormat(pattern);
590                sdf.setLenient(false);
591                return parseDate(readAndTrim(index), sdf);
592        }
593
594        /*
595         * (non-Javadoc)
596         * 
597         * @see org.springframework.batch.item.file.mapping.IFieldSet#readDate(int,
598         * java.lang.String)
599         */
600    @Override
601        public Date readDate(int index, String pattern, Date defaultValue) {
602                String candidate = readAndTrim(index);
603                return StringUtils.hasText(candidate) ? readDate(index, pattern) : defaultValue;
604        }
605
606        /*
607         * (non-Javadoc)
608         * 
609         * @see
610         * org.springframework.batch.item.file.mapping.IFieldSet#readDate(java.lang
611         * .String, java.lang.String)
612         */
613    @Override
614        public Date readDate(String name, String pattern) {
615                try {
616                        return readDate(indexOf(name), pattern);
617                }
618                catch (IllegalArgumentException e) {
619                        throw new IllegalArgumentException(e.getMessage() + ", name: [" + name + "]");
620                }
621        }
622
623        /*
624         * (non-Javadoc)
625         * 
626         * @see org.springframework.batch.item.file.mapping.IFieldSet#readDate(int,
627         * java.lang.String)
628         */
629    @Override
630        public Date readDate(String name, String pattern, Date defaultValue) {
631                try {
632                        return readDate(indexOf(name), pattern, defaultValue);
633                }
634                catch (IllegalArgumentException e) {
635                        throw new IllegalArgumentException(e.getMessage() + ", name: [" + name + "]");
636                }
637        }
638
639        /*
640         * (non-Javadoc)
641         * 
642         * @see
643         * org.springframework.batch.item.file.mapping.IFieldSet#getFieldCount()
644         */
645    @Override
646        public int getFieldCount() {
647                return tokens.length;
648        }
649
650        /**
651         * Read and trim the {@link String} value at '<code>index</code>'.
652         *
653         * @param index the offset in the token array to obtain the value to be trimmed.
654         * 
655         * @return null if the field value is <code>null</code>.
656         */
657        protected String readAndTrim(int index) {
658                String value = tokens[index];
659
660                if (value != null) {
661                        return value.trim();
662                }
663                else {
664                        return null;
665                }
666        }
667
668        /**
669         * Retrieve the index of where a specified column is located based on the
670         * {@code name} parameter.
671         *
672         * @param name the value to search in the {@link List} of names.
673         * @return the index in the {@link List} of names where the name was found.
674         *
675         * @throws IllegalArgumentException if a column with given name is not
676         * defined.
677         */
678        protected int indexOf(String name) {
679                if (names == null) {
680                        throw new IllegalArgumentException("Cannot access columns by name without meta data");
681                }
682                int index = names.indexOf(name);
683                if (index >= 0) {
684                        return index;
685                }
686                throw new IllegalArgumentException("Cannot access column [" + name + "] from " + names);
687        }
688
689    @Override
690        public String toString() {
691                if (names != null) {
692                        return getProperties().toString();
693                }
694
695                return tokens == null ? "" : Arrays.asList(tokens).toString();
696        }
697
698        /**
699         * @see java.lang.Object#equals(java.lang.Object)
700         */
701    @Override
702        public boolean equals(Object object) {
703                if (object instanceof DefaultFieldSet) {
704                        DefaultFieldSet fs = (DefaultFieldSet) object;
705
706                        if (this.tokens == null) {
707                                return fs.tokens == null;
708                        }
709                        else {
710                                return Arrays.equals(this.tokens, fs.tokens);
711                        }
712                }
713
714                return false;
715        }
716
717    @Override
718        public int hashCode() {
719                // this algorithm was taken from java 1.5 jdk Arrays.hashCode(Object[])
720                if (tokens == null) {
721                        return 0;
722                }
723
724                int result = 1;
725
726                for (String token : tokens) {
727                        result = 31 * result + (token == null ? 0 : token.hashCode());
728                }
729
730                return result;
731        }
732
733        /*
734         * (non-Javadoc)
735         * 
736         * @see
737         * org.springframework.batch.item.file.mapping.IFieldSet#getProperties()
738         */
739    @Override
740        public Properties getProperties() {
741                if (names == null) {
742                        throw new IllegalStateException("Cannot create properties without meta data");
743                }
744                Properties props = new Properties();
745                for (int i = 0; i < tokens.length; i++) {
746                        String value = readAndTrim(i);
747                        if (value != null) {
748                                props.setProperty(names.get(i), value);
749                        }
750                }
751                return props;
752        }
753
754        private Number parseNumber(String candidate) {
755                try {
756                        return numberFormat.parse(candidate);
757                }
758                catch (ParseException e) {
759                        throw new NumberFormatException("Unparseable number: " + candidate);
760                }
761        }
762
763        private Date parseDate(String readAndTrim, DateFormat dateFormat) {
764                try {
765                        return dateFormat.parse(readAndTrim);
766                }
767                catch (ParseException e) {
768                        String pattern;
769                        if (dateFormat instanceof SimpleDateFormat) {
770                                pattern = ((SimpleDateFormat) dateFormat).toPattern();
771                        }
772                        else {
773                                pattern = dateFormat.toString();
774                        }
775                        throw new IllegalArgumentException(e.getMessage() + ", format: [" + pattern + "]");
776                }
777        }
778
779}