001/*
002 *  jDTAUS Banking Utilities
003 *  Copyright (C) 2005 Christian Schulte
004 *  <cs@schulte.it>
005 *
006 *  This library is free software; you can redistribute it and/or
007 *  modify it under the terms of the GNU Lesser General Public
008 *  License as published by the Free Software Foundation; either
009 *  version 2.1 of the License, or any later version.
010 *
011 *  This library is distributed in the hope that it will be useful,
012 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
013 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 *  Lesser General Public License for more details.
015 *
016 *  You should have received a copy of the GNU Lesser General Public
017 *  License along with this library; if not, write to the Free Software
018 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
019 *
020 */
021package org.jdtaus.banking.util;
022
023import java.io.IOException;
024import java.io.InputStreamReader;
025import java.io.LineNumberReader;
026import java.io.UnsupportedEncodingException;
027import java.net.URL;
028import java.text.DecimalFormat;
029import java.text.NumberFormat;
030import java.text.ParseException;
031import java.util.ArrayList;
032import java.util.Date;
033import java.util.HashMap;
034import java.util.Iterator;
035import java.util.List;
036import java.util.Locale;
037import java.util.Map;
038import org.jdtaus.banking.Bankleitzahl;
039import org.jdtaus.banking.BankleitzahlInfo;
040import org.jdtaus.banking.messages.UpdatesBankleitzahlenDateiMessage;
041import org.jdtaus.core.container.ContainerFactory;
042import org.jdtaus.core.container.PropertyException;
043import org.jdtaus.core.logging.spi.Logger;
044import org.jdtaus.core.monitor.spi.Task;
045import org.jdtaus.core.monitor.spi.TaskMonitor;
046
047/**
048 * German Bankleitzahlendatei for the format as of 2006-06-01.
049 * <p>For further information see the
050 * <a href="../../../../doc-files/merkblatt_bankleitzahlendatei.pdf">Merkblatt Bankleitzahlendatei</a>.
051 * An updated version of the document may be found at
052 * <a href="http://www.bundesbank.de">Deutsche Bundesbank</a>.
053 * </p>
054 *
055 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
056 * @version $JDTAUS: BankleitzahlenDatei.java 8861 2014-01-10 17:09:50Z schulte $
057 */
058public final class BankleitzahlenDatei
059{
060    //--Dependencies------------------------------------------------------------
061
062// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausDependencies
063    // This section is managed by jdtaus-container-mojo.
064
065    /**
066     * Gets the configured <code>Logger</code> implementation.
067     *
068     * @return The configured <code>Logger</code> implementation.
069     */
070    private Logger getLogger()
071    {
072        return (Logger) ContainerFactory.getContainer().
073            getDependency( this, "Logger" );
074
075    }
076
077    /**
078     * Gets the configured <code>TaskMonitor</code> implementation.
079     *
080     * @return The configured <code>TaskMonitor</code> implementation.
081     */
082    private TaskMonitor getTaskMonitor()
083    {
084        return (TaskMonitor) ContainerFactory.getContainer().
085            getDependency( this, "TaskMonitor" );
086
087    }
088
089    /**
090     * Gets the configured <code>Locale</code> implementation.
091     *
092     * @return The configured <code>Locale</code> implementation.
093     */
094    private Locale getLocale()
095    {
096        return (Locale) ContainerFactory.getContainer().
097            getDependency( this, "Locale" );
098
099    }
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}