View Javadoc
1   /*
2    *  jDTAUS Banking Utilities
3    *  Copyright (C) 2005 Christian Schulte
4    *  <cs@schulte.it>
5    *
6    *  This library is free software; you can redistribute it and/or
7    *  modify it under the terms of the GNU Lesser General Public
8    *  License as published by the Free Software Foundation; either
9    *  version 2.1 of the License, or any later version.
10   *
11   *  This library is distributed in the hope that it will be useful,
12   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14   *  Lesser General Public License for more details.
15   *
16   *  You should have received a copy of the GNU Lesser General Public
17   *  License along with this library; if not, write to the Free Software
18   *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19   *
20   */
21  package org.jdtaus.banking.util;
22  
23  import java.io.IOException;
24  import java.io.InputStreamReader;
25  import java.io.LineNumberReader;
26  import java.io.UnsupportedEncodingException;
27  import java.net.URL;
28  import java.text.DecimalFormat;
29  import java.text.NumberFormat;
30  import java.text.ParseException;
31  import java.util.ArrayList;
32  import java.util.Date;
33  import java.util.HashMap;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.Map;
38  import org.jdtaus.banking.Bankleitzahl;
39  import org.jdtaus.banking.BankleitzahlInfo;
40  import org.jdtaus.banking.messages.UpdatesBankleitzahlenDateiMessage;
41  import org.jdtaus.core.container.ContainerFactory;
42  import org.jdtaus.core.container.PropertyException;
43  import org.jdtaus.core.logging.spi.Logger;
44  import org.jdtaus.core.monitor.spi.Task;
45  import org.jdtaus.core.monitor.spi.TaskMonitor;
46  
47  /**
48   * German Bankleitzahlendatei for the format as of 2006-06-01.
49   * <p>For further information see the
50   * <a href="../../../../doc-files/merkblatt_bankleitzahlendatei.pdf">Merkblatt Bankleitzahlendatei</a>.
51   * An updated version of the document may be found at
52   * <a href="http://www.bundesbank.de">Deutsche Bundesbank</a>.
53   * </p>
54   *
55   * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
56   * @version $JDTAUS: BankleitzahlenDatei.java 8861 2014-01-10 17:09:50Z schulte $
57   */
58  public final class BankleitzahlenDatei
59  {
60      //--Dependencies------------------------------------------------------------
61  
62  // <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausDependencies
63      // This section is managed by jdtaus-container-mojo.
64  
65      /**
66       * Gets the configured <code>Logger</code> implementation.
67       *
68       * @return The configured <code>Logger</code> implementation.
69       */
70      private Logger getLogger()
71      {
72          return (Logger) ContainerFactory.getContainer().
73              getDependency( this, "Logger" );
74  
75      }
76  
77      /**
78       * Gets the configured <code>TaskMonitor</code> implementation.
79       *
80       * @return The configured <code>TaskMonitor</code> implementation.
81       */
82      private TaskMonitor getTaskMonitor()
83      {
84          return (TaskMonitor) ContainerFactory.getContainer().
85              getDependency( this, "TaskMonitor" );
86  
87      }
88  
89      /**
90       * Gets the configured <code>Locale</code> implementation.
91       *
92       * @return The configured <code>Locale</code> implementation.
93       */
94      private Locale getLocale()
95      {
96          return (Locale) ContainerFactory.getContainer().
97              getDependency( this, "Locale" );
98  
99      }
100 
101 // </editor-fold>//GEN-END:jdtausDependencies
102 
103     //------------------------------------------------------------Dependencies--
104     //--Properties--------------------------------------------------------------
105 
106 // <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausProperties
107     // This section is managed by jdtaus-container-mojo.
108 
109     /**
110      * Gets the value of property <code>defaultEncoding</code>.
111      *
112      * @return Default encoding to use when reading bankfile resources.
113      */
114     private java.lang.String getDefaultEncoding()
115     {
116         return (java.lang.String) ContainerFactory.getContainer().
117             getProperty( this, "defaultEncoding" );
118 
119     }
120 
121 // </editor-fold>//GEN-END:jdtausProperties
122 
123     //--------------------------------------------------------------Properties--
124     //--BankleitzahlenDatei-----------------------------------------------------
125 
126     /**
127      * Empty {@code BankleitzahlInfo} array.
128      * @since 1.15
129      */
130     private static final BankleitzahlInfo[] NO_RECORDS =
131     {
132     };
133 
134     /**
135      * Constant for the format as of june 2006.
136      * @since 1.15
137      */
138     public static final int JUNE_2006_FORMAT = 20060601;
139 
140     /**
141      * Constant for the format as of june 2013.
142      * @since 1.15
143      */
144     public static final int JUNE_2013_FORMAT = 20130601;
145 
146     /**
147      * index = index of the field in the record line; value = offset the field's value starts in the record line.
148      */
149     private static final int[] FIELD_TO_OFFSET =
150     {
151         0, 8, 9, 67, 72, 107, 134, 139, 150, 152, 158, 159, 160, 168, 172
152     };
153 
154     /**
155      * index = index of the field in the record line; value = length of the field's value in the record line.
156      */
157     private static final int[] FIELD_TO_LENGTH =
158     {
159         8, 1, 58, 5, 35, 27, 5, 11, 2, 6, 1, 1, 8, 4, 2
160     };
161 
162     /**
163      * index = index of the field in the record line; value = end offset in the record line exclusive.
164      */
165     private static final int[] FIELD_TO_ENDOFFSET =
166     {
167         FIELD_TO_OFFSET[0] + FIELD_TO_LENGTH[0],
168         FIELD_TO_OFFSET[1] + FIELD_TO_LENGTH[1],
169         FIELD_TO_OFFSET[2] + FIELD_TO_LENGTH[2],
170         FIELD_TO_OFFSET[3] + FIELD_TO_LENGTH[3],
171         FIELD_TO_OFFSET[4] + FIELD_TO_LENGTH[4],
172         FIELD_TO_OFFSET[5] + FIELD_TO_LENGTH[5],
173         FIELD_TO_OFFSET[6] + FIELD_TO_LENGTH[6],
174         FIELD_TO_OFFSET[7] + FIELD_TO_LENGTH[7],
175         FIELD_TO_OFFSET[8] + FIELD_TO_LENGTH[8],
176         FIELD_TO_OFFSET[9] + FIELD_TO_LENGTH[9],
177         FIELD_TO_OFFSET[10] + FIELD_TO_LENGTH[10],
178         FIELD_TO_OFFSET[11] + FIELD_TO_LENGTH[11],
179         FIELD_TO_OFFSET[12] + FIELD_TO_LENGTH[12],
180         FIELD_TO_OFFSET[13] + FIELD_TO_LENGTH[13],
181         FIELD_TO_OFFSET[14] + FIELD_TO_LENGTH[14]
182     };
183 
184     /** Records held by the instance. */
185     private Map records = new HashMap( 5000 );
186     private Map deletedRecords = new HashMap( 5000 );
187     private Map headOffices = new HashMap( 5000 );
188     private Map branchOffices = new HashMap( 5000 );
189     private Map deletedHeadOffices = new HashMap( 5000 );
190     private Map deletedBranchOffices = new HashMap( 5000 );
191     private BankleitzahlInfo[] cachedRecords;
192     private BankleitzahlInfo[] cachedDeletedRecords;
193 
194     /** Encoding to use when reading bankfile resources. */
195     private String encoding;
196 
197     /**
198      * Format of the file backing the instance.
199      * @since 1.15
200      */
201     private int format;
202 
203     /**
204      * The date of validity of the file.
205      * @since 1.15
206      */
207     private Date dateOfValidity;
208 
209     /**
210      * The date of expiration of the file.
211      * @since 1.15
212      */
213     private Date dateOfExpiration;
214 
215     /**
216      * Reads a Bankleitzahlendatei form an URL initializing the instance to hold its data.
217      * <p>Calling this constructor is the same as calling<blockquote><pre>
218      * new BankleitzahlenDatei( url, JUNE_2006_FORMAT );
219      * </pre></blockquote></p>
220      *
221      * @param resource An URL to a Bankleitzahlendatei.
222      *
223      * @throws NullPointerException if {@code resource} is {@code null}.
224      * @throws PropertyException for invalid property values.
225      * @throws IllegalArgumentException if {@code resource} does not provide a valid Bankleitzahlendatei.
226      * @throws IOException if reading fails.
227      *
228      * @deprecated As of 1.15, replaced by constructor
229      * {@link BankleitzahlenDatei#BankleitzahlenDatei(java.net.URL, int, java.util.Date, java.util.Date)}.
230      */
231     public BankleitzahlenDatei( final URL resource ) throws IOException
232     {
233         super();
234 
235         if ( resource == null )
236         {
237             throw new NullPointerException( "resource" );
238         }
239 
240         this.assertValidProperties();
241         this.format = JUNE_2006_FORMAT;
242         this.dateOfValidity = null;
243         this.dateOfExpiration = null;
244         this.readBankfile( resource );
245     }
246 
247     /**
248      * Reads a Bankleitzahlendatei form an URL initializing the instance to hold its data taking a format constant.
249      *
250      * @param resource An URL to a Bankleitzahlendatei.
251      * @param format The format of the file to parse.
252      * @param dateOfValidity The date of validity of the file.
253      * @param dateOfExpiration The date of expiration of the file.
254      *
255      * @throws NullPointerException if {@code resource}, {@code dateOfValidity} or {@code dateOfExpiration} is
256      * {@code null}.
257      * @throws PropertyException for invalid property values.
258      * @throws IllegalArgumentException if {@code resource} does not provide a valid Bankleitzahlendatei or if
259      * {@code format} does not equal one of the format constants defined in this class.
260      * @throws IOException if reading fails.
261      *
262      * @see #JUNE_2006_FORMAT
263      * @see #JUNE_2013_FORMAT
264      */
265     public BankleitzahlenDatei( final URL resource, final int format, final Date dateOfValidity,
266                                 final Date dateOfExpiration ) throws IOException
267     {
268         super();
269 
270         if ( resource == null )
271         {
272             throw new NullPointerException( "resource" );
273         }
274         if ( dateOfValidity == null )
275         {
276             throw new NullPointerException( "dateOfValidity" );
277         }
278         if ( dateOfExpiration == null )
279         {
280             throw new NullPointerException( "dateOfExpiration" );
281         }
282 
283         assertValidFormat( format );
284         this.assertValidProperties();
285         this.format = format;
286         this.dateOfValidity = (Date) dateOfValidity.clone();
287         this.dateOfExpiration = (Date) dateOfExpiration.clone();
288         this.readBankfile( resource );
289     }
290 
291     /**
292      * Reads a Bankleitzahlendatei form an URL initializing the instance to hold its data taking the encoding to use
293      * when reading the file.
294      * <p>Calling this constructor is the same as calling<blockquote><pre>
295      * new BankleitzahlenDatei( url, encoding, JUNE_2006_FORMAT );
296      * </pre></blockquote></p>
297      *
298      * @param resource An URL to a Bankleitzahlendatei.
299      * @param encoding The encoding to use when reading {@code resource}.
300      *
301      * @throws NullPointerException if either {@code resource} or {@code encoding} is {@code null}.
302      * @throws PropertyException for invalid property values.
303      * @throws IllegalArgumentException if {@code resource} does not provide a valid Bankleitzahlendatei.
304      * @throws IOException if reading fails.
305      *
306      * @deprecated As of 1.15, replaced by constructor
307      * {@link BankleitzahlenDatei#BankleitzahlenDatei(java.net.URL, java.lang.String, int, java.util.Date, java.util.Date)}.
308      */
309     public BankleitzahlenDatei( final URL resource, final String encoding ) throws IOException
310     {
311         super();
312 
313         if ( resource == null )
314         {
315             throw new NullPointerException( "resource" );
316         }
317         if ( encoding == null )
318         {
319             throw new NullPointerException( "encoding" );
320         }
321 
322         this.assertValidProperties();
323         this.format = JUNE_2006_FORMAT;
324         this.encoding = encoding;
325         this.dateOfValidity = null;
326         this.dateOfExpiration = null;
327         this.readBankfile( resource );
328     }
329 
330     /**
331      * Reads a Bankleitzahlendatei form an URL initializing the instance to hold its data taking the encoding of the
332      * file and a format constant.
333      *
334      * @param resource An URL to a Bankleitzahlendatei.
335      * @param encoding The encoding to use when reading {@code resource}.
336      * @param format The format of the file to parse.
337      * @param dateOfValidity The date of validity of the file.
338      * @param dateOfExpiration The date of expiration of the file.
339      *
340      * @throws NullPointerException if {@code resource}, {@code encoding}, {@code dateOfValidity} or
341      * {@code dateOfExpiration} is {@code null}.
342      * @throws PropertyException for invalid property values.
343      * @throws IllegalArgumentException if {@code resource} does not provide a valid Bankleitzahlendatei or if
344      * {@code format} does not equal one of the format constants defined in this class.
345      * @throws IOException if reading fails.
346      *
347      * @see #JUNE_2006_FORMAT
348      * @see #JUNE_2013_FORMAT
349      */
350     public BankleitzahlenDatei( final URL resource, final String encoding, final int format,
351                                 final Date dateOfValidity, final Date dateOfExpiration ) throws IOException
352     {
353         super();
354 
355         if ( resource == null )
356         {
357             throw new NullPointerException( "resource" );
358         }
359         if ( encoding == null )
360         {
361             throw new NullPointerException( "encoding" );
362         }
363         if ( dateOfValidity == null )
364         {
365             throw new NullPointerException( "dateOfValidity" );
366         }
367         if ( dateOfExpiration == null )
368         {
369             throw new NullPointerException( "dateOfExpiration" );
370         }
371 
372         assertValidFormat( format );
373         this.assertValidProperties();
374         this.encoding = encoding;
375         this.format = format;
376         this.dateOfValidity = (Date) dateOfValidity.clone();
377         this.dateOfExpiration = (Date) dateOfExpiration.clone();
378         this.readBankfile( resource );
379     }
380 
381     /**
382      * Gets the encoding used for reading bankfile resources.
383      *
384      * @return The encoding used for reading bankfile resources.
385      */
386     public String getEncoding()
387     {
388         if ( this.encoding == null )
389         {
390             this.encoding = this.getDefaultEncoding();
391         }
392 
393         return this.encoding;
394     }
395 
396     /**
397      * Gets the format of the bankfile backing the instance.
398      *
399      * @return The format of the bankfile backing the instance.
400      *
401      * @since 1.15
402      */
403     public int getFormat()
404     {
405         return this.format;
406     }
407 
408     /**
409      * Gets the date of validity of the file.
410      *
411      * @return The date of validity of the file or {@code null}, if the instance got created by using one of the
412      * deprecated constructors.
413      *
414      * @since 1.15
415      */
416     public Date getDateOfValidity()
417     {
418         return (Date) ( this.dateOfValidity != null ? this.dateOfValidity.clone() : null );
419     }
420 
421     /**
422      * Gets the date of expiration of the file.
423      *
424      * @return The date of expiration of the file or {@code null}, if the instance got created by using one of the
425      * deprecated constructors.
426      *
427      * @since 1.15
428      */
429     public Date getDateOfExpiration()
430     {
431         return (Date) ( this.dateOfExpiration != null ? this.dateOfExpiration.clone() : null );
432     }
433 
434     /**
435      * Gets all records held by the instance.
436      *
437      * @return All records held by the instance.
438      */
439     public BankleitzahlInfo[] getRecords()
440     {
441         if ( this.cachedRecords == null )
442         {
443             this.cachedRecords = (BankleitzahlInfo[]) this.records.values().
444                 toArray( new BankleitzahlInfo[ this.records.size() ] );
445 
446         }
447 
448         return this.cachedRecords;
449     }
450 
451     /**
452      * Gets all records deleted during updating.
453      *
454      * @return All records deleted during updating.
455      *
456      * @see #update(org.jdtaus.banking.util.BankleitzahlenDatei)
457      *
458      * @see #update(org.jdtaus.banking.util.BankleitzahlenDatei)
459      * @since 1.15
460      */
461     public BankleitzahlInfo[] getDeletedRecords()
462     {
463         if ( this.cachedDeletedRecords == null )
464         {
465             this.cachedDeletedRecords = (BankleitzahlInfo[]) this.deletedRecords.values().
466                 toArray( new BankleitzahlInfo[ this.deletedRecords.size() ] );
467 
468         }
469 
470         return this.cachedDeletedRecords;
471     }
472 
473     /**
474      * Gets a record identified by a serial number.
475      *
476      * @param serialNumber The serial number of the record to return.
477      *
478      * @return The record with serial number {@code serialNumber} or {@code null}, if no record matching
479      * {@code serialNumber} exists in the file.
480      *
481      * @throws NullPointerException if {@code serialNumber} is {@code null}.
482      */
483     public BankleitzahlInfo getRecord( final Integer serialNumber )
484     {
485         if ( serialNumber == null )
486         {
487             throw new NullPointerException( "serialNumber" );
488         }
489 
490         return (BankleitzahlInfo) this.records.get( serialNumber );
491     }
492 
493     /**
494      * Gets a deleted record identified by a serial number.
495      *
496      * @param serialNumber The serial number of the deleted record to return.
497      *
498      * @return The deleted record with serial number {@code serialNumber} or {@code null}, if no such record is found.
499      *
500      * @throws NullPointerException if {@code serialNumber} is {@code null}.
501      *
502      * @see #getDeletedRecords()
503      * @see #update(org.jdtaus.banking.util.BankleitzahlenDatei)
504      * @since 1.15
505      */
506     public BankleitzahlInfo getDeletedRecord( final Integer serialNumber )
507     {
508         if ( serialNumber == null )
509         {
510             throw new NullPointerException( "serialNumber" );
511         }
512 
513         return (BankleitzahlInfo) this.deletedRecords.get( serialNumber );
514     }
515 
516     /**
517      * Gets a head office record for a given bank code.
518      *
519      * @param bankCode The bank code of the head office record to return.
520      *
521      * @return The head office record of the bank identified by {@code bankCode} or {@code null}, if no such record is
522      * found.
523      *
524      * @throws NullPointerException if {@code bankCode} is {@code null}.
525      *
526      * @see #getRecords()
527      * @see BankleitzahlInfo#isHeadOffice()
528      * @since 1.15
529      */
530     public BankleitzahlInfo getHeadOfficeRecord( final Bankleitzahl bankCode )
531     {
532         if ( bankCode == null )
533         {
534             throw new NullPointerException( "bankCode" );
535         }
536 
537         return (BankleitzahlInfo) this.headOffices.get( bankCode );
538     }
539 
540     /**
541      * Gets a deleted head office record for a given bank code.
542      *
543      * @param bankCode The bank code of the deleted head office record to return.
544      *
545      * @return The deleted head office record of the bank identified by {@code bankCode} or {@code null}, if no such
546      * record is found.
547      *
548      * @throws NullPointerException if {@code bankCode} is {@code null}.
549      *
550      * @see #getDeletedRecords()
551      * @see BankleitzahlInfo#isHeadOffice()
552      * @see #update(org.jdtaus.banking.util.BankleitzahlenDatei)
553      * @since 1.15
554      */
555     public BankleitzahlInfo getDeletedHeadOfficeRecord( final Bankleitzahl bankCode )
556     {
557         if ( bankCode == null )
558         {
559             throw new NullPointerException( "bankCode" );
560         }
561 
562         return (BankleitzahlInfo) this.deletedHeadOffices.get( bankCode );
563     }
564 
565     /**
566      * Gets branch office records for a given bank code.
567      *
568      * @param bankCode The bank code of the branch office records to return.
569      *
570      * @return The branch office records of the bank identified by {@code bankCode}.
571      *
572      * @throws NullPointerException if {@code bankCode} is {@code null}.
573      *
574      * @see #getRecords()
575      * @see BankleitzahlInfo#isHeadOffice()
576      * @since 1.15
577      */
578     public BankleitzahlInfo[] getBranchOfficeRecords( final Bankleitzahl bankCode )
579     {
580         if ( bankCode == null )
581         {
582             throw new NullPointerException( "bankCode" );
583         }
584 
585         final List records = (List) this.branchOffices.get( bankCode );
586         return records != null
587                ? (BankleitzahlInfo[]) records.toArray( new BankleitzahlInfo[ records.size() ] )
588                : NO_RECORDS;
589 
590     }
591 
592     /**
593      * Gets deleted branch office records for a given bank code.
594      *
595      * @param bankCode The bank code of the deleted branch office records to return.
596      *
597      * @return The deleted branch office records of the bank identified by {@code bankCode}.
598      *
599      * @throws NullPointerException if {@code bankCode} is {@code null}.
600      *
601      * @see #getDeletedRecords()
602      * @see BankleitzahlInfo#isHeadOffice()
603      * @since 1.15
604      */
605     public BankleitzahlInfo[] getDeletedBranchOfficeRecords( final Bankleitzahl bankCode )
606     {
607         if ( bankCode == null )
608         {
609             throw new NullPointerException( "bankCode" );
610         }
611 
612         final List records = (List) this.deletedBranchOffices.get( bankCode );
613         return records != null
614                ? (BankleitzahlInfo[]) records.toArray( new BankleitzahlInfo[ records.size() ] )
615                : NO_RECORDS;
616 
617     }
618 
619     /**
620      * Given a newer version of the Bankleitzahlendatei updates the records of the instance to reflect the changes.
621      *
622      * @param file A newer version of the Bankleitzahlendatei to use for updating the records of this instance.
623      *
624      * @throws NullPointerException if {@code file} is {@code null}.
625      * @throws IllegalArgumentException if {@code file} cannot be used for updating this instance.
626      */
627     public void update( final BankleitzahlenDatei file )
628     {
629         if ( file == null )
630         {
631             throw new NullPointerException( "file" );
632         }
633         if ( file.getFormat() < this.getFormat() )
634         {
635             throw new IllegalArgumentException( this.getCannotUpdateIncomptibleFileMessage(
636                 this.getLocale(), toFormatName( this.getFormat() ), toFormatName( file.getFormat() ) ) );
637 
638         }
639 
640         final boolean log = this.getLogger().isDebugEnabled();
641         final boolean upgrade = this.getFormat() < file.getFormat();
642 
643         int progress = 0;
644         Task task = new Task();
645         task.setIndeterminate( false );
646         task.setCancelable( false );
647         task.setDescription( new UpdatesBankleitzahlenDateiMessage() );
648         task.setMinimum( 0 );
649         task.setMaximum( file.getRecords().length );
650         task.setProgress( progress );
651 
652         try
653         {
654             this.getTaskMonitor().monitor( task );
655 
656             for ( int i = file.getRecords().length - 1; i >= 0; i-- )
657             {
658                 task.setProgress( progress++ );
659                 final BankleitzahlInfo newVersion = file.getRecords()[i];
660 
661                 if ( 'A' == newVersion.getChangeLabel() )
662                 {
663                     final BankleitzahlInfo oldVersion =
664                         (BankleitzahlInfo) this.records.get( newVersion.getSerialNumber() );
665 
666                     if ( oldVersion != null && oldVersion.getChangeLabel() != 'D' )
667                     {
668                         this.resetRecords();
669                         throw new IllegalArgumentException( this.getCannotAddDuplicateRecordMessage(
670                             this.getLocale(), newVersion.getSerialNumber() ) );
671 
672                     }
673 
674                     this.records.put( newVersion.getSerialNumber(), newVersion );
675 
676                     if ( log )
677                     {
678                         this.getLogger().debug( this.getAddRecordInfoMessage(
679                             this.getLocale(), String.valueOf( newVersion.getChangeLabel() ),
680                             newVersion.getSerialNumber() ) );
681 
682                     }
683                 }
684                 else if ( 'M' == newVersion.getChangeLabel() || 'D' == newVersion.getChangeLabel() )
685                 {
686                     if ( this.records.put( newVersion.getSerialNumber(), newVersion ) == null )
687                     {
688                         this.resetRecords();
689                         throw new IllegalArgumentException( this.getCannotModifyNonexistentRecordMessage(
690                             this.getLocale(), newVersion.getSerialNumber() ) );
691 
692                     }
693 
694                     if ( log )
695                     {
696                         this.getLogger().debug( this.getModifyRecordInfoMessage(
697                             this.getLocale(), String.valueOf( newVersion.getChangeLabel() ),
698                             newVersion.getSerialNumber() ) );
699 
700                     }
701                 }
702                 else if ( 'U' == newVersion.getChangeLabel() )
703                 {
704                     if ( ( upgrade && this.records.put( newVersion.getSerialNumber(), newVersion ) == null )
705                          || !this.records.containsKey( newVersion.getSerialNumber() ) )
706                     {
707                         this.resetRecords();
708                         throw new IllegalArgumentException( this.getCannotModifyNonexistentRecordMessage(
709                             this.getLocale(), newVersion.getSerialNumber() ) );
710 
711                     }
712                 }
713             }
714 
715             if ( upgrade )
716             {
717                 if ( this.getLogger().isInfoEnabled() )
718                 {
719                     this.getLogger().info( this.getBankcodeFileUpgradeInfoMessage(
720                         this.getLocale(), toFormatName( this.format ), toFormatName( file.getFormat() ) ) );
721 
722                 }
723 
724                 this.format = file.getFormat();
725             }
726 
727             this.dateOfValidity = file.getDateOfValidity();
728             this.dateOfExpiration = file.getDateOfExpiration();
729         }
730         finally
731         {
732             this.getTaskMonitor().finish( task );
733         }
734 
735         progress = 0;
736         task = new Task();
737         task.setIndeterminate( false );
738         task.setCancelable( false );
739         task.setDescription( new UpdatesBankleitzahlenDateiMessage() );
740         task.setMinimum( 0 );
741         task.setMaximum( this.records.size() );
742         task.setProgress( progress );
743 
744         try
745         {
746             this.getTaskMonitor().monitor( task );
747 
748             for ( final Iterator it = this.records.values().iterator(); it.hasNext(); )
749             {
750                 task.setProgress( progress++ );
751                 final BankleitzahlInfo oldVersion = (BankleitzahlInfo) it.next();
752 
753                 if ( 'D' == oldVersion.getChangeLabel() )
754                 {
755                     final BankleitzahlInfo newVersion = file.getRecord( oldVersion.getSerialNumber() );
756 
757                     if ( newVersion == null )
758                     {
759                         if ( this.deletedRecords.put( oldVersion.getSerialNumber(), oldVersion ) != null )
760                         {
761                             this.resetRecords();
762                             throw new IllegalStateException( this.getCannotRemoveDuplicateRecordMessage(
763                                 this.getLocale(), oldVersion.getSerialNumber() ) );
764 
765                         }
766 
767                         it.remove();
768 
769                         if ( log )
770                         {
771                             this.getLogger().debug( this.getRemoveRecordInfoMessage(
772                                 this.getLocale(), String.valueOf( oldVersion.getChangeLabel() ),
773                                 oldVersion.getSerialNumber() ) );
774 
775                         }
776                     }
777                 }
778             }
779         }
780         finally
781         {
782             this.getTaskMonitor().finish( task );
783         }
784 
785         this.updateRecords();
786     }
787 
788     /**
789      * Checks configured properties.
790      *
791      * @throws PropertyException for invalid property values.
792      */
793     private void assertValidProperties()
794     {
795         if ( this.getEncoding() == null || this.getEncoding().length() == 0 )
796         {
797             throw new PropertyException( "encoding", this.getEncoding() );
798         }
799 
800         try
801         {
802             "".getBytes( this.getEncoding() );
803         }
804         catch ( final UnsupportedEncodingException e )
805         {
806             throw new PropertyException( "encoding", this.getEncoding(), e );
807         }
808     }
809 
810     /**
811      * Checks a given integer to equal one of the format constants defined in this class.
812      *
813      * @param value The value to check.
814      *
815      * @throws IllegalArgumentException if {@code value} does not equal one of the format constants defined in this
816      * class.
817      */
818     private static void assertValidFormat( final int value )
819     {
820         if ( value != JUNE_2006_FORMAT && value != JUNE_2013_FORMAT )
821         {
822             throw new IllegalArgumentException( Integer.toString( value ) );
823         }
824     }
825 
826     /**
827      * Reads a Bankleitzahlendatei from an URL initializing the instance to hold its data.
828      *
829      * @param resource An URL to a Bankleitzahlendatei.
830      *
831      * @throws NullPointerException if {@code resource} is {@code null}.
832      * @throws IllegalArgumentException if {@code resource} does not provide a valid Bankleitzahlendatei.
833      * @throws IOException if reading fails.
834      */
835     private void readBankfile( final URL resource ) throws IOException
836     {
837         if ( resource == null )
838         {
839             throw new NullPointerException( "resource" );
840         }
841 
842         this.records.clear();
843 
844         if ( this.getLogger().isDebugEnabled() )
845         {
846             this.getLogger().debug( this.getFileNameInfoMessage( this.getLocale(), resource.toExternalForm() ) );
847         }
848 
849         LineNumberReader reader = null;
850         final NumberFormat plzFmt = new DecimalFormat( "00000" );
851         final NumberFormat serFmt = new DecimalFormat( "000000" );
852         final NumberFormat blzFmt = new DecimalFormat( "00000000" );
853 
854         try
855         {
856             reader = new LineNumberReader( new InputStreamReader( resource.openStream(), this.getEncoding() ) );
857             boolean emptyLine = false;
858 
859             for ( String line = reader.readLine(); line != null; line = reader.readLine() )
860             {
861                 if ( line.trim().length() == 0 )
862                 {
863                     emptyLine = true;
864                     continue;
865                 }
866 
867                 if ( emptyLine )
868                 {
869                     throw new IllegalArgumentException( this.getUnexpectedDataMessage(
870                         this.getLocale(), new Integer( reader.getLineNumber() ), resource.toExternalForm() ) );
871 
872                 }
873 
874                 final BankleitzahlInfo r = new BankleitzahlInfo();
875 
876                 // Field 1
877                 r.setBankCode( Bankleitzahl.parse( field( line, FIELD_TO_OFFSET[0], FIELD_TO_ENDOFFSET[0] ) ) );
878                 // Field 2
879                 r.setHeadOffice( "1".equals( field( line, FIELD_TO_OFFSET[1], FIELD_TO_ENDOFFSET[1] ) ) );
880                 // Field 3
881                 r.setName( field( line, FIELD_TO_OFFSET[2], FIELD_TO_ENDOFFSET[2] ) );
882                 // Field 4
883                 r.setPostalCode( plzFmt.parse( field( line, FIELD_TO_OFFSET[3], FIELD_TO_ENDOFFSET[3] ) ).intValue() );
884                 // Field 5
885                 r.setCity( field( line, FIELD_TO_OFFSET[4], FIELD_TO_ENDOFFSET[4] ) );
886                 // Field 6
887                 r.setDescription( field( line, FIELD_TO_OFFSET[5], FIELD_TO_ENDOFFSET[5] ) );
888                 // Field 7
889                 String field = field( line, FIELD_TO_OFFSET[6], FIELD_TO_ENDOFFSET[6] );
890                 r.setPanInstituteNumber( field.length() > 0 ? plzFmt.parse( field ).intValue() : 0 );
891                 // Field 8
892                 r.setBic( field( line, FIELD_TO_OFFSET[7], FIELD_TO_ENDOFFSET[7] ) );
893                 // Field 9
894                 r.setValidationLabel( field( line, FIELD_TO_OFFSET[8], FIELD_TO_ENDOFFSET[8] ) );
895                 // Field 10
896                 field = field( line, FIELD_TO_OFFSET[9], FIELD_TO_ENDOFFSET[9] );
897                 r.setSerialNumber( new Integer( serFmt.parse( field ).intValue() ) );
898                 // Field 11
899                 r.setChangeLabel( field( line, FIELD_TO_OFFSET[10], FIELD_TO_ENDOFFSET[10] ).toCharArray()[0] );
900                 // Field 12
901                 r.setMarkedForDeletion( "1".equals( field( line, FIELD_TO_OFFSET[11], FIELD_TO_ENDOFFSET[11] ) ) );
902                 // Field 13
903                 Number blz = blzFmt.parse( field( line, FIELD_TO_OFFSET[12], FIELD_TO_ENDOFFSET[12] ) );
904                 if ( blz.intValue() != 0 )
905                 {
906                     r.setReplacingBankCode( Bankleitzahl.valueOf( blz ) );
907                 }
908                 else
909                 {
910                     r.setReplacingBankCode( null );
911                 }
912 
913                 if ( this.getFormat() >= JUNE_2013_FORMAT )
914                 {
915                     // Field 14
916                     r.setIbanRuleLabel( Integer.valueOf( field( line, FIELD_TO_OFFSET[13],
917                                                                 FIELD_TO_ENDOFFSET[13] ) ) );
918 
919                     r.setIbanRuleVersion( Integer.valueOf( field( line, FIELD_TO_OFFSET[14],
920                                                                   FIELD_TO_ENDOFFSET[14] ) ) );
921 
922                 }
923 
924                 switch ( r.getChangeLabel() )
925                 {
926                     case 'A':
927                         r.setCreationDate( this.getDateOfValidity() );
928                         break;
929                     case 'M':
930                         r.setModificationDate( this.getDateOfValidity() );
931                         break;
932                     case 'D':
933                         r.setDeletionDate( this.getDateOfExpiration() );
934                         break;
935                     case 'U':
936                         // ignored
937                         break;
938                     default:
939                         throw new AssertionError( r.getChangeLabel() );
940                 }
941 
942                 if ( this.records.put( r.getSerialNumber(), r ) != null )
943                 {
944                     this.resetRecords();
945                     throw new IllegalArgumentException( this.getCannotAddDuplicateRecordMessage(
946                         this.getLocale(), r.getSerialNumber() ) );
947 
948                 }
949             }
950         }
951         catch ( final ParseException e )
952         {
953             this.resetRecords();
954             throw (IllegalArgumentException) new IllegalArgumentException( resource.toExternalForm() ).initCause( e );
955         }
956         catch ( final IndexOutOfBoundsException e )
957         {
958             this.resetRecords();
959             throw (IllegalArgumentException) new IllegalArgumentException( resource.toExternalForm() ).initCause( e );
960         }
961         catch ( final IOException e )
962         {
963             this.resetRecords();
964             throw e;
965         }
966         finally
967         {
968             this.cachedRecords = null;
969             this.cachedDeletedRecords = null;
970 
971             if ( reader != null )
972             {
973                 reader.close();
974             }
975         }
976     }
977 
978     private void resetRecords()
979     {
980         this.records.clear();
981         this.deletedRecords.clear();
982         this.updateRecords();
983     }
984 
985     private void updateRecords()
986     {
987         this.headOffices.clear();
988         this.deletedHeadOffices.clear();
989         this.branchOffices.clear();
990         this.deletedBranchOffices.clear();
991         this.cachedRecords = null;
992         this.cachedDeletedRecords = null;
993 
994         for ( int i = 0, l0 = this.getRecords().length; i < l0; i++ )
995         {
996             final BankleitzahlInfo record = this.getRecords()[i];
997 
998             if ( record.isHeadOffice() )
999             {
1000                 if ( this.headOffices.put( record.getBankCode(), record ) != null )
1001                 {
1002                     this.resetRecords();
1003                     throw new IllegalStateException( this.getCannotAddDuplicateHeadOfficeRecordMessage(
1004                         this.getLocale(), record.getBankCode() ) );
1005 
1006                 }
1007             }
1008             else
1009             {
1010                 List list = (List) this.branchOffices.get( record.getBankCode() );
1011 
1012                 if ( list == null )
1013                 {
1014                     list = new ArrayList();
1015                     this.branchOffices.put( record.getBankCode(), list );
1016                 }
1017 
1018                 list.add( record );
1019             }
1020         }
1021 
1022         for ( int i = 0, l0 = this.getDeletedRecords().length; i < l0; i++ )
1023         {
1024             final BankleitzahlInfo record = this.getDeletedRecords()[i];
1025 
1026             if ( record.isHeadOffice() )
1027             {
1028                 if ( this.deletedHeadOffices.put( record.getBankCode(), record ) != null )
1029                 {
1030                     this.resetRecords();
1031                     throw new IllegalStateException( this.getCannotAddDuplicateHeadOfficeRecordMessage(
1032                         this.getLocale(), record.getBankCode() ) );
1033 
1034                 }
1035             }
1036             else
1037             {
1038                 List list = (List) this.deletedBranchOffices.get( record.getBankCode() );
1039 
1040                 if ( list == null )
1041                 {
1042                     list = new ArrayList();
1043                     this.deletedBranchOffices.put( record.getBankCode(), list );
1044                 }
1045 
1046                 list.add( record );
1047             }
1048         }
1049     }
1050 
1051     private static String field( final String line, final int startOffset, final int endOffset )
1052     {
1053         return line.substring( startOffset, endOffset ).trim();
1054     }
1055 
1056     private static String toFormatName( final long format )
1057     {
1058         String name = "";
1059 
1060         if ( format == JUNE_2006_FORMAT )
1061         {
1062             name = "JUNE2006";
1063         }
1064         else if ( format == JUNE_2013_FORMAT )
1065         {
1066             name = "JUNE2013";
1067         }
1068 
1069         return name;
1070     }
1071 
1072     //-----------------------------------------------------BankleitzahlenDatei--
1073     //--Messages----------------------------------------------------------------
1074 
1075 // <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausMessages
1076     // This section is managed by jdtaus-container-mojo.
1077 
1078     /**
1079      * Gets the text of message <code>fileNameInfo</code>.
1080      * <blockquote><pre>Lädt Bankleitzahlendatei "{0}".</pre></blockquote>
1081      * <blockquote><pre>Loading Bankleitzahlendatei "{0}".</pre></blockquote>
1082      *
1083      * @param locale The locale of the message instance to return.
1084      * @param fileName format parameter.
1085      *
1086      * @return the text of message <code>fileNameInfo</code>.
1087      */
1088     private String getFileNameInfoMessage( final Locale locale,
1089             final java.lang.String fileName )
1090     {
1091         return ContainerFactory.getContainer().
1092             getMessage( this, "fileNameInfo", locale,
1093                 new Object[]
1094                 {
1095                     fileName
1096                 });
1097 
1098     }
1099 
1100     /**
1101      * Gets the text of message <code>addRecordInfo</code>.
1102      * <blockquote><pre>{0}: Datensatz {1, number} hinzugefügt.</pre></blockquote>
1103      * <blockquote><pre>{0}: Added record {1, number}.</pre></blockquote>
1104      *
1105      * @param locale The locale of the message instance to return.
1106      * @param label format parameter.
1107      * @param serialNumber format parameter.
1108      *
1109      * @return the text of message <code>addRecordInfo</code>.
1110      */
1111     private String getAddRecordInfoMessage( final Locale locale,
1112             final java.lang.String label,
1113             final java.lang.Number serialNumber )
1114     {
1115         return ContainerFactory.getContainer().
1116             getMessage( this, "addRecordInfo", locale,
1117                 new Object[]
1118                 {
1119                     label,
1120                     serialNumber
1121                 });
1122 
1123     }
1124 
1125     /**
1126      * Gets the text of message <code>modifyRecordInfo</code>.
1127      * <blockquote><pre>{0}: Datensatz {1, number} aktualisiert.</pre></blockquote>
1128      * <blockquote><pre>{0}: Updated record {1, number}.</pre></blockquote>
1129      *
1130      * @param locale The locale of the message instance to return.
1131      * @param label format parameter.
1132      * @param serialNumber format parameter.
1133      *
1134      * @return the text of message <code>modifyRecordInfo</code>.
1135      */
1136     private String getModifyRecordInfoMessage( final Locale locale,
1137             final java.lang.String label,
1138             final java.lang.Number serialNumber )
1139     {
1140         return ContainerFactory.getContainer().
1141             getMessage( this, "modifyRecordInfo", locale,
1142                 new Object[]
1143                 {
1144                     label,
1145                     serialNumber
1146                 });
1147 
1148     }
1149 
1150     /**
1151      * Gets the text of message <code>removeRecordInfo</code>.
1152      * <blockquote><pre>{0}: Datensatz {1, number} entfernt.</pre></blockquote>
1153      * <blockquote><pre>{0}: Removed record {1, number}.</pre></blockquote>
1154      *
1155      * @param locale The locale of the message instance to return.
1156      * @param label format parameter.
1157      * @param serialNumber format parameter.
1158      *
1159      * @return the text of message <code>removeRecordInfo</code>.
1160      */
1161     private String getRemoveRecordInfoMessage( final Locale locale,
1162             final java.lang.String label,
1163             final java.lang.Number serialNumber )
1164     {
1165         return ContainerFactory.getContainer().
1166             getMessage( this, "removeRecordInfo", locale,
1167                 new Object[]
1168                 {
1169                     label,
1170                     serialNumber
1171                 });
1172 
1173     }
1174 
1175     /**
1176      * Gets the text of message <code>cannotAddDuplicateRecord</code>.
1177      * <blockquote><pre>Datensatz mit Seriennummer {0,number} existiert bereits und kann nicht hinzugefügt werden.</pre></blockquote>
1178      * <blockquote><pre>Record with serial number {0,number} already exists and cannot be added.</pre></blockquote>
1179      *
1180      * @param locale The locale of the message instance to return.
1181      * @param serialNumber format parameter.
1182      *
1183      * @return the text of message <code>cannotAddDuplicateRecord</code>.
1184      */
1185     private String getCannotAddDuplicateRecordMessage( final Locale locale,
1186             final java.lang.Number serialNumber )
1187     {
1188         return ContainerFactory.getContainer().
1189             getMessage( this, "cannotAddDuplicateRecord", locale,
1190                 new Object[]
1191                 {
1192                     serialNumber
1193                 });
1194 
1195     }
1196 
1197     /**
1198      * Gets the text of message <code>cannotAddDuplicateHeadOfficeRecord</code>.
1199      * <blockquote><pre>Datensatz der Hauptstelle {0,number} existiert bereits und kann nicht hinzugefügt werden.</pre></blockquote>
1200      * <blockquote><pre>Head office record of bank code {0,number} already exists and cannot be added.</pre></blockquote>
1201      *
1202      * @param locale The locale of the message instance to return.
1203      * @param bankCode format parameter.
1204      *
1205      * @return the text of message <code>cannotAddDuplicateHeadOfficeRecord</code>.
1206      */
1207     private String getCannotAddDuplicateHeadOfficeRecordMessage( final Locale locale,
1208             final java.lang.Number bankCode )
1209     {
1210         return ContainerFactory.getContainer().
1211             getMessage( this, "cannotAddDuplicateHeadOfficeRecord", locale,
1212                 new Object[]
1213                 {
1214                     bankCode
1215                 });
1216 
1217     }
1218 
1219     /**
1220      * Gets the text of message <code>cannotModifyNonexistentRecord</code>.
1221      * <blockquote><pre>Ein Datensatz mit Seriennummer {0,number} existiert nicht und kann nicht aktualisiert werden.</pre></blockquote>
1222      * <blockquote><pre>Record with serial number {0,number} does not exist and cannot be updated.</pre></blockquote>
1223      *
1224      * @param locale The locale of the message instance to return.
1225      * @param serialNumber format parameter.
1226      *
1227      * @return the text of message <code>cannotModifyNonexistentRecord</code>.
1228      */
1229     private String getCannotModifyNonexistentRecordMessage( final Locale locale,
1230             final java.lang.Number serialNumber )
1231     {
1232         return ContainerFactory.getContainer().
1233             getMessage( this, "cannotModifyNonexistentRecord", locale,
1234                 new Object[]
1235                 {
1236                     serialNumber
1237                 });
1238 
1239     }
1240 
1241     /**
1242      * Gets the text of message <code>cannotUpdateIncomptibleFile</code>.
1243      * <blockquote><pre>''{0}'' Bankleitzahlendatei kann nicht mit ''{1}'' Bankleitzahlendatei aktualisiert werden.</pre></blockquote>
1244      * <blockquote><pre>''{0}'' bank code file cannot be updated with a ''{1}'' bank code file.</pre></blockquote>
1245      *
1246      * @param locale The locale of the message instance to return.
1247      * @param targetBankCodeFileFormat format parameter.
1248      * @param sourceBankCodeFileFormat format parameter.
1249      *
1250      * @return the text of message <code>cannotUpdateIncomptibleFile</code>.
1251      */
1252     private String getCannotUpdateIncomptibleFileMessage( final Locale locale,
1253             final java.lang.String targetBankCodeFileFormat,
1254             final java.lang.String sourceBankCodeFileFormat )
1255     {
1256         return ContainerFactory.getContainer().
1257             getMessage( this, "cannotUpdateIncomptibleFile", locale,
1258                 new Object[]
1259                 {
1260                     targetBankCodeFileFormat,
1261                     sourceBankCodeFileFormat
1262                 });
1263 
1264     }
1265 
1266     /**
1267      * Gets the text of message <code>unexpectedData</code>.
1268      * <blockquote><pre>Unerwartete Daten in Zeile {0,number} bei der Verarbeitung von {1}.</pre></blockquote>
1269      * <blockquote><pre>Unexpected data at line {0,number} processing {1}.</pre></blockquote>
1270      *
1271      * @param locale The locale of the message instance to return.
1272      * @param lineNumber format parameter.
1273      * @param resourceName format parameter.
1274      *
1275      * @return the text of message <code>unexpectedData</code>.
1276      */
1277     private String getUnexpectedDataMessage( final Locale locale,
1278             final java.lang.Number lineNumber,
1279             final java.lang.String resourceName )
1280     {
1281         return ContainerFactory.getContainer().
1282             getMessage( this, "unexpectedData", locale,
1283                 new Object[]
1284                 {
1285                     lineNumber,
1286                     resourceName
1287                 });
1288 
1289     }
1290 
1291     /**
1292      * Gets the text of message <code>bankcodeFileUpgradeInfo</code>.
1293      * <blockquote><pre>''{0}'' Bankleitzahlendatei zu ''{1}'' Bankleitzahlendatei aktualisiert.</pre></blockquote>
1294      * <blockquote><pre>''{0}'' bank code file upgraded to ''{1}'' bank code file.</pre></blockquote>
1295      *
1296      * @param locale The locale of the message instance to return.
1297      * @param targetBankCodeFileFormat format parameter.
1298      * @param sourceBankCodeFileFormat format parameter.
1299      *
1300      * @return the text of message <code>bankcodeFileUpgradeInfo</code>.
1301      */
1302     private String getBankcodeFileUpgradeInfoMessage( final Locale locale,
1303             final java.lang.String targetBankCodeFileFormat,
1304             final java.lang.String sourceBankCodeFileFormat )
1305     {
1306         return ContainerFactory.getContainer().
1307             getMessage( this, "bankcodeFileUpgradeInfo", locale,
1308                 new Object[]
1309                 {
1310                     targetBankCodeFileFormat,
1311                     sourceBankCodeFileFormat
1312                 });
1313 
1314     }
1315 
1316     /**
1317      * Gets the text of message <code>cannotRemoveDuplicateRecord</code>.
1318      * <blockquote><pre>Datensatz mit Seriennummer {0,number} bereits gelöscht.</pre></blockquote>
1319      * <blockquote><pre>Record with serial number {0,number} already deleted.</pre></blockquote>
1320      *
1321      * @param locale The locale of the message instance to return.
1322      * @param serialNumber format parameter.
1323      *
1324      * @return the text of message <code>cannotRemoveDuplicateRecord</code>.
1325      */
1326     private String getCannotRemoveDuplicateRecordMessage( final Locale locale,
1327             final java.lang.Number serialNumber )
1328     {
1329         return ContainerFactory.getContainer().
1330             getMessage( this, "cannotRemoveDuplicateRecord", locale,
1331                 new Object[]
1332                 {
1333                     serialNumber
1334                 });
1335 
1336     }
1337 
1338 // </editor-fold>//GEN-END:jdtausMessages
1339 
1340     //----------------------------------------------------------------Messages--
1341 }