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}