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