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 	public String[] getNames() {
122 		if (names == null) {
123 			throw new IllegalStateException("Field names are not known");
124 		}
125 		return names.toArray(new String[names.size()]);
126 	}
127 
128 	/*
129 	 * (non-Javadoc)
130 	 * 
131 	 * @see org.springframework.batch.item.file.mapping.FieldSet#hasNames()
132 	 */
133 	public boolean hasNames() {
134 		return names != null;
135 	}
136 
137 	/*
138 	 * (non-Javadoc)
139 	 * 
140 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#getValues()
141 	 */
142 	public String[] getValues() {
143 		return tokens.clone();
144 	}
145 
146 	/*
147 	 * (non-Javadoc)
148 	 * 
149 	 * @see
150 	 * org.springframework.batch.item.file.mapping.IFieldSet#readString(int)
151 	 */
152 	public String readString(int index) {
153 		return readAndTrim(index);
154 	}
155 
156 	/*
157 	 * (non-Javadoc)
158 	 * 
159 	 * @see
160 	 * org.springframework.batch.item.file.mapping.IFieldSet#readString(java
161 	 * .lang.String)
162 	 */
163 	public String readString(String name) {
164 		return readString(indexOf(name));
165 	}
166 
167 	/*
168 	 * (non-Javadoc)
169 	 * 
170 	 * @see
171 	 * org.springframework.batch.item.file.mapping.IFieldSet#readRawString(int)
172 	 */
173 	public String readRawString(int index) {
174 		return tokens[index];
175 	}
176 
177 	/*
178 	 * (non-Javadoc)
179 	 * 
180 	 * @see
181 	 * org.springframework.batch.item.file.mapping.IFieldSet#readRawString(java
182 	 * .lang.String)
183 	 */
184 	public String readRawString(String name) {
185 		return readRawString(indexOf(name));
186 	}
187 
188 	/*
189 	 * (non-Javadoc)
190 	 * 
191 	 * @see
192 	 * org.springframework.batch.item.file.mapping.IFieldSet#readBoolean(int)
193 	 */
194 	public boolean readBoolean(int index) {
195 		return readBoolean(index, "true");
196 	}
197 
198 	/*
199 	 * (non-Javadoc)
200 	 * 
201 	 * @see
202 	 * org.springframework.batch.item.file.mapping.IFieldSet#readBoolean(java
203 	 * .lang.String)
204 	 */
205 	public boolean readBoolean(String name) {
206 		return readBoolean(indexOf(name));
207 	}
208 
209 	/*
210 	 * (non-Javadoc)
211 	 * 
212 	 * @see
213 	 * org.springframework.batch.item.file.mapping.IFieldSet#readBoolean(int,
214 	 * java.lang.String)
215 	 */
216 	public boolean readBoolean(int index, String trueValue) {
217 		Assert.notNull(trueValue, "'trueValue' cannot be null.");
218 
219 		String value = readAndTrim(index);
220 
221 		return trueValue.equals(value) ? true : false;
222 	}
223 
224 	/*
225 	 * (non-Javadoc)
226 	 * 
227 	 * @see
228 	 * org.springframework.batch.item.file.mapping.IFieldSet#readBoolean(java
229 	 * .lang.String, java.lang.String)
230 	 */
231 	public boolean readBoolean(String name, String trueValue) {
232 		return readBoolean(indexOf(name), trueValue);
233 	}
234 
235 	/*
236 	 * (non-Javadoc)
237 	 * 
238 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#readChar(int)
239 	 */
240 	public char readChar(int index) {
241 		String value = readAndTrim(index);
242 
243 		Assert.isTrue(value.length() == 1, "Cannot convert field value '" + value + "' to char.");
244 
245 		return value.charAt(0);
246 	}
247 
248 	/*
249 	 * (non-Javadoc)
250 	 * 
251 	 * @see
252 	 * org.springframework.batch.item.file.mapping.IFieldSet#readChar(java.lang
253 	 * .String)
254 	 */
255 	public char readChar(String name) {
256 		return readChar(indexOf(name));
257 	}
258 
259 	/*
260 	 * (non-Javadoc)
261 	 * 
262 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#readByte(int)
263 	 */
264 	public byte readByte(int index) {
265 		return Byte.parseByte(readAndTrim(index));
266 	}
267 
268 	/*
269 	 * (non-Javadoc)
270 	 * 
271 	 * @see
272 	 * org.springframework.batch.item.file.mapping.IFieldSet#readByte(java.lang
273 	 * .String)
274 	 */
275 	public byte readByte(String name) {
276 		return readByte(indexOf(name));
277 	}
278 
279 	/*
280 	 * (non-Javadoc)
281 	 * 
282 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#readShort(int)
283 	 */
284 	public short readShort(int index) {
285 		return Short.parseShort(readAndTrim(index));
286 	}
287 
288 	/*
289 	 * (non-Javadoc)
290 	 * 
291 	 * @see
292 	 * org.springframework.batch.item.file.mapping.IFieldSet#readShort(java.
293 	 * lang.String)
294 	 */
295 	public short readShort(String name) {
296 		return readShort(indexOf(name));
297 	}
298 
299 	/*
300 	 * (non-Javadoc)
301 	 * 
302 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#readInt(int)
303 	 */
304 	public int readInt(int index) {
305 		return parseNumber(readAndTrim(index)).intValue();
306 	}
307 
308 	/*
309 	 * (non-Javadoc)
310 	 * 
311 	 * @see
312 	 * org.springframework.batch.item.file.mapping.IFieldSet#readInt(java.lang
313 	 * .String)
314 	 */
315 	public int readInt(String name) {
316 		return readInt(indexOf(name));
317 	}
318 
319 	/*
320 	 * (non-Javadoc)
321 	 * 
322 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#readInt(int,
323 	 * int)
324 	 */
325 	public int readInt(int index, int defaultValue) {
326 		String value = readAndTrim(index);
327 
328 		return StringUtils.hasLength(value) ? Integer.parseInt(value) : defaultValue;
329 	}
330 
331 	/*
332 	 * (non-Javadoc)
333 	 * 
334 	 * @see
335 	 * org.springframework.batch.item.file.mapping.IFieldSet#readInt(java.lang
336 	 * .String, int)
337 	 */
338 	public int readInt(String name, int defaultValue) {
339 		return readInt(indexOf(name), defaultValue);
340 	}
341 
342 	/*
343 	 * (non-Javadoc)
344 	 * 
345 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#readLong(int)
346 	 */
347 	public long readLong(int index) {
348 		return parseNumber(readAndTrim(index)).longValue();
349 	}
350 
351 	/*
352 	 * (non-Javadoc)
353 	 * 
354 	 * @see
355 	 * org.springframework.batch.item.file.mapping.IFieldSet#readLong(java.lang
356 	 * .String)
357 	 */
358 	public long readLong(String name) {
359 		return readLong(indexOf(name));
360 	}
361 
362 	/*
363 	 * (non-Javadoc)
364 	 * 
365 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#readLong(int,
366 	 * long)
367 	 */
368 	public long readLong(int index, long defaultValue) {
369 		String value = readAndTrim(index);
370 
371 		return StringUtils.hasLength(value) ? Long.parseLong(value) : defaultValue;
372 	}
373 
374 	/*
375 	 * (non-Javadoc)
376 	 * 
377 	 * @see
378 	 * org.springframework.batch.item.file.mapping.IFieldSet#readLong(java.lang
379 	 * .String, long)
380 	 */
381 	public long readLong(String name, long defaultValue) {
382 		return readLong(indexOf(name), defaultValue);
383 	}
384 
385 	/*
386 	 * (non-Javadoc)
387 	 * 
388 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#readFloat(int)
389 	 */
390 	public float readFloat(int index) {
391 		return parseNumber(readAndTrim(index)).floatValue();
392 	}
393 
394 	/*
395 	 * (non-Javadoc)
396 	 * 
397 	 * @see
398 	 * org.springframework.batch.item.file.mapping.IFieldSet#readFloat(java.
399 	 * lang.String)
400 	 */
401 	public float readFloat(String name) {
402 		return readFloat(indexOf(name));
403 	}
404 
405 	/*
406 	 * (non-Javadoc)
407 	 * 
408 	 * @see
409 	 * org.springframework.batch.item.file.mapping.IFieldSet#readDouble(int)
410 	 */
411 	public double readDouble(int index) {
412 		return (Double) parseNumber(readAndTrim(index)).doubleValue();
413 	}
414 
415 	/*
416 	 * (non-Javadoc)
417 	 * 
418 	 * @see
419 	 * org.springframework.batch.item.file.mapping.IFieldSet#readDouble(java
420 	 * .lang.String)
421 	 */
422 	public double readDouble(String name) {
423 		return readDouble(indexOf(name));
424 	}
425 
426 	/*
427 	 * (non-Javadoc)
428 	 * 
429 	 * @see
430 	 * org.springframework.batch.item.file.mapping.IFieldSet#readBigDecimal(int)
431 	 */
432 	public BigDecimal readBigDecimal(int index) {
433 		return readBigDecimal(index, null);
434 	}
435 
436 	/*
437 	 * (non-Javadoc)
438 	 * 
439 	 * @see
440 	 * org.springframework.batch.item.file.mapping.IFieldSet#readBigDecimal(
441 	 * java.lang.String)
442 	 */
443 	public BigDecimal readBigDecimal(String name) {
444 		return readBigDecimal(name, null);
445 	}
446 
447 	/*
448 	 * (non-Javadoc)
449 	 * 
450 	 * @see
451 	 * org.springframework.batch.item.file.mapping.IFieldSet#readBigDecimal(int,
452 	 * java.math.BigDecimal)
453 	 */
454 	public BigDecimal readBigDecimal(int index, BigDecimal defaultValue) {
455 		String candidate = readAndTrim(index);
456 
457 		if (!StringUtils.hasText(candidate)) {
458 			return defaultValue;
459 		}
460 
461 		try {
462 			String result = removeSeparators(candidate);
463 			return new BigDecimal(result);
464 		}
465 		catch (NumberFormatException e) {
466 			throw new NumberFormatException("Unparseable number: " + candidate);
467 		}
468 	}
469 
470 	private String removeSeparators(String candidate) {
471 		return candidate.replace(grouping, "").replace(decimal, ".");
472 	}
473 
474 	/*
475 	 * (non-Javadoc)
476 	 * 
477 	 * @see
478 	 * org.springframework.batch.item.file.mapping.IFieldSet#readBigDecimal(
479 	 * java.lang.String, java.math.BigDecimal)
480 	 */
481 	public BigDecimal readBigDecimal(String name, BigDecimal defaultValue) {
482 		try {
483 			return readBigDecimal(indexOf(name), defaultValue);
484 		}
485 		catch (NumberFormatException e) {
486 			throw new NumberFormatException(e.getMessage() + ", name: [" + name + "]");
487 		}
488 		catch (IllegalArgumentException e) {
489 			throw new IllegalArgumentException(e.getMessage() + ", name: [" + name + "]");
490 		}
491 	}
492 
493 	/*
494 	 * (non-Javadoc)
495 	 * 
496 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#readDate(int)
497 	 */
498 	public Date readDate(int index) {
499 		return parseDate(readAndTrim(index), dateFormat);
500 	}
501 
502 	/*
503 	 * (non-Javadoc)
504 	 * 
505 	 * @see org.springframework.batch.item.file.transform.FieldSet#readDate(int,
506 	 * java.util.Date)
507 	 */
508 	public Date readDate(int index, Date defaultValue) {
509 		try {
510 			return readDate(index);
511 		}
512 		catch (IllegalArgumentException e) {
513 			return defaultValue;
514 		}
515 	}
516 
517 	/*
518 	 * (non-Javadoc)
519 	 * 
520 	 * @see
521 	 * org.springframework.batch.item.file.mapping.IFieldSet#readDate(java.lang
522 	 * .String)
523 	 */
524 	public Date readDate(String name) {
525 		try {
526 			return readDate(indexOf(name));
527 		}
528 		catch (IllegalArgumentException e) {
529 			throw new IllegalArgumentException(e.getMessage() + ", name: [" + name + "]");
530 		}
531 	}
532 
533 	/*
534 	 * (non-Javadoc)
535 	 * 
536 	 * @see org.springframework.batch.item.file.transform.FieldSet#readDate(int,
537 	 * java.util.Date)
538 	 */
539 	public Date readDate(String name, Date defaultValue) {
540 		try {
541 			return readDate(name);
542 		}
543 		catch (IllegalArgumentException e) {
544 			return defaultValue;
545 		}
546 	}
547 
548 	/*
549 	 * (non-Javadoc)
550 	 * 
551 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#readDate(int,
552 	 * java.lang.String)
553 	 */
554 	public Date readDate(int index, String pattern) {
555 		SimpleDateFormat sdf = new SimpleDateFormat(pattern);
556 		sdf.setLenient(false);
557 		return parseDate(readAndTrim(index), sdf);
558 	}
559 
560 	/*
561 	 * (non-Javadoc)
562 	 * 
563 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#readDate(int,
564 	 * java.lang.String)
565 	 */
566 	public Date readDate(int index, String pattern, Date defaultValue) {
567 		try {
568 			return readDate(index, pattern);
569 		}
570 		catch (IllegalArgumentException e) {
571 			return defaultValue;
572 		}
573 	}
574 
575 	/*
576 	 * (non-Javadoc)
577 	 * 
578 	 * @see
579 	 * org.springframework.batch.item.file.mapping.IFieldSet#readDate(java.lang
580 	 * .String, java.lang.String)
581 	 */
582 	public Date readDate(String name, String pattern) {
583 		try {
584 			return readDate(indexOf(name), pattern);
585 		}
586 		catch (IllegalArgumentException e) {
587 			throw new IllegalArgumentException(e.getMessage() + ", name: [" + name + "]");
588 		}
589 	}
590 
591 	/*
592 	 * (non-Javadoc)
593 	 * 
594 	 * @see org.springframework.batch.item.file.mapping.IFieldSet#readDate(int,
595 	 * java.lang.String)
596 	 */
597 	public Date readDate(String name, String pattern, Date defaultValue) {
598 		try {
599 			return readDate(name, pattern);
600 		}
601 		catch (IllegalArgumentException e) {
602 			return defaultValue;
603 		}
604 	}
605 
606 	/*
607 	 * (non-Javadoc)
608 	 * 
609 	 * @see
610 	 * org.springframework.batch.item.file.mapping.IFieldSet#getFieldCount()
611 	 */
612 	public int getFieldCount() {
613 		return tokens.length;
614 	}
615 
616 	/**
617 	 * Read and trim the {@link String} value at '<code>index</code>'.
618 	 * 
619 	 * @returns null if the field value is <code>null</code>.
620 	 */
621 	protected String readAndTrim(int index) {
622 		String value = tokens[index];
623 
624 		if (value != null) {
625 			return value.trim();
626 		}
627 		else {
628 			return null;
629 		}
630 	}
631 
632 	/**
633 	 * Read and trim the {@link String} value from column with given '
634 	 * <code>name</code>.
635 	 * 
636 	 * @throws IllegalArgumentException if a column with given name is not
637 	 * defined.
638 	 */
639 	protected int indexOf(String name) {
640 		if (names == null) {
641 			throw new IllegalArgumentException("Cannot access columns by name without meta data");
642 		}
643 		int index = names.indexOf(name);
644 		if (index >= 0) {
645 			return index;
646 		}
647 		throw new IllegalArgumentException("Cannot access column [" + name + "] from " + names);
648 	}
649 
650 	public String toString() {
651 		if (names != null) {
652 			return getProperties().toString();
653 		}
654 
655 		return tokens == null ? "" : Arrays.asList(tokens).toString();
656 	}
657 
658 	/**
659 	 * @see java.lang.Object#equals(java.lang.Object)
660 	 */
661 	public boolean equals(Object object) {
662 		if (object instanceof DefaultFieldSet) {
663 			DefaultFieldSet fs = (DefaultFieldSet) object;
664 
665 			if (this.tokens == null) {
666 				return fs.tokens == null;
667 			}
668 			else {
669 				return Arrays.equals(this.tokens, fs.tokens);
670 			}
671 		}
672 
673 		return false;
674 	}
675 
676 	public int hashCode() {
677 		// this algorithm was taken from java 1.5 jdk Arrays.hashCode(Object[])
678 		if (tokens == null) {
679 			return 0;
680 		}
681 
682 		int result = 1;
683 
684 		for (int i = 0; i < tokens.length; i++) {
685 			result = 31 * result + (tokens[i] == null ? 0 : tokens[i].hashCode());
686 		}
687 
688 		return result;
689 	}
690 
691 	/*
692 	 * (non-Javadoc)
693 	 * 
694 	 * @see
695 	 * org.springframework.batch.item.file.mapping.IFieldSet#getProperties()
696 	 */
697 	public Properties getProperties() {
698 		if (names == null) {
699 			throw new IllegalStateException("Cannot create properties without meta data");
700 		}
701 		Properties props = new Properties();
702 		for (int i = 0; i < tokens.length; i++) {
703 			String value = readAndTrim(i);
704 			if (value != null) {
705 				props.setProperty((String) names.get(i), value);
706 			}
707 		}
708 		return props;
709 	}
710 
711 	private Number parseNumber(String candidate) {
712 		try {
713 			return numberFormat.parse(candidate);
714 		}
715 		catch (ParseException e) {
716 			throw new NumberFormatException("Unparseable number: " + candidate);
717 		}
718 	}
719 
720 	private Date parseDate(String readAndTrim, DateFormat dateFormat) {
721 		try {
722 			return dateFormat.parse(readAndTrim);
723 		}
724 		catch (ParseException e) {
725 			String pattern;
726 			if (dateFormat instanceof SimpleDateFormat) {
727 				pattern = ((SimpleDateFormat) dateFormat).toPattern();
728 			}
729 			else {
730 				pattern = dateFormat.toString();
731 			}
732 			throw new IllegalArgumentException(e.getMessage() + ", format: [" + pattern + "]");
733 		}
734 	}
735 
736 }