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.InputStream;
025import java.io.InputStreamReader;
026import java.io.LineNumberReader;
027import java.io.UnsupportedEncodingException;
028import java.net.URL;
029import java.util.HashMap;
030import java.util.Iterator;
031import java.util.Locale;
032import java.util.Map;
033import org.jdtaus.banking.BankleitzahlInfo;
034import org.jdtaus.banking.messages.UpdatesBankleitzahlenDateiMessage;
035import org.jdtaus.core.container.ContainerFactory;
036import org.jdtaus.core.container.PropertyException;
037import org.jdtaus.core.logging.spi.Logger;
038import org.jdtaus.core.monitor.spi.Task;
039import org.jdtaus.core.monitor.spi.TaskMonitor;
040
041/**
042 * German Bankleitzahlendatei for the format as of 2006-06-01.
043 * <p>For further information see the
044 * <a href="../../../../doc-files/merkblatt_bankleitzahlendatei.pdf">Merkblatt Bankleitzahlendatei</a>.
045 * An updated version of the document may be found at
046 * <a href="http://www.bundesbank.de">Deutsche Bundesbank</a>.
047 * </p>
048 *
049 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
050 * @version $JDTAUS: BankleitzahlenDatei.java 8725 2012-10-04 21:26:50Z schulte $
051 */
052public final class BankleitzahlenDatei
053{
054    //--Dependencies------------------------------------------------------------
055
056// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausDependencies
057    // This section is managed by jdtaus-container-mojo.
058
059    /**
060     * Gets the configured <code>Logger</code> implementation.
061     *
062     * @return The configured <code>Logger</code> implementation.
063     */
064    private Logger getLogger()
065    {
066        return (Logger) ContainerFactory.getContainer().
067            getDependency( this, "Logger" );
068
069    }
070
071    /**
072     * Gets the configured <code>TaskMonitor</code> implementation.
073     *
074     * @return The configured <code>TaskMonitor</code> implementation.
075     */
076    private TaskMonitor getTaskMonitor()
077    {
078        return (TaskMonitor) ContainerFactory.getContainer().
079            getDependency( this, "TaskMonitor" );
080
081    }
082
083    /**
084     * Gets the configured <code>Locale</code> implementation.
085     *
086     * @return The configured <code>Locale</code> implementation.
087     */
088    private Locale getLocale()
089    {
090        return (Locale) ContainerFactory.getContainer().
091            getDependency( this, "Locale" );
092
093    }
094
095// </editor-fold>//GEN-END:jdtausDependencies
096
097    //------------------------------------------------------------Dependencies--
098    //--Properties--------------------------------------------------------------
099
100// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausProperties
101    // This section is managed by jdtaus-container-mojo.
102
103    /**
104     * Gets the value of property <code>defaultEncoding</code>.
105     *
106     * @return Default encoding to use when reading bankfile resources.
107     */
108    private java.lang.String getDefaultEncoding()
109    {
110        return (java.lang.String) ContainerFactory.getContainer().
111            getProperty( this, "defaultEncoding" );
112
113    }
114
115// </editor-fold>//GEN-END:jdtausProperties
116
117    //--------------------------------------------------------------Properties--
118    //--BankleitzahlenDatei-----------------------------------------------------
119
120    /** Records held by the instance. */
121    private Map records = new HashMap( 20000 );
122    private BankleitzahlInfo[] cachedRecords;
123
124    /** Encoding to use when reading bankfile resources. */
125    private String encoding;
126
127    /**
128     * Reads a Bankleitzahlendatei form an URL initializing the instance to
129     * hold its data.
130     *
131     * @param resource an URL to a Bankleitzahlendatei.
132     *
133     * @throws NullPointerException if {@code resource} is {@code null}.
134     * @throws PropertyException for invalid property values.
135     * @throws IllegalArgumentException if {@code resource} does not provide
136     * a valid Bankleitzahlendatei.
137     * @throws IOException if reading fails.
138     */
139    public BankleitzahlenDatei( final URL resource ) throws IOException
140    {
141        super();
142        this.assertValidProperties();
143        this.readBankfile( resource );
144    }
145
146    /**
147     * Reads a Bankleitzahlendatei form an URL initializing the instance to
148     * hold its data taking the encoding to use when reading the file.
149     *
150     * @param resource an URL to a Bankleitzahlendatei.
151     * @param encoding the encoding to use when reading {@code resource}.
152     *
153     * @throws NullPointerException if either {@code resource} or
154     * {@code encoding} is {@code null}.
155     * @throws PropertyException for invalid property values.
156     * @throws IllegalArgumentException if {@code resource} does not provide
157     * a valid Bankleitzahlendatei.
158     * @throws IOException if reading fails.
159     */
160    public BankleitzahlenDatei( final URL resource, final String encoding )
161        throws IOException
162    {
163        super();
164        this.encoding = encoding;
165        this.assertValidProperties();
166        this.readBankfile( resource );
167    }
168
169    /**
170     * Gets the encoding used for reading bankfile resources.
171     *
172     * @return the encoding used for reading bankfile resources.
173     */
174    public String getEncoding()
175    {
176        if ( this.encoding == null )
177        {
178            this.encoding = this.getDefaultEncoding();
179        }
180
181        return this.encoding;
182    }
183
184    /**
185     * Gets all records held by the instance.
186     *
187     * @return all records held by the instance.
188     */
189    public BankleitzahlInfo[] getRecords()
190    {
191        if ( this.cachedRecords == null )
192        {
193            this.cachedRecords = (BankleitzahlInfo[]) this.records.values().
194                toArray( new BankleitzahlInfo[ this.records.size() ] );
195
196        }
197
198        return this.cachedRecords;
199    }
200
201    /**
202     * Gets a record identified by a serial number.
203     *
204     * @param serialNumber the serial number of the record to return.
205     *
206     * @return the record with serial number {@code serialNumber} or
207     * {@code null} if no record matching {@code serialNumber} exists in the
208     * file.
209     *
210     * @throws NullPointerException if {@code serialNumber} is {@code null}.
211     */
212    public BankleitzahlInfo getRecord( final Integer serialNumber )
213    {
214        if ( serialNumber == null )
215        {
216            throw new NullPointerException( "serialNumber" );
217        }
218
219        return (BankleitzahlInfo) this.records.get( serialNumber );
220    }
221
222    /**
223     * Given a newer version of the Bankleitzahlendatei updates the records of
224     * the instance to reflect the changes.
225     *
226     * @param file a newer version of the Bankleitzahlendatei to use for
227     * updating the records of this instance.
228     *
229     * @throws NullPointerException if {@code file} is {@code null}.
230     * @throws IllegalArgumentException if {@code file} cannot be used for
231     * updating this instance.
232     */
233    public void update( final BankleitzahlenDatei file )
234    {
235        if ( file == null )
236        {
237            throw new NullPointerException( "file" );
238        }
239
240        int i;
241        final boolean log = this.getLogger().isDebugEnabled();
242        BankleitzahlInfo oldVersion;
243        BankleitzahlInfo newVersion;
244
245        int progress = 0;
246        Task task = new Task();
247        task.setIndeterminate( false );
248        task.setCancelable( false );
249        task.setDescription( new UpdatesBankleitzahlenDateiMessage() );
250        task.setMinimum( 0 );
251        task.setMaximum( file.getRecords().length );
252        task.setProgress( progress );
253
254        try
255        {
256            this.getTaskMonitor().monitor( task );
257
258            for ( i = file.getRecords().length - 1; i >= 0; i-- )
259            {
260                task.setProgress( progress++ );
261                newVersion = file.getRecords()[i];
262                if ( 'A' == newVersion.getChangeLabel() )
263                {
264                    oldVersion = (BankleitzahlInfo) this.records.get(
265                        newVersion.getSerialNumber() );
266
267                    if ( oldVersion != null &&
268                        oldVersion.getChangeLabel() != 'D' )
269                    {
270                        throw new IllegalArgumentException(
271                            this.getCannotAddDuplicateRecordMessage(
272                            this.getLocale(),
273                            newVersion.getSerialNumber() ) );
274
275                    }
276
277                    this.records.put( newVersion.getSerialNumber(), newVersion );
278
279                    if ( log )
280                    {
281                        this.getLogger().debug(
282                            this.getAddRecordInfoMessage(
283                            this.getLocale(),
284                            String.valueOf( newVersion.getChangeLabel() ),
285                            newVersion.getSerialNumber() ) );
286
287                    }
288                }
289                else if ( 'M' == newVersion.getChangeLabel() ||
290                    'D' == newVersion.getChangeLabel() )
291                {
292                    if ( this.records.put( newVersion.getSerialNumber(),
293                                           newVersion ) == null )
294                    {
295                        throw new IllegalArgumentException(
296                            this.getCannotModifyNonexistentRecordMessage(
297                            this.getLocale(), newVersion.getSerialNumber() ) );
298
299                    }
300
301                    if ( log )
302                    {
303                        this.getLogger().debug(
304                            this.getModifyRecordInfoMessage(
305                            this.getLocale(),
306                            String.valueOf( newVersion.getChangeLabel() ),
307                            newVersion.getSerialNumber() ) );
308
309                    }
310
311                }
312                else if ( 'U' == newVersion.getChangeLabel() &&
313                    !this.records.containsKey( newVersion.getSerialNumber() ) )
314                {
315                    throw new IllegalArgumentException(
316                        this.getCannotModifyNonexistentRecordMessage(
317                        this.getLocale(), newVersion.getSerialNumber() ) );
318
319                }
320            }
321        }
322        finally
323        {
324            this.getTaskMonitor().finish( task );
325        }
326
327        progress = 0;
328        task = new Task();
329        task.setIndeterminate( false );
330        task.setCancelable( false );
331        task.setDescription( new UpdatesBankleitzahlenDateiMessage() );
332        task.setMinimum( 0 );
333        task.setMaximum( this.records.size() );
334        task.setProgress( progress );
335
336        try
337        {
338            this.getTaskMonitor().monitor( task );
339            for ( Iterator it = this.records.values().iterator(); it.hasNext();)
340            {
341                task.setProgress( progress++ );
342                oldVersion = (BankleitzahlInfo) it.next();
343
344                if ( 'D' == oldVersion.getChangeLabel() )
345                {
346                    newVersion = file.getRecord( oldVersion.getSerialNumber() );
347                    if ( newVersion == null )
348                    {
349                        it.remove();
350
351                        if ( log )
352                        {
353                            this.getLogger().debug(
354                                this.getRemoveRecordInfoMessage(
355                                this.getLocale(),
356                                String.valueOf( oldVersion.getChangeLabel() ),
357                                oldVersion.getSerialNumber() ) );
358
359                        }
360                    }
361                }
362            }
363        }
364        finally
365        {
366            this.getTaskMonitor().finish( task );
367        }
368
369        this.cachedRecords = null;
370    }
371
372    /**
373     * Checks configured properties.
374     *
375     * @throws PropertyException for invalid property values.
376     */
377    private void assertValidProperties()
378    {
379        if ( this.getEncoding() == null || this.getEncoding().length() == 0 )
380        {
381            throw new PropertyException( "encoding", this.getEncoding() );
382        }
383
384        try
385        {
386            "".getBytes( this.getEncoding() );
387        }
388        catch ( UnsupportedEncodingException e )
389        {
390            throw new PropertyException( "encoding", this.getEncoding(), e );
391        }
392    }
393
394    /**
395     * Reads a Bankleitzahlendatei from an URL initializing the instance to
396     * hold its data.
397     *
398     * @param resource An URL to a Bankleitzahlendatei.
399     *
400     * @throws NullPointerException if {@code resource} is {@code null}.
401     * @throws IllegalArgumentException if {@code resource} does not provide
402     * a valid Bankleitzahlendatei.
403     * @throws IOException if reading fails.
404     */
405    private void readBankfile( final URL resource ) throws IOException
406    {
407        if ( resource == null )
408        {
409            throw new NullPointerException( "resource" );
410        }
411
412        this.records.clear();
413
414        if ( this.getLogger().isDebugEnabled() )
415        {
416            this.getLogger().debug( this.getFileNameInfoMessage(
417                this.getLocale(), resource.toExternalForm() ) );
418
419        }
420
421        InputStream stream = null;
422
423        try
424        {
425            stream = resource.openStream();
426            final LineNumberReader reader = new LineNumberReader(
427                new InputStreamReader( stream, this.getEncoding() ) );
428
429            String line;
430            boolean emptyLine = false;
431            while ( ( line = reader.readLine() ) != null )
432            {
433                if ( line.trim().length() == 0 )
434                {
435                    emptyLine = true;
436                    continue;
437                }
438
439                if ( emptyLine )
440                {
441                    throw new IllegalArgumentException(
442                        this.getUnexpectedDataMessage(
443                        this.getLocale(), new Integer( reader.getLineNumber() ),
444                        resource.toExternalForm() ) );
445
446                }
447
448
449                final BankleitzahlInfo rec = new BankleitzahlInfo();
450                rec.parse( line );
451
452                if ( this.records.put( rec.getSerialNumber(), rec ) != null )
453                {
454                    throw new IllegalArgumentException(
455                        this.getCannotAddDuplicateRecordMessage(
456                        this.getLocale(), rec.getSerialNumber() ) );
457
458                }
459            }
460        }
461        finally
462        {
463            this.cachedRecords = null;
464
465            if ( stream != null )
466            {
467                stream.close();
468            }
469        }
470    }
471
472    //-----------------------------------------------------BankleitzahlenDatei--
473    //--Messages----------------------------------------------------------------
474
475// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausMessages
476    // This section is managed by jdtaus-container-mojo.
477
478    /**
479     * Gets the text of message <code>fileNameInfo</code>.
480     * <blockquote><pre>Lädt Bankleitzahlendatei "{0}".</pre></blockquote>
481     * <blockquote><pre>Loading Bankleitzahlendatei "{0}".</pre></blockquote>
482     *
483     * @param locale The locale of the message instance to return.
484     * @param fileName format argument.
485     *
486     * @return the text of message <code>fileNameInfo</code>.
487     */
488    private String getFileNameInfoMessage( final Locale locale,
489            final java.lang.String fileName )
490    {
491        return ContainerFactory.getContainer().
492            getMessage( this, "fileNameInfo", locale,
493                new Object[]
494                {
495                    fileName
496                });
497
498    }
499
500    /**
501     * Gets the text of message <code>addRecordInfo</code>.
502     * <blockquote><pre>{0}: Datensatz {1, number} hinzugefügt.</pre></blockquote>
503     * <blockquote><pre>{0}: Added record {1, number}.</pre></blockquote>
504     *
505     * @param locale The locale of the message instance to return.
506     * @param label format argument.
507     * @param serialNumber format argument.
508     *
509     * @return the text of message <code>addRecordInfo</code>.
510     */
511    private String getAddRecordInfoMessage( final Locale locale,
512            final java.lang.String label,
513            final java.lang.Number serialNumber )
514    {
515        return ContainerFactory.getContainer().
516            getMessage( this, "addRecordInfo", locale,
517                new Object[]
518                {
519                    label,
520                    serialNumber
521                });
522
523    }
524
525    /**
526     * Gets the text of message <code>modifyRecordInfo</code>.
527     * <blockquote><pre>{0}: Datensatz {1, number} aktualisiert.</pre></blockquote>
528     * <blockquote><pre>{0}: Updated record {1, number}.</pre></blockquote>
529     *
530     * @param locale The locale of the message instance to return.
531     * @param label format argument.
532     * @param serialNumber format argument.
533     *
534     * @return the text of message <code>modifyRecordInfo</code>.
535     */
536    private String getModifyRecordInfoMessage( final Locale locale,
537            final java.lang.String label,
538            final java.lang.Number serialNumber )
539    {
540        return ContainerFactory.getContainer().
541            getMessage( this, "modifyRecordInfo", locale,
542                new Object[]
543                {
544                    label,
545                    serialNumber
546                });
547
548    }
549
550    /**
551     * Gets the text of message <code>removeRecordInfo</code>.
552     * <blockquote><pre>{0}: Datensatz {1, number} entfernt.</pre></blockquote>
553     * <blockquote><pre>{0}: Removed record {1, number}.</pre></blockquote>
554     *
555     * @param locale The locale of the message instance to return.
556     * @param label format argument.
557     * @param serialNumber format argument.
558     *
559     * @return the text of message <code>removeRecordInfo</code>.
560     */
561    private String getRemoveRecordInfoMessage( final Locale locale,
562            final java.lang.String label,
563            final java.lang.Number serialNumber )
564    {
565        return ContainerFactory.getContainer().
566            getMessage( this, "removeRecordInfo", locale,
567                new Object[]
568                {
569                    label,
570                    serialNumber
571                });
572
573    }
574
575    /**
576     * Gets the text of message <code>cannotAddDuplicateRecord</code>.
577     * <blockquote><pre>Datensatz mit Seriennummer {0,number} existiert bereits und kann nicht hinzugefügt werden.</pre></blockquote>
578     * <blockquote><pre>Record with serial number {0,number} already exists and cannot be added.</pre></blockquote>
579     *
580     * @param locale The locale of the message instance to return.
581     * @param serialNumber format argument.
582     *
583     * @return the text of message <code>cannotAddDuplicateRecord</code>.
584     */
585    private String getCannotAddDuplicateRecordMessage( final Locale locale,
586            final java.lang.Number serialNumber )
587    {
588        return ContainerFactory.getContainer().
589            getMessage( this, "cannotAddDuplicateRecord", locale,
590                new Object[]
591                {
592                    serialNumber
593                });
594
595    }
596
597    /**
598     * Gets the text of message <code>cannotModifyNonexistentRecord</code>.
599     * <blockquote><pre>Ein Datensatz mit Seriennummer {0,number} existiert nicht und kann nicht aktualisiert werden.</pre></blockquote>
600     * <blockquote><pre>Record with serial number {0,number} does not exist and cannot be updated.</pre></blockquote>
601     *
602     * @param locale The locale of the message instance to return.
603     * @param serialNumber format argument.
604     *
605     * @return the text of message <code>cannotModifyNonexistentRecord</code>.
606     */
607    private String getCannotModifyNonexistentRecordMessage( final Locale locale,
608            final java.lang.Number serialNumber )
609    {
610        return ContainerFactory.getContainer().
611            getMessage( this, "cannotModifyNonexistentRecord", locale,
612                new Object[]
613                {
614                    serialNumber
615                });
616
617    }
618
619    /**
620     * Gets the text of message <code>unexpectedData</code>.
621     * <blockquote><pre>Unerwartete Daten in Zeile {0,number} bei der Verarbeitung von {1}.</pre></blockquote>
622     * <blockquote><pre>Unexpected data at line {0,number} processing {1}.</pre></blockquote>
623     *
624     * @param locale The locale of the message instance to return.
625     * @param lineNumber format argument.
626     * @param resourceName format argument.
627     *
628     * @return the text of message <code>unexpectedData</code>.
629     */
630    private String getUnexpectedDataMessage( final Locale locale,
631            final java.lang.Number lineNumber,
632            final java.lang.String resourceName )
633    {
634        return ContainerFactory.getContainer().
635            getMessage( this, "unexpectedData", locale,
636                new Object[]
637                {
638                    lineNumber,
639                    resourceName
640                });
641
642    }
643
644// </editor-fold>//GEN-END:jdtausMessages
645
646    //----------------------------------------------------------------Messages--
647}