001/*
002 *  jDTAUS Banking RI Textschluesselverzeichnis
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.ri.txtdirectory;
022
023import java.io.File;
024import java.io.IOException;
025import java.io.InputStream;
026import java.net.URI;
027import java.net.URISyntaxException;
028import java.net.URL;
029import java.text.DateFormat;
030import java.text.ParseException;
031import java.text.SimpleDateFormat;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Calendar;
035import java.util.Collection;
036import java.util.Date;
037import java.util.HashMap;
038import java.util.HashSet;
039import java.util.Iterator;
040import java.util.LinkedList;
041import java.util.List;
042import java.util.Locale;
043import java.util.Map;
044import javax.xml.parsers.DocumentBuilder;
045import javax.xml.parsers.DocumentBuilderFactory;
046import javax.xml.parsers.ParserConfigurationException;
047import org.jdtaus.banking.Textschluessel;
048import org.jdtaus.banking.TextschluesselVerzeichnis;
049import org.jdtaus.banking.messages.ReadsTextschluesselMessage;
050import org.jdtaus.banking.messages.SearchesTextschluesselMessage;
051import org.jdtaus.core.container.ContainerFactory;
052import org.jdtaus.core.container.PropertyException;
053import org.jdtaus.core.logging.spi.Logger;
054import org.jdtaus.core.monitor.spi.Task;
055import org.jdtaus.core.monitor.spi.TaskMonitor;
056import org.jdtaus.core.sax.util.EntityResolverChain;
057import org.w3c.dom.Document;
058import org.w3c.dom.Element;
059import org.w3c.dom.NodeList;
060import org.xml.sax.ErrorHandler;
061import org.xml.sax.SAXException;
062import org.xml.sax.SAXParseException;
063
064/**
065 * Textschlüssel directory implementation backed by XML files.
066 * <p>This implementation uses XML resources provided by any available {@link JaxpTextschluesselProvider}
067 * implementation. Resources with a {@code file} URI scheme are monitored for changes by querying the last modification
068 * time. Monitoring is controlled by property {@code reloadIntervalMillis}.</p>
069 *
070 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
071 * @version $JDTAUS: JaxpTextschluesselVerzeichnis.java 8865 2014-01-10 17:13:42Z schulte $
072 */
073public class JaxpTextschluesselVerzeichnis implements TextschluesselVerzeichnis
074{
075
076    /** JAXP configuration key to the Schema implementation attribute. */
077    private static final String SCHEMA_LANGUAGE_KEY = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
078
079    /**
080     * JAXP Schema implementation to use.
081     * @see javax.xml.XMLConstants#W3C_XML_SCHEMA_NS_URI
082     */
083    private static final String SCHEMA_LANGUAGE = "http://www.w3.org/2001/XMLSchema";
084
085    /** jDTAUS {@code textschluessel} namespace URI. */
086    private static final String TEXTSCHLUESSEL_NS = "http://jdtaus.org/banking/xml/textschluessel";
087
088    /** jDTAUS {@code banking} namespace URI. */
089    private static final String BANKING_NS = "http://jdtaus.org/banking/model";
090
091    /** {@code http://www.w3.org/2001/XMLSchema-instance} namespace URI. */
092    private static final String XSI_NS = "http://www.w3.org/2001/XMLSchema-instance";
093
094    /** Version supported by this implementation. */
095    private static final String[] SUPPORTED_VERSIONS =
096    {
097        "1.0", "1.1"
098    };
099
100    /* Flag indicating that initialization has been performed. */
101    private boolean initialized;
102
103    /** Holds the loaded Textschlüssel instances. */
104    private Textschluessel[] instances;
105
106    /**
107     * Holds mappings of Textschlüssel keys to instances.
108     * @since 1.15
109     */
110    private final Map textschluesselMap = new HashMap();
111
112    /**
113     * Holds mappings of Textschlüssel keys to instances with variable extension.
114     * @since 1.15
115     */
116    private final Map variableTextschluesselMap = new HashMap();
117
118    /** Maps {@code File} instances to theire last modification timestamp. */
119    private final Map monitorMap = new HashMap();
120
121    /** Holds the timestamp resources got checked for modifications. */
122    private long lastCheck = System.currentTimeMillis();
123
124    /** Number of milliseconds to pass before resources are checked for modifications. */
125    private Long reloadIntervalMillis;
126
127    /** Number of Textschluessel for which progress monitoring gets enabled. */
128    private Long monitoringThreshold;
129
130    /**
131     * Creates a new {@code XMLTextschluesselVerzeichnis} instance taking the number of milliseconds to pass before
132     * resources are checked for modifications and the number of Textschluessel for which progress monitoring gets
133     * enabled.
134     *
135     * @param reloadIntervalMillis Number of milliseconds to pass before resources are checked for modifications.
136     * @param monitoringThreshold Number of Textschluessel for which progress monitoring gets enabled.
137     */
138    public JaxpTextschluesselVerzeichnis( final long reloadIntervalMillis, final long monitoringThreshold )
139    {
140        this();
141        if ( reloadIntervalMillis > 0 )
142        {
143            this.reloadIntervalMillis = new Long( reloadIntervalMillis );
144        }
145        if ( monitoringThreshold > 0 )
146        {
147            this.monitoringThreshold = new Long( monitoringThreshold );
148        }
149    }
150
151    /**
152     * Gets the number of milliseconds to pass before resources are checked for modifications.
153     *
154     * @return The number of milliseconds to pass before resources are checked for modifications.
155     */
156    public long getReloadIntervalMillis()
157    {
158        if ( this.reloadIntervalMillis == null )
159        {
160            this.reloadIntervalMillis = this.getDefaultReloadIntervalMillis();
161        }
162
163        return this.reloadIntervalMillis.longValue();
164    }
165
166    /**
167     * Gets the number of Textschluessel for which progress monitoring gets enabled.
168     *
169     * @return The number of Textschluessel for which progress monitoring gets enabled.
170     */
171    public long getMonitoringThreshold()
172    {
173        if ( this.monitoringThreshold == null )
174        {
175            this.monitoringThreshold = this.getDefaultMonitoringThreshold();
176        }
177
178        return this.monitoringThreshold.longValue();
179    }
180
181    public Textschluessel[] getTextschluessel()
182    {
183        this.assertValidProperties();
184        this.assertInitialized();
185        return this.searchTextschluessel( null, null, null );
186    }
187
188    public Textschluessel getTextschluessel( final int key, final int extension )
189    {
190        if ( key < 0 || key > 99 )
191        {
192            throw new IllegalArgumentException( Integer.toString( key ) );
193        }
194        if ( extension < 0 || extension > 999 )
195        {
196            throw new IllegalArgumentException( Integer.toString( extension ) );
197        }
198
199        this.assertValidProperties();
200        this.assertInitialized();
201
202        Textschluessel textschluessel = null;
203        final Number keyObject = new Integer( key );
204        final Map extensionMap = (Map) this.textschluesselMap.get( keyObject );
205
206        if ( extensionMap != null )
207        {
208            textschluessel = (Textschluessel) extensionMap.get( new Integer( extension ) );
209
210            if ( textschluessel != null )
211            {
212                textschluessel = (Textschluessel) textschluessel.clone();
213            }
214        }
215
216        if ( textschluessel == null )
217        {
218            textschluessel = (Textschluessel) this.variableTextschluesselMap.get( keyObject );
219
220            if ( textschluessel != null )
221            {
222                textschluessel = (Textschluessel) textschluessel.clone();
223                textschluessel.setExtension( extension );
224            }
225        }
226
227        return textschluessel;
228    }
229
230    public Textschluessel getTextschluessel( final int key, final int extension, final Date date )
231    {
232        if ( key < 0 || key > 99 )
233        {
234            throw new IllegalArgumentException( Integer.toString( key ) );
235        }
236        if ( extension < 0 || extension > 999 )
237        {
238            throw new IllegalArgumentException( Integer.toString( extension ) );
239        }
240        if ( date == null )
241        {
242            throw new NullPointerException( "date" );
243        }
244
245        this.assertValidProperties();
246        this.assertInitialized();
247
248        final Textschluessel textschluessel = this.getTextschluessel( key, extension );
249        return textschluessel != null && textschluessel.isValidAt( date ) ? textschluessel : null;
250    }
251
252    public final Textschluessel[] search( final boolean debit, final boolean remittance )
253    {
254        return this.searchTextschluessel( Boolean.valueOf( debit ), Boolean.valueOf( remittance ), null );
255    }
256
257    public final Textschluessel[] search( final boolean debit, final boolean remittance, final Date date )
258    {
259        if ( date == null )
260        {
261            throw new NullPointerException( "date" );
262        }
263
264        return this.searchTextschluessel( Boolean.valueOf( debit ), Boolean.valueOf( remittance ), date );
265    }
266
267    public Textschluessel[] searchTextschluessel( final Boolean debit, final Boolean remittance, final Date date )
268    {
269        this.assertValidProperties();
270        this.assertInitialized();
271
272        final Collection col = new ArrayList( this.instances.length );
273
274        if ( this.instances.length > 0 )
275        {
276            final Task task = new Task();
277            task.setCancelable( true );
278            task.setDescription( new SearchesTextschluesselMessage() );
279            task.setIndeterminate( false );
280            task.setMaximum( this.instances.length - 1 );
281            task.setMinimum( 0 );
282            task.setProgress( 0 );
283
284            try
285            {
286                if ( task.getMaximum() > this.getMonitoringThreshold() )
287                {
288                    this.getTaskMonitor().monitor( task );
289                }
290
291                for ( int i = this.instances.length - 1; i >= 0 && !task.isCancelled(); i-- )
292                {
293                    task.setProgress( task.getMaximum() - i );
294
295                    if ( ( debit == null ? true : this.instances[i].isDebit() == debit.booleanValue() )
296                         && ( remittance == null ? true : this.instances[i].isRemittance() == remittance.booleanValue() )
297                         && ( date == null ? true : this.instances[i].isValidAt( date ) ) )
298                    {
299                        col.add( this.instances[i].clone() );
300                    }
301                }
302
303                if ( task.isCancelled() )
304                {
305                    col.clear();
306                }
307
308            }
309            finally
310            {
311                if ( task.getMaximum() > this.getMonitoringThreshold() )
312                {
313                    this.getTaskMonitor().finish( task );
314                }
315            }
316        }
317
318        return (Textschluessel[]) col.toArray( new Textschluessel[ col.size() ] );
319    }
320
321    /**
322     * Initializes the instance to hold the parsed XML Textschluessel instances.
323     *
324     * @see #assertValidProperties()
325     * @see #parseResources()
326     * @see #transformDocument(Document)
327     */
328    private synchronized void assertInitialized()
329    {
330        try
331        {
332            if ( System.currentTimeMillis() - this.lastCheck > this.getReloadIntervalMillis()
333                 && !this.monitorMap.isEmpty() )
334            {
335                this.lastCheck = System.currentTimeMillis();
336                for ( final Iterator it = this.monitorMap.entrySet().iterator(); it.hasNext(); )
337                {
338                    final Map.Entry entry = (Map.Entry) it.next();
339                    final File file = (File) entry.getKey();
340                    final Long lastModified = (Long) entry.getValue();
341
342                    assert lastModified != null : "Expected modification time.";
343
344                    if ( file.lastModified() != lastModified.longValue() )
345                    {
346                        this.getLogger().info( this.getChangeInfoMessage( this.getLocale(), file.getAbsolutePath() ) );
347                        this.initialized = false;
348                        break;
349                    }
350                }
351            }
352
353            if ( !this.initialized )
354            {
355                this.monitorMap.clear();
356                this.textschluesselMap.clear();
357                this.variableTextschluesselMap.clear();
358
359                final List/*<Document>*/ documents = this.parseResources();
360                final Collection parsedTextschluessel = new LinkedList();
361
362                for ( final Iterator it = documents.iterator(); it.hasNext(); )
363                {
364                    final Document document = (Document) it.next();
365                    parsedTextschluessel.addAll( this.transformDocument( document ) );
366                }
367
368                final Collection checked = new ArrayList( parsedTextschluessel.size() );
369
370                for ( final Iterator it = parsedTextschluessel.iterator(); it.hasNext(); )
371                {
372                    final Textschluessel i = (Textschluessel) it.next();
373                    final Integer key = new Integer( i.getKey() );
374                    final Integer ext = new Integer( i.getExtension() );
375
376                    if ( i.isVariable() )
377                    {
378                        if ( this.textschluesselMap.containsKey( key )
379                             || this.variableTextschluesselMap.put( key, i ) != null )
380                        {
381                            throw new IllegalStateException( this.getDuplicateTextschluesselMessage(
382                                this.getLocale(), key, ext ) );
383
384                        }
385                    }
386                    else
387                    {
388                        Map extensionsMap = (Map) this.textschluesselMap.get( key );
389
390                        if ( extensionsMap == null )
391                        {
392                            extensionsMap = new HashMap();
393                            this.textschluesselMap.put( key, extensionsMap );
394                        }
395
396                        if ( extensionsMap.put( ext, i ) != null )
397                        {
398                            throw new IllegalStateException( this.getDuplicateTextschluesselMessage(
399                                this.getLocale(), key, ext ) );
400
401                        }
402                    }
403
404                    checked.add( i );
405                }
406
407                this.instances = (Textschluessel[]) checked.toArray( new Textschluessel[ checked.size() ] );
408                this.getLogger().info( this.getTextschluesselInfoMessage(
409                    this.getLocale(), new Integer( this.instances.length ), new Integer( documents.size() ) ) );
410
411                this.initialized = true;
412            }
413        }
414        catch ( final IOException e )
415        {
416            this.initialized = false;
417            throw new RuntimeException( e );
418        }
419        catch ( final SAXException e )
420        {
421            this.initialized = false;
422            throw new RuntimeException( e );
423        }
424        catch ( final ParserConfigurationException e )
425        {
426            this.initialized = false;
427            throw new RuntimeException( e );
428        }
429        catch ( final ParseException e )
430        {
431            this.initialized = false;
432            throw new RuntimeException( e );
433        }
434    }
435
436    /**
437     * Checks configured properties.
438     *
439     * @throws PropertyException if properties hold invalid values.
440     */
441    private void assertValidProperties()
442    {
443        if ( this.getReloadIntervalMillis() < 0L )
444        {
445            throw new PropertyException( "reloadIntervalMillis", Long.toString( this.getReloadIntervalMillis() ) );
446        }
447        if ( this.getMonitoringThreshold() < 0L )
448        {
449            throw new PropertyException( "monitoringThreshold", Long.toString( this.getMonitoringThreshold() ) );
450        }
451    }
452
453    /**
454     * Gets XML resources provided by any available {@code TextschluesselProvider} implementation.
455     *
456     * @return XML resources provided by any available {@code TextschluesselProvider} implementation.
457     *
458     * @throws IOException if retrieving the resources fails.
459     *
460     * @see JaxpTextschluesselProvider
461     */
462    private URL[] getResources() throws IOException
463    {
464        final Collection resources = new HashSet();
465        final JaxpTextschluesselProvider[] provider = this.getTextschluesselProvider();
466
467        for ( int i = provider.length - 1; i >= 0; i-- )
468        {
469            resources.addAll( Arrays.asList( provider[i].getResources() ) );
470        }
471
472        return (URL[]) resources.toArray( new URL[ resources.size() ] );
473    }
474
475    /**
476     * Adds a resource to the list of resources to monitor for changes.
477     *
478     * @param url the URL of the resource to monitor for changes.
479     *
480     * @throws NullPointerException if {@code url} is {@code null}.
481     */
482    private void monitorResource( final URL url )
483    {
484        if ( url == null )
485        {
486            throw new NullPointerException( "url" );
487        }
488
489        try
490        {
491            final File file = new File( new URI( url.toString() ) );
492            this.monitorMap.put( file, new Long( file.lastModified() ) );
493            this.getLogger().info( this.getMonitoringInfoMessage( this.getLocale(), file.getAbsolutePath() ) );
494        }
495        catch ( final IllegalArgumentException e )
496        {
497            this.getLogger().info( this.getNotMonitoringWarningMessage(
498                this.getLocale(), url.toExternalForm(), e.getMessage() ) );
499
500        }
501        catch ( final URISyntaxException e )
502        {
503            this.getLogger().info( this.getNotMonitoringWarningMessage(
504                this.getLocale(), url.toExternalForm(), e.getMessage() ) );
505
506        }
507    }
508
509    /**
510     * Parses all XML resources.
511     *
512     * @return the parsed XML documents.
513     *
514     * @see #getResources()
515     * @see #getDocumentBuilder()
516     *
517     * @throws ParserConfigurationException if configuring the parser fails.
518     * @throws IOException if reading resources fails.
519     * @throws SAXException if parsing fails.
520     */
521    private List/*<Document>*/ parseResources() throws ParserConfigurationException, IOException, SAXException
522    {
523        InputStream stream = null;
524
525        final URL[] resources = this.getResources();
526        final List documents = new LinkedList();
527
528        if ( resources.length > 0 )
529        {
530            final DocumentBuilder validatingParser = this.getDocumentBuilder();
531            final DocumentBuilderFactory namespaceAwareFactory = DocumentBuilderFactory.newInstance();
532
533            namespaceAwareFactory.setNamespaceAware( true );
534            final DocumentBuilder nonValidatingParser = namespaceAwareFactory.newDocumentBuilder();
535
536            final Task task = new Task();
537            task.setCancelable( false );
538            task.setDescription( new ReadsTextschluesselMessage() );
539            task.setIndeterminate( false );
540            task.setMaximum( resources.length - 1 );
541            task.setMinimum( 0 );
542            task.setProgress( 0 );
543
544            try
545            {
546                this.getTaskMonitor().monitor( task );
547
548                for ( int i = resources.length - 1; i >= 0; i-- )
549                {
550                    task.setProgress( task.getMaximum() - i );
551                    final URL resource = resources[i];
552                    final ErrorHandler errorHandler = new ErrorHandler()
553                    {
554
555                        public void warning( final SAXParseException e )
556                            throws SAXException
557                        {
558                            getLogger().warn( getParseExceptionMessage(
559                                getLocale(), resource.toExternalForm(),
560                                e.getMessage(), new Integer( e.getLineNumber() ),
561                                new Integer( e.getColumnNumber() ) ) );
562
563                        }
564
565                        public void error( final SAXParseException e )
566                            throws SAXException
567                        {
568                            throw new SAXException( getParseExceptionMessage(
569                                getLocale(), resource.toExternalForm(),
570                                e.getMessage(), new Integer( e.getLineNumber() ),
571                                new Integer( e.getColumnNumber() ) ), e );
572
573                        }
574
575                        public void fatalError( final SAXParseException e )
576                            throws SAXException
577                        {
578                            throw new SAXException( getParseExceptionMessage(
579                                getLocale(), resource.toExternalForm(),
580                                e.getMessage(), new Integer( e.getLineNumber() ),
581                                new Integer( e.getColumnNumber() ) ), e );
582
583                        }
584
585                    };
586
587                    nonValidatingParser.setErrorHandler( errorHandler );
588                    validatingParser.setErrorHandler( errorHandler );
589
590                    this.monitorResource( resource );
591                    stream = resource.openStream();
592                    Document doc = nonValidatingParser.parse( stream );
593                    if ( doc.getDocumentElement().hasAttributeNS( XSI_NS, "schemaLocation" ) )
594                    {
595                        stream.close();
596                        stream = resource.openStream();
597                        doc = validatingParser.parse( stream );
598                    }
599                    else if ( this.getLogger().isInfoEnabled() )
600                    {
601                        this.getLogger().info(
602                            this.getNoSchemaLocationMessage( this.getLocale(), resource.toExternalForm() ) );
603                    }
604
605                    documents.add( doc );
606                    stream.close();
607                }
608            }
609            finally
610            {
611                this.getTaskMonitor().finish( task );
612            }
613        }
614        else
615        {
616            this.getLogger().warn( this.getNoTextschluesselFoundMessage( this.getLocale() ) );
617        }
618
619        return documents;
620    }
621
622    /**
623     * Transforms a document to the Textschluessel instances it contains.
624     *
625     * @param doc the document to transform.
626     *
627     * @return an array of Textschluessel instances from the given document.
628     *
629     * @throws IllegalArgumentException if {@code doc} cannot be transformed.
630     * @throws ParseException if parsing fails.
631     *
632     * @see #transformTextschluesselDocument(Document)
633     * @see #transformBankingDocument(Document)
634     */
635    private List/*<Textschluessel>*/ transformDocument( final Document doc ) throws ParseException
636    {
637        String modelVersion = null;
638        final String namespace = doc.getDocumentElement().getNamespaceURI();
639
640        if ( namespace == null )
641        {
642            throw new RuntimeException( this.getUnsupportedNamespaceMessage( this.getLocale(), null ) );
643        }
644        else if ( TEXTSCHLUESSEL_NS.equals( namespace ) )
645        {
646            modelVersion = doc.getDocumentElement().getAttributeNS( namespace, "version" );
647        }
648        else if ( BANKING_NS.equals( namespace ) )
649        {
650            modelVersion = doc.getDocumentElement().getAttributeNS( namespace, "modelVersion" );
651        }
652        else
653        {
654            throw new RuntimeException( this.getUnsupportedNamespaceMessage( this.getLocale(), namespace ) );
655        }
656
657        boolean supportedModelVersion = false;
658        for ( int i = SUPPORTED_VERSIONS.length - 1; i >= 0; i-- )
659        {
660            if ( SUPPORTED_VERSIONS[i].equals( modelVersion ) )
661            {
662                supportedModelVersion = true;
663                break;
664            }
665        }
666
667        if ( !supportedModelVersion )
668        {
669            throw new RuntimeException( this.getUnsupportedModelVersionMessage( this.getLocale(), modelVersion ) );
670        }
671
672        final List textschluessel = new LinkedList();
673
674        if ( namespace.equals( TEXTSCHLUESSEL_NS ) )
675        {
676            textschluessel.addAll( this.transformTextschluesselDocument( doc ) );
677        }
678        else if ( namespace.equals( BANKING_NS ) )
679        {
680            textschluessel.addAll( this.transformBankingDocument( doc ) );
681        }
682
683        return textschluessel;
684    }
685
686    /**
687     * Transforms a document from deprecated {@code textschluessel} namespace to the {@code Textschluessel} instances it
688     * contains.
689     *
690     * @param doc the document to transform.
691     *
692     * @return an list of Textschluessel instances from the given document.
693     *
694     * @throws IllegalArgumentException if {@code doc} contains invalid content.
695     */
696    private List/*<Textschluessel>*/ transformTextschluesselDocument( final Document doc )
697    {
698        final List list = new LinkedList();
699        final NodeList typeList = doc.getDocumentElement().getElementsByTagNameNS(
700            TEXTSCHLUESSEL_NS, "transactionTypes" );
701
702        for ( int i = typeList.getLength() - 1; i >= 0; i-- )
703        {
704            final Element parent = (Element) typeList.item( i );
705            if ( parent.getParentNode().equals( doc.getDocumentElement() ) )
706            {
707                final NodeList type = parent.getElementsByTagNameNS( TEXTSCHLUESSEL_NS, "transactionType" );
708                for ( int t = type.getLength() - 1; t >= 0; t-- )
709                {
710                    final Element e = (Element) type.item( t );
711                    if ( e.getParentNode().equals( parent ) )
712                    {
713                        final Textschluessel textschluessel = new Textschluessel();
714                        list.add( textschluessel );
715
716                        final String textschluesselType = e.getAttributeNS( TEXTSCHLUESSEL_NS, "type" );
717                        textschluessel.setDebit( "DEBIT".equals( textschluesselType ) );
718                        textschluessel.setRemittance( "REMITTANCE".equals( textschluesselType ) );
719                        textschluessel.setKey( Integer.valueOf(
720                            e.getAttributeNS( TEXTSCHLUESSEL_NS, "key" ) ).intValue() );
721
722                        final String extension = e.getAttributeNS( TEXTSCHLUESSEL_NS, "extension" );
723                        if ( "VARIABLE".equals( extension ) )
724                        {
725                            textschluessel.setVariable( true );
726                            textschluessel.setExtension( 0 );
727                        }
728                        else
729                        {
730                            textschluessel.setExtension( Integer.valueOf( extension ).intValue() );
731                        }
732
733                        final NodeList descriptions = e.getElementsByTagNameNS( TEXTSCHLUESSEL_NS, "description" );
734                        for ( int d = descriptions.getLength() - 1; d >= 0; d-- )
735                        {
736                            final Element description = (Element) descriptions.item( d );
737
738                            if ( description.getParentNode().equals( e ) )
739                            {
740                                final String language = description.getAttributeNS( TEXTSCHLUESSEL_NS, "language" );
741                                final String text = description.getFirstChild().getNodeValue();
742                                textschluessel.setShortDescription( new Locale( language.toLowerCase() ), text );
743                            }
744                        }
745                    }
746                }
747            }
748        }
749
750        return list;
751    }
752
753    /**
754     * Transforms a document from the {@code banking} namespace to the {@code Textschluessel} instances it contains.
755     *
756     * @param doc the document to transform.
757     *
758     * @return an list of Textschluessel instances from the given document.
759     *
760     * @throws IllegalArgumentException if {@code doc} contains invalid content.
761     * @throws ParseException if parsing fails.
762     */
763    private List/*<Textschluessel>*/ transformBankingDocument( final Document doc ) throws ParseException
764    {
765        final List list = new LinkedList();
766        final Calendar cal = Calendar.getInstance();
767        final DateFormat dateFormat = new SimpleDateFormat( "yyyy-MM-dd" );
768        final String systemLanguage = Locale.getDefault().getLanguage().toLowerCase();
769
770        final NodeList typeList = doc.getDocumentElement().getElementsByTagNameNS( BANKING_NS, "textschluessel" );
771        for ( int i = typeList.getLength() - 1; i >= 0; i-- )
772        {
773            final Element e = (Element) typeList.item( i );
774            if ( e.getParentNode().equals( doc.getDocumentElement() ) )
775            {
776                final Textschluessel textschluessel = new Textschluessel();
777                list.add( textschluessel );
778
779                textschluessel.setKey( Integer.valueOf( e.getAttributeNS( BANKING_NS, "key" ) ).intValue() );
780                if ( e.hasAttributeNS( BANKING_NS, "extension" ) )
781                {
782                    textschluessel.setExtension( Integer.valueOf(
783                        e.getAttributeNS( BANKING_NS, "extension" ) ).intValue() );
784
785                }
786
787                textschluessel.setDebit( Boolean.valueOf( e.getAttributeNS( BANKING_NS, "debit" ) ).booleanValue() );
788                textschluessel.setRemittance( Boolean.valueOf(
789                    e.getAttributeNS( BANKING_NS, "remittance" ) ).booleanValue() );
790
791                textschluessel.setVariable( Boolean.valueOf(
792                    e.getAttributeNS( BANKING_NS, "variableExtension" ) ).booleanValue() );
793
794                final NodeList texts = e.getElementsByTagNameNS( BANKING_NS, "texts" );
795                if ( e.hasAttributeNS( BANKING_NS, "validFrom" ) )
796                {
797                    cal.setTime( dateFormat.parse( e.getAttributeNS( BANKING_NS, "validFrom" ) ) );
798                    cal.set( Calendar.HOUR_OF_DAY, 0 );
799                    cal.set( Calendar.MINUTE, 0 );
800                    cal.set( Calendar.SECOND, 0 );
801                    cal.set( Calendar.MILLISECOND, 0 );
802                    textschluessel.setValidFrom( cal.getTime() );
803                }
804
805                if ( e.hasAttributeNS( BANKING_NS, "validTo" ) )
806                {
807                    cal.setTime( dateFormat.parse( e.getAttributeNS( BANKING_NS, "validTo" ) ) );
808                    cal.set( Calendar.HOUR_OF_DAY, 0 );
809                    cal.set( Calendar.MINUTE, 0 );
810                    cal.set( Calendar.SECOND, 0 );
811                    cal.set( Calendar.MILLISECOND, 0 );
812                    textschluessel.setValidTo( cal.getTime() );
813                }
814
815                for ( int t = texts.getLength() - 1; t >= 0; t-- )
816                {
817                    final Element textsElement = (Element) texts.item( t );
818                    if ( textsElement.getParentNode().equals( e ) )
819                    {
820                        final String defaultLanguage =
821                            textsElement.getAttributeNS( BANKING_NS, "defaultLanguage" ).toLowerCase();
822
823                        boolean hasSystemLanguage = false;
824                        String defaultText = null;
825
826                        final NodeList l = textsElement.getElementsByTagNameNS( BANKING_NS, "text" );
827
828                        for ( int d = l.getLength() - 1; d >= 0; d-- )
829                        {
830                            final Element description = (Element) l.item( d );
831                            if ( description.getParentNode().equals( textsElement ) )
832                            {
833                                final String language = description.getAttributeNS(
834                                    BANKING_NS, "language" ).toLowerCase();
835
836                                final String text = description.getFirstChild().getNodeValue();
837
838                                if ( language.equals( defaultLanguage ) )
839                                {
840                                    defaultText = text;
841                                }
842
843                                if ( systemLanguage.equals( language ) )
844                                {
845                                    hasSystemLanguage = true;
846                                }
847
848                                textschluessel.setShortDescription( new Locale( language ), text );
849                            }
850                        }
851
852                        if ( !hasSystemLanguage )
853                        {
854                            textschluessel.setShortDescription( new Locale( systemLanguage ), defaultText );
855                        }
856                    }
857                }
858            }
859        }
860
861        return list;
862    }
863
864    /**
865     * Creates a new {@code DocumentBuilder} to use for parsing the XML resources.
866     * <p>This method tries to set the following JAXP property on the system's default XML parser:
867     * <ul>
868     * <li>{@code http://java.sun.com/xml/jaxp/properties/schemaLanguage} set to
869     * {@code http://www.w3.org/2001/XMLSchema}</li>
870     * </ul>When setting this property fails, a non-validating {@code DocumentBuilder} is returned and a warning message
871     * is logged.</p>
872     *
873     * @return a new {@code DocumentBuilder} to be used for parsing resources.
874     *
875     * @throws ParserConfigurationException if configuring the XML parser fails.
876     */
877    private DocumentBuilder getDocumentBuilder() throws ParserConfigurationException
878    {
879        final DocumentBuilder xmlBuilder;
880        final DocumentBuilderFactory xmlFactory = DocumentBuilderFactory.newInstance();
881        xmlFactory.setNamespaceAware( true );
882
883        try
884        {
885            xmlFactory.setValidating( true );
886            xmlFactory.setAttribute( SCHEMA_LANGUAGE_KEY, SCHEMA_LANGUAGE );
887        }
888        catch ( IllegalArgumentException e )
889        {
890            this.getLogger().info( this.getNoJAXPValidationWarningMessage( this.getLocale(), e.getMessage() ) );
891            xmlFactory.setValidating( false );
892        }
893
894        xmlBuilder = xmlFactory.newDocumentBuilder();
895        xmlBuilder.setEntityResolver( new EntityResolverChain() );
896        return xmlBuilder;
897    }
898
899    //--Constructors------------------------------------------------------------
900
901// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausConstructors
902    // This section is managed by jdtaus-container-mojo.
903
904    /** Standard implementation constructor <code>org.jdtaus.banking.ri.txtdirectory.JaxpTextschluesselVerzeichnis</code>. */
905    public JaxpTextschluesselVerzeichnis()
906    {
907        super();
908    }
909
910// </editor-fold>//GEN-END:jdtausConstructors
911
912    //------------------------------------------------------------Constructors--
913    //--Dependencies------------------------------------------------------------
914
915// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausDependencies
916    // This section is managed by jdtaus-container-mojo.
917
918    /**
919     * Gets the configured <code>Logger</code> implementation.
920     *
921     * @return The configured <code>Logger</code> implementation.
922     */
923    private Logger getLogger()
924    {
925        return (Logger) ContainerFactory.getContainer().
926            getDependency( this, "Logger" );
927
928    }
929
930    /**
931     * Gets the configured <code>TextschluesselProvider</code> implementation.
932     *
933     * @return The configured <code>TextschluesselProvider</code> implementation.
934     */
935    private JaxpTextschluesselProvider[] getTextschluesselProvider()
936    {
937        return (JaxpTextschluesselProvider[]) ContainerFactory.getContainer().
938            getDependency( this, "TextschluesselProvider" );
939
940    }
941
942    /**
943     * Gets the configured <code>TaskMonitor</code> implementation.
944     *
945     * @return The configured <code>TaskMonitor</code> implementation.
946     */
947    private TaskMonitor getTaskMonitor()
948    {
949        return (TaskMonitor) ContainerFactory.getContainer().
950            getDependency( this, "TaskMonitor" );
951
952    }
953
954    /**
955     * Gets the configured <code>Locale</code> implementation.
956     *
957     * @return The configured <code>Locale</code> implementation.
958     */
959    private Locale getLocale()
960    {
961        return (Locale) ContainerFactory.getContainer().
962            getDependency( this, "Locale" );
963
964    }
965
966// </editor-fold>//GEN-END:jdtausDependencies
967
968    //------------------------------------------------------------Dependencies--
969    //--Properties--------------------------------------------------------------
970
971// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausProperties
972    // This section is managed by jdtaus-container-mojo.
973
974    /**
975     * Gets the value of property <code>defaultReloadIntervalMillis</code>.
976     *
977     * @return Default number of milliseconds to pass before resources are checked for modifications.
978     */
979    private java.lang.Long getDefaultReloadIntervalMillis()
980    {
981        return (java.lang.Long) ContainerFactory.getContainer().
982            getProperty( this, "defaultReloadIntervalMillis" );
983
984    }
985
986    /**
987     * Gets the value of property <code>defaultMonitoringThreshold</code>.
988     *
989     * @return Default number of Textschlüssel for which progress monitoring gets enabled.
990     */
991    private java.lang.Long getDefaultMonitoringThreshold()
992    {
993        return (java.lang.Long) ContainerFactory.getContainer().
994            getProperty( this, "defaultMonitoringThreshold" );
995
996    }
997
998// </editor-fold>//GEN-END:jdtausProperties
999
1000    //--------------------------------------------------------------Properties--
1001    //--Messages----------------------------------------------------------------
1002
1003// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausMessages
1004    // This section is managed by jdtaus-container-mojo.
1005
1006    /**
1007     * Gets the text of message <code>noJAXPValidationWarning</code>.
1008     * <blockquote><pre>Keine JAXP Validierung verfügbar. {0}</pre></blockquote>
1009     * <blockquote><pre>No JAXP validation available. {0}</pre></blockquote>
1010     *
1011     * @param locale The locale of the message instance to return.
1012     * @param detailMessage format parameter.
1013     *
1014     * @return the text of message <code>noJAXPValidationWarning</code>.
1015     */
1016    private String getNoJAXPValidationWarningMessage( final Locale locale,
1017            final java.lang.String detailMessage )
1018    {
1019        return ContainerFactory.getContainer().
1020            getMessage( this, "noJAXPValidationWarning", locale,
1021                new Object[]
1022                {
1023                    detailMessage
1024                });
1025
1026    }
1027
1028    /**
1029     * Gets the text of message <code>notMonitoringWarning</code>.
1030     * <blockquote><pre>{0} kann bei Änderung nicht automatisch neu geladen werden. {1}</pre></blockquote>
1031     * <blockquote><pre>{0} cannot be monitored. {1}</pre></blockquote>
1032     *
1033     * @param locale The locale of the message instance to return.
1034     * @param resourceName format parameter.
1035     * @param detailMessage format parameter.
1036     *
1037     * @return the text of message <code>notMonitoringWarning</code>.
1038     */
1039    private String getNotMonitoringWarningMessage( final Locale locale,
1040            final java.lang.String resourceName,
1041            final java.lang.String detailMessage )
1042    {
1043        return ContainerFactory.getContainer().
1044            getMessage( this, "notMonitoringWarning", locale,
1045                new Object[]
1046                {
1047                    resourceName,
1048                    detailMessage
1049                });
1050
1051    }
1052
1053    /**
1054     * Gets the text of message <code>changeInfo</code>.
1055     * <blockquote><pre>{0} aktualisiert.</pre></blockquote>
1056     * <blockquote><pre>{0} changed.</pre></blockquote>
1057     *
1058     * @param locale The locale of the message instance to return.
1059     * @param resourceName format parameter.
1060     *
1061     * @return the text of message <code>changeInfo</code>.
1062     */
1063    private String getChangeInfoMessage( final Locale locale,
1064            final java.lang.String resourceName )
1065    {
1066        return ContainerFactory.getContainer().
1067            getMessage( this, "changeInfo", locale,
1068                new Object[]
1069                {
1070                    resourceName
1071                });
1072
1073    }
1074
1075    /**
1076     * Gets the text of message <code>monitoringInfo</code>.
1077     * <blockquote><pre>{0} wird bei Änderung automatisch neu geladen.</pre></blockquote>
1078     * <blockquote><pre>Monitoring {0} for changes.</pre></blockquote>
1079     *
1080     * @param locale The locale of the message instance to return.
1081     * @param resourceName format parameter.
1082     *
1083     * @return the text of message <code>monitoringInfo</code>.
1084     */
1085    private String getMonitoringInfoMessage( final Locale locale,
1086            final java.lang.String resourceName )
1087    {
1088        return ContainerFactory.getContainer().
1089            getMessage( this, "monitoringInfo", locale,
1090                new Object[]
1091                {
1092                    resourceName
1093                });
1094
1095    }
1096
1097    /**
1098     * Gets the text of message <code>textschluesselInfo</code>.
1099     * <blockquote><pre>{1,choice,0#Kein Dokument|1#Ein Dokument|1<{1} Dokumente} gelesen. {0,choice,0#Keine|1#Einen|1<{0}} Textschlüssel verarbeitet.</pre></blockquote>
1100     * <blockquote><pre>Read {1,choice,0#no document|1#one document|1<{1} documents}. Processed {0,choice,0#no entities|1#one entity|1<{0} entities}.</pre></blockquote>
1101     *
1102     * @param locale The locale of the message instance to return.
1103     * @param entityCount format parameter.
1104     * @param documentCount format parameter.
1105     *
1106     * @return the text of message <code>textschluesselInfo</code>.
1107     */
1108    private String getTextschluesselInfoMessage( final Locale locale,
1109            final java.lang.Number entityCount,
1110            final java.lang.Number documentCount )
1111    {
1112        return ContainerFactory.getContainer().
1113            getMessage( this, "textschluesselInfo", locale,
1114                new Object[]
1115                {
1116                    entityCount,
1117                    documentCount
1118                });
1119
1120    }
1121
1122    /**
1123     * Gets the text of message <code>unsupportedNamespace</code>.
1124     * <blockquote><pre>Ungültiger XML-Namensraum {0}.</pre></blockquote>
1125     * <blockquote><pre>Unsupported XML namespace {0}.</pre></blockquote>
1126     *
1127     * @param locale The locale of the message instance to return.
1128     * @param namespace format parameter.
1129     *
1130     * @return the text of message <code>unsupportedNamespace</code>.
1131     */
1132    private String getUnsupportedNamespaceMessage( final Locale locale,
1133            final java.lang.String namespace )
1134    {
1135        return ContainerFactory.getContainer().
1136            getMessage( this, "unsupportedNamespace", locale,
1137                new Object[]
1138                {
1139                    namespace
1140                });
1141
1142    }
1143
1144    /**
1145     * Gets the text of message <code>unsupportedModelVersion</code>.
1146     * <blockquote><pre>Keine Unterstützung für Modellversion {0}.</pre></blockquote>
1147     * <blockquote><pre>Unsupported model version {0}.</pre></blockquote>
1148     *
1149     * @param locale The locale of the message instance to return.
1150     * @param modelVersion format parameter.
1151     *
1152     * @return the text of message <code>unsupportedModelVersion</code>.
1153     */
1154    private String getUnsupportedModelVersionMessage( final Locale locale,
1155            final java.lang.String modelVersion )
1156    {
1157        return ContainerFactory.getContainer().
1158            getMessage( this, "unsupportedModelVersion", locale,
1159                new Object[]
1160                {
1161                    modelVersion
1162                });
1163
1164    }
1165
1166    /**
1167     * Gets the text of message <code>parseException</code>.
1168     * <blockquote><pre>Fehler bei der Verarbeitung der Resource "{0}" in Zeile {2}, Spalte {3}. {1}</pre></blockquote>
1169     * <blockquote><pre>Error parsing resource "{0}" at line {2}, column {3}. {1}</pre></blockquote>
1170     *
1171     * @param locale The locale of the message instance to return.
1172     * @param resourceName format parameter.
1173     * @param cause format parameter.
1174     * @param line format parameter.
1175     * @param column format parameter.
1176     *
1177     * @return the text of message <code>parseException</code>.
1178     */
1179    private String getParseExceptionMessage( final Locale locale,
1180            final java.lang.String resourceName,
1181            final java.lang.String cause,
1182            final java.lang.Number line,
1183            final java.lang.Number column )
1184    {
1185        return ContainerFactory.getContainer().
1186            getMessage( this, "parseException", locale,
1187                new Object[]
1188                {
1189                    resourceName,
1190                    cause,
1191                    line,
1192                    column
1193                });
1194
1195    }
1196
1197    /**
1198     * Gets the text of message <code>noSchemaLocation</code>.
1199     * <blockquote><pre>Kein schemaLocation Attribut in Ressource "{0}". Keine Schema-Validierung.</pre></blockquote>
1200     * <blockquote><pre>No schemaLocation attribute in resource "{0}". Schema validation skipped.</pre></blockquote>
1201     *
1202     * @param locale The locale of the message instance to return.
1203     * @param resource format parameter.
1204     *
1205     * @return the text of message <code>noSchemaLocation</code>.
1206     */
1207    private String getNoSchemaLocationMessage( final Locale locale,
1208            final java.lang.String resource )
1209    {
1210        return ContainerFactory.getContainer().
1211            getMessage( this, "noSchemaLocation", locale,
1212                new Object[]
1213                {
1214                    resource
1215                });
1216
1217    }
1218
1219    /**
1220     * Gets the text of message <code>duplicateTextschluessel</code>.
1221     * <blockquote><pre>Textschlüssel {0,number,00}{1,number,000}  ist mehrfach vorhanden.</pre></blockquote>
1222     * <blockquote><pre>Non-unique Textschluessel {0,number,00}{1,number,000}.</pre></blockquote>
1223     *
1224     * @param locale The locale of the message instance to return.
1225     * @param key format parameter.
1226     * @param extension format parameter.
1227     *
1228     * @return the text of message <code>duplicateTextschluessel</code>.
1229     */
1230    private String getDuplicateTextschluesselMessage( final Locale locale,
1231            final java.lang.Number key,
1232            final java.lang.Number extension )
1233    {
1234        return ContainerFactory.getContainer().
1235            getMessage( this, "duplicateTextschluessel", locale,
1236                new Object[]
1237                {
1238                    key,
1239                    extension
1240                });
1241
1242    }
1243
1244    /**
1245     * Gets the text of message <code>noTextschluesselFound</code>.
1246     * <blockquote><pre>Keine Textschlüssel gefunden.</pre></blockquote>
1247     * <blockquote><pre>No Textschlüssel found.</pre></blockquote>
1248     *
1249     * @param locale The locale of the message instance to return.
1250     *
1251     * @return the text of message <code>noTextschluesselFound</code>.
1252     */
1253    private String getNoTextschluesselFoundMessage( final Locale locale )
1254    {
1255        return ContainerFactory.getContainer().
1256            getMessage( this, "noTextschluesselFound", locale, null );
1257
1258    }
1259
1260// </editor-fold>//GEN-END:jdtausMessages
1261
1262    //----------------------------------------------------------------Messages--
1263}