View Javadoc

1   /*
2    * Copyright 2006-2007 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.batch.item.file.transform;
18  
19  import java.math.BigDecimal;
20  import java.text.DateFormat;
21  import java.text.DecimalFormat;
22  import java.text.NumberFormat;
23  import java.text.ParseException;
24  import java.text.SimpleDateFormat;
25  import java.util.Arrays;
26  import java.util.Date;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Properties;
30  
31  import org.springframework.util.Assert;
32  import org.springframework.util.StringUtils;
33  
34  /**
35   * Default implementation of {@link FieldSet} using Java using Java primitive
36   * and standard types and utilities. Strings are trimmed before parsing by
37   * default, and so are plain String values.
38   * 
39   * @author Rob Harrop
40   * @author Dave Syer
41   */
42  public class DefaultFieldSet implements FieldSet {
43  
44  	private final static String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";
45  
46  	private DateFormat dateFormat = new SimpleDateFormat(DEFAULT_DATE_PATTERN);
47  	{
48  		dateFormat.setLenient(false);
49  	}
50  
51  	private NumberFormat numberFormat = NumberFormat.getInstance(Locale.US);
52  
53  	private String grouping = ",";
54  
55  	private String decimal = ".";
56  
57  	/**
58  	 * The fields wrapped by this '<code>FieldSet</code>' instance.
59  	 */
60  	private String[] tokens;
61  
62  	private List<String> names;
63  
64  	/**
65  	 * The {@link NumberFormat} to use for parsing numbers. If unset the US
66  	 * locale will be used ('.' as decimal place).
67  	 * @param numberFormat the {@link NumberFormat} to use for number parsing
68  	 */
69  	public final void setNumberFormat(NumberFormat numberFormat) {
70  		this.numberFormat = numberFormat;
71  		if (numberFormat instanceof DecimalFormat) {
72  			grouping = "" + ((DecimalFormat) numberFormat).getDecimalFormatSymbols().getGroupingSeparator();
73  			decimal = "" + ((DecimalFormat) numberFormat).getDecimalFormatSymbols().getDecimalSeparator();
74  		}
75  	}
76  
77  	/**
78  	 * The {@link DateFormat} to use for parsing numbers. If unset the default
79  	 * pattern is ISO standard <code>yyyy/MM/dd</code>.
80  	 * @param dateFormat the {@link DateFormat} to use for date parsing
81  	 */
82  	public void setDateFormat(DateFormat dateFormat) {
83  		this.dateFormat = dateFormat;
84  	}
85  
86  	/**
87  	 * Create a FieldSet with anonymous tokens. They can only be retrieved by
88  	 * column number.
89  	 * @param tokens the token values
90  	 * @see FieldSet#readString(int)
91  	 */
92  	public DefaultFieldSet(String[] tokens) {
93  		this.tokens = tokens == null ? null : (String[]) tokens.clone();
94  		setNumberFormat(NumberFormat.getInstance(Locale.US));
95  	}
96  
97  	/**
98  	 * Create a FieldSet with named tokens. The token values can then be
99  	 * 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);
106 		Assert.notNull(names);
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 = (String[]) 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) ? true : false;
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 (Double) 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 		try {
545 			return readDate(index);
546 		}
547 		catch (IllegalArgumentException e) {
548 			return defaultValue;
549 		}
550 	}
551 
552 	/*
553 	 * (non-Javadoc)
554 	 * 
555 	 * @see
556 	 * org.springframework.batch.item.file.mapping.IFieldSet#readDate(java.lang
557 	 * .String)
558 	 */
559     @Override
560 	public Date readDate(String name) {
561 		try {
562 			return readDate(indexOf(name));
563 		}
564 		catch (IllegalArgumentException e) {
565 			throw new IllegalArgumentException(e.getMessage() + ", name: [" + name + "]");
566 		}
567 	}
568 
569 	/*
570 	 * (non-Javadoc)
571 	 * 
572 	 * @see org.springframework.batch.item.file.transform.FieldSet#readDate(int,
573 	 * java.util.Date)
574 	 */
575     @Override
576 	public Date readDate(String name, Date defaultValue) {
577 		try {
578 			return readDate(name);
579 		}
580 		catch (IllegalArgumentException e) {
581 			return defaultValue;
582 		}
583 	}
584 
585 	/*
586 	 * (non-Javadoc)
587 	 * 
588 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#readDate(int,
589 	 * java.lang.String)
590 	 */
591     @Override
592 	public Date readDate(int index, String pattern) {
593 		SimpleDateFormat sdf = new SimpleDateFormat(pattern);
594 		sdf.setLenient(false);
595 		return parseDate(readAndTrim(index), sdf);
596 	}
597 
598 	/*
599 	 * (non-Javadoc)
600 	 * 
601 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#readDate(int,
602 	 * java.lang.String)
603 	 */
604     @Override
605 	public Date readDate(int index, String pattern, Date defaultValue) {
606 		try {
607 			return readDate(index, pattern);
608 		}
609 		catch (IllegalArgumentException e) {
610 			return defaultValue;
611 		}
612 	}
613 
614 	/*
615 	 * (non-Javadoc)
616 	 * 
617 	 * @see
618 	 * org.springframework.batch.item.file.mapping.IFieldSet#readDate(java.lang
619 	 * .String, java.lang.String)
620 	 */
621     @Override
622 	public Date readDate(String name, String pattern) {
623 		try {
624 			return readDate(indexOf(name), pattern);
625 		}
626 		catch (IllegalArgumentException e) {
627 			throw new IllegalArgumentException(e.getMessage() + ", name: [" + name + "]");
628 		}
629 	}
630 
631 	/*
632 	 * (non-Javadoc)
633 	 * 
634 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#readDate(int,
635 	 * java.lang.String)
636 	 */
637     @Override
638 	public Date readDate(String name, String pattern, Date defaultValue) {
639 		try {
640 			return readDate(name, pattern);
641 		}
642 		catch (IllegalArgumentException e) {
643 			return defaultValue;
644 		}
645 	}
646 
647 	/*
648 	 * (non-Javadoc)
649 	 * 
650 	 * @see
651 	 * org.springframework.batch.item.file.mapping.IFieldSet#getFieldCount()
652 	 */
653     @Override
654 	public int getFieldCount() {
655 		return tokens.length;
656 	}
657 
658 	/**
659 	 * Read and trim the {@link String} value at '<code>index</code>'.
660 	 * 
661 	 * @returns null if the field value is <code>null</code>.
662 	 */
663 	protected String readAndTrim(int index) {
664 		String value = tokens[index];
665 
666 		if (value != null) {
667 			return value.trim();
668 		}
669 		else {
670 			return null;
671 		}
672 	}
673 
674 	/**
675 	 * Read and trim the {@link String} value from column with given '
676 	 * <code>name</code>.
677 	 * 
678 	 * @throws IllegalArgumentException if a column with given name is not
679 	 * defined.
680 	 */
681 	protected int indexOf(String name) {
682 		if (names == null) {
683 			throw new IllegalArgumentException("Cannot access columns by name without meta data");
684 		}
685 		int index = names.indexOf(name);
686 		if (index >= 0) {
687 			return index;
688 		}
689 		throw new IllegalArgumentException("Cannot access column [" + name + "] from " + names);
690 	}
691 
692     @Override
693 	public String toString() {
694 		if (names != null) {
695 			return getProperties().toString();
696 		}
697 
698 		return tokens == null ? "" : Arrays.asList(tokens).toString();
699 	}
700 
701 	/**
702 	 * @see java.lang.Object#equals(java.lang.Object)
703 	 */
704     @Override
705 	public boolean equals(Object object) {
706 		if (object instanceof DefaultFieldSet) {
707 			DefaultFieldSet fs = (DefaultFieldSet) object;
708 
709 			if (this.tokens == null) {
710 				return fs.tokens == null;
711 			}
712 			else {
713 				return Arrays.equals(this.tokens, fs.tokens);
714 			}
715 		}
716 
717 		return false;
718 	}
719 
720     @Override
721 	public int hashCode() {
722 		// this algorithm was taken from java 1.5 jdk Arrays.hashCode(Object[])
723 		if (tokens == null) {
724 			return 0;
725 		}
726 
727 		int result = 1;
728 
729 		for (int i = 0; i < tokens.length; i++) {
730 			result = 31 * result + (tokens[i] == null ? 0 : tokens[i].hashCode());
731 		}
732 
733 		return result;
734 	}
735 
736 	/*
737 	 * (non-Javadoc)
738 	 * 
739 	 * @see
740 	 * org.springframework.batch.item.file.mapping.IFieldSet#getProperties()
741 	 */
742     @Override
743 	public Properties getProperties() {
744 		if (names == null) {
745 			throw new IllegalStateException("Cannot create properties without meta data");
746 		}
747 		Properties props = new Properties();
748 		for (int i = 0; i < tokens.length; i++) {
749 			String value = readAndTrim(i);
750 			if (value != null) {
751 				props.setProperty((String) names.get(i), value);
752 			}
753 		}
754 		return props;
755 	}
756 
757 	private Number parseNumber(String candidate) {
758 		try {
759 			return numberFormat.parse(candidate);
760 		}
761 		catch (ParseException e) {
762 			throw new NumberFormatException("Unparseable number: " + candidate);
763 		}
764 	}
765 
766 	private Date parseDate(String readAndTrim, DateFormat dateFormat) {
767 		try {
768 			return dateFormat.parse(readAndTrim);
769 		}
770 		catch (ParseException e) {
771 			String pattern;
772 			if (dateFormat instanceof SimpleDateFormat) {
773 				pattern = ((SimpleDateFormat) dateFormat).toPattern();
774 			}
775 			else {
776 				pattern = dateFormat.toString();
777 			}
778 			throw new IllegalArgumentException(e.getMessage() + ", format: [" + pattern + "]");
779 		}
780 	}
781 
782 }