001/*
002 *  jDTAUS Banking API
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;
022
023import java.io.Serializable;
024import java.lang.ref.Reference;
025import java.lang.ref.SoftReference;
026import java.text.ParseException;
027import java.text.ParsePosition;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.Map;
031
032/**
033 * Alpha numeric text with a maximum length of twenty seven characters.
034 * <p>Data type for the alpha-numeric DTAUS alphabet. For further information
035 * see the <a href="../../../doc-files/Anlage3_Datenformate_V2.7.pdf">
036 * Spezifikation der Datenformate</a>. An updated version of the document may be found at
037 * <a href="http://www.ebics.de">EBICS</a>.</p>
038 *
039 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
040 * @version $JDTAUS: AlphaNumericText27.java 8860 2014-01-10 17:09:37Z schulte $
041 */
042public final class AlphaNumericText27 implements CharSequence, Comparable, Serializable
043{
044
045    /** Serial version UID for backwards compatibility with 1.0.x classes. */
046    private static final long serialVersionUID = -5231830564347967536L;
047
048    /** Used to cache instances. */
049    private static volatile Reference cacheReference = new SoftReference( null );
050
051    /** Constant for the maximum length allowed for an instance. */
052    public static final int MAX_LENGTH = 27;
053
054    /**
055     * The alpha-numeric text.
056     * @serial
057     */
058    private String text;
059
060    /**
061     * Flag indicating that {@code text} contains no text.
062     * @serial
063     */
064    private boolean empty;
065
066    /**
067     * Creates a new {@code AlphaNumericText27} instance holding {@code text}.
068     *
069     * @param text The text for the instance.
070     *
071     * @throws NullPointerException if {@code text} is {@code null}.
072     * @throws IllegalArgumentException if the length of {@code text} is greater than {@code MAX_LENGTH}.
073     *
074     * @see #parse(String, ParsePosition)
075     */
076    private AlphaNumericText27( final String text )
077    {
078        if ( text == null )
079        {
080            throw new NullPointerException( "text" );
081        }
082        if ( text.length() > MAX_LENGTH )
083        {
084            throw new IllegalArgumentException( text );
085        }
086
087        this.text = text;
088        this.empty = text.trim().length() == 0;
089    }
090
091    /**
092     * Parses text from a string to produce an {@code AlphaNumericText27} instance.
093     * <p>The method attempts to parse text starting at the index given by {@code pos}. If parsing succeeds, then the
094     * index of {@code pos} is updated to the index after the last character used (parsing does not necessarily use all
095     * characters up to the end of the string), and the parsed value is returned. The updated {@code pos} can be used to
096     * indicate the starting point for the next call to this method.</p>
097     *
098     * @param text A string to parse alpha numeric characters from.
099     * @param pos A {@code ParsePosition} object with index and error index information as described above.
100     *
101     * @return The parsed value, or {@code null} if the parse fails.
102     *
103     * @throws NullPointerException if either {@code text} or {@code pos} is {@code null}.
104     */
105    public static AlphaNumericText27 parse( final String text, final ParsePosition pos )
106    {
107        if ( text == null )
108        {
109            throw new NullPointerException( "text" );
110        }
111        if ( pos == null )
112        {
113            throw new NullPointerException( "pos" );
114        }
115
116        int i;
117        boolean valid = true;
118        AlphaNumericText27 ret = null;
119        final int beginIndex = pos.getIndex();
120        final int len = text.length();
121
122        for ( i = beginIndex; i < beginIndex + MAX_LENGTH && i < len; i++ )
123        {
124            if ( !AlphaNumericText27.checkAlphaNumeric( text.charAt( i ) ) )
125            {
126                pos.setErrorIndex( i );
127                valid = false;
128                break;
129            }
130        }
131
132        if ( valid )
133        {
134            pos.setIndex( i );
135            ret = new AlphaNumericText27( text.substring( beginIndex, i ) );
136        }
137
138        return ret;
139    }
140
141    /**
142     * Parses text from the beginning of the given string to produce an {@code AlphaNumericText27} instance.
143     * <p>Unlike the {@link #parse(String, ParsePosition)} method this method throws a {@code ParseException} if
144     * {@code text} cannot be parsed or is of invalid length.</p>
145     *
146     * @param text A string to parse alpha numeric characters from.
147     *
148     * @return The parsed value.
149     *
150     * @throws NullPointerException if {@code text} is {@code null}.
151     * @throws ParseException if the parse fails or the length of {@code text} is greater than {@code 27}.
152     */
153    public static AlphaNumericText27 parse( final String text ) throws ParseException
154    {
155        if ( text == null )
156        {
157            throw new NullPointerException( "text" );
158        }
159
160        AlphaNumericText27 txt = (AlphaNumericText27) getCache().get( text );
161
162        if ( txt == null )
163        {
164            final ParsePosition pos = new ParsePosition( 0 );
165            txt = AlphaNumericText27.parse( text, pos );
166
167            if ( txt == null || pos.getErrorIndex() != -1 || pos.getIndex() < text.length() )
168            {
169                throw new ParseException( text, pos.getErrorIndex() != -1 ? pos.getErrorIndex() : pos.getIndex() );
170            }
171            else
172            {
173                getCache().put( text, txt );
174            }
175        }
176
177        return txt;
178    }
179
180    /**
181     * Parses text from the beginning of the given string to produce an {@code AlphaNumericText27} instance.
182     * <p>Unlike the {@link #parse(String)} method this method throws an {@code IllegalArgumentException} if
183     * {@code text} cannot be parsed or is of invalid length.</p>
184     *
185     * @param text A formatted string representation of an {@code AlphaNumericText27} instance.
186     *
187     * @return The parsed value.
188     *
189     * @throws NullPointerException if {@code text} is {@code null}.
190     * @throws IllegalArgumentException if the parse fails or the length of {@code text} is greater than {@code 27}.
191     */
192    public static AlphaNumericText27 valueOf( final String text )
193    {
194        try
195        {
196            return AlphaNumericText27.parse( text );
197        }
198        catch ( final ParseException e )
199        {
200            throw (IllegalArgumentException) new IllegalArgumentException( text ).initCause( e );
201        }
202    }
203
204    /**
205     * Formats alpha-numeric characters and appends the resulting text to the given string buffer.
206     *
207     * @param toAppendTo The buffer to which the formatted text is to be appended.
208     *
209     * @return The value passed in as {@code toAppendTo}.
210     *
211     * @throws NullPointerException if {@code toAppendTo} is {@code null}.
212     */
213    public StringBuffer format( final StringBuffer toAppendTo )
214    {
215        if ( toAppendTo == null )
216        {
217            throw new NullPointerException( "toAppendTo" );
218        }
219
220        return toAppendTo.append( this.text );
221    }
222
223    /**
224     * Formats alpha-numeric characters to produce a string. Same as
225     * <blockquote>
226     * {@link #format(StringBuffer) format<code>(new StringBuffer()).toString()</code>}
227     * </blockquote>
228     *
229     * @return The formatted string.
230     */
231    public String format()
232    {
233        return this.text;
234    }
235
236    /**
237     * Formats alpha-numeric characters to produce a string. Same as
238     * <blockquote>
239     * {@link #format() alphaNumericText27.format()}
240     * </blockquote>
241     *
242     * @param alphaNumericText27 The {@code AlphaNumericText27} instance to format.
243     *
244     * @return The formatted string.
245     *
246     * @throws NullPointerException if {@code alphaNumericText27} is {@code null}.
247     */
248    public static String toString( final AlphaNumericText27 alphaNumericText27 )
249    {
250        if ( alphaNumericText27 == null )
251        {
252            throw new NullPointerException( "alphaNumericText27" );
253        }
254
255        return alphaNumericText27.format();
256    }
257
258    /**
259     * Checks a given character to belong to the alpha-numeric alphabet.
260     *
261     * @param c The character to check.
262     *
263     * @return {@code true} if {@code c} is a character of the alpha-numeric DTAUS alphabet; {@code false} if not.
264     */
265    public static boolean checkAlphaNumeric( final char c )
266    {
267        return ( c >= 'A' && c <= 'Z' ) || ( c >= '0' && c <= '9' ) ||
268               ( c == '.' || c == '+' || c == '*' || c == '$' || c == ' ' || c == ',' || c == '&' || c == '-' ||
269                 c == '/' || c == '%' || c == 'Ä' || c == 'Ö' || c == 'Ü' || c == 'ß' );
270
271    }
272
273    /**
274     * Normalizes text to conform to the alpha-numeric alphabet.
275     * <p>This method converts lower case letters to upper case letters and replaces all illegal characters with spaces.
276     * It will return the unchanged text if for every given character {@code C} the method {@code checkAlphaNumeric(C)}
277     * returns {@code true}.</p>
278     * <p>Note that code like
279     * <blockquote><code>
280     * AlphaNumericText27.parse(AlphaNumericText27.normalize(getSomething()));
281     * </code></blockquote>
282     * may be dangerous if {@code getSomething()} may provide unchecked or otherwise invalid data which will get
283     * converted to valid data by this method.</p>
284     * <p>Also note that
285     * <blockquote><code>
286     * AlphaNumericText27.normalize(<i>something</i>).length() == <i>something</i>.length();
287     * </code></blockquote>
288     * is always {@code true} although
289     * <blockquote><code>
290     * AlphaNumericText27.normalize(<i>something</i>).equals(<i>something</i>);
291     * </code></blockquote> may be {@code false}.</p>
292     * <p>It is recommended to always check for changes before proceeding with the data this method returns.
293     * For example:
294     * <blockquote><pre>
295     * something = getSomething();
296     * normalized = AlphaNumericText27.normalize(something);
297     * if(!something.equals(normalized)) {
298     *
299     *     <i>e.g. check the normalized value, log a warning, display the
300     *     normalized value to the user for confirmation</i>
301     *
302     * }</pre></blockquote></p>
303     *
304     * @param text The text to normalize.
305     *
306     * @return {@code text} normalized to conform to the alpha-numeric alphabet.
307     *
308     * @throws NullPointerException if {@code text} is {@code null}.
309     */
310    public static String normalize( final String text )
311    {
312        if ( text == null )
313        {
314            throw new NullPointerException( "text" );
315        }
316
317        final char[] ret = text.toCharArray();
318
319        for ( int i = ret.length - 1; i >= 0; i-- )
320        {
321            if ( Character.isLowerCase( ret[i] ) )
322            {
323                ret[i] = Character.toUpperCase( ret[i] );
324            }
325
326            if ( !AlphaNumericText27.checkAlphaNumeric( ret[i] ) )
327            {
328                ret[i] = ' ';
329            }
330        }
331
332        return new String( ret );
333    }
334
335    /**
336     * Flag indicating that the instance contains no text.
337     *
338     * @return {@code true} if the instance contains no text but just whitespace characters; {@code false} if the
339     * instance contains text.
340     */
341    public boolean isEmpty()
342    {
343        return this.empty;
344    }
345
346    /**
347     * Gets the current cache instance.
348     *
349     * @return Current cache instance.
350     */
351    private static Map getCache()
352    {
353        Map cache = (Map) cacheReference.get();
354        if ( cache == null )
355        {
356            cache = Collections.synchronizedMap( new HashMap( 1024 ) );
357            cacheReference = new SoftReference( cache );
358        }
359
360        return cache;
361    }
362
363    /**
364     * Returns the length of this character sequence.  The length is the number of 16-bit {@code char}s in the sequence.
365     *
366     * @return The number of {@code char}s in this sequence.
367     */
368    public int length()
369    {
370        return this.text.length();
371    }
372
373    /**
374     * Returns the {@code char} value at the specified index.
375     * <p>An index ranges from zero to {@code length() - 1}. The first {@code char} value of the sequence is at index
376     * zero, the next at index one, and so on, as for array indexing.</p>
377     *
378     * @param index The index of the {@code char} value to be returned.
379     *
380     * @return The specified {@code char} value.
381     *
382     * @throws IndexOutOfBoundsException if {@code index} is negative or not less than {@code length()}.
383     */
384    public char charAt( final int index )
385    {
386        return this.text.charAt( index );
387    }
388
389    /**
390     * Returns a new {@code CharSequence} that is a subsequence of this sequence.
391     * <p>The subsequence starts with the {@code char} value at the specified index and ends with the {@code char} value
392     * at index {@code end - 1}. The length (in {@code char}s) of the returned sequence is {@code end - start}, so if
393     * {@code start == end} then an empty sequence is returned.</p>
394     *
395     * @param start The start index, inclusive.
396     * @param end The end index, exclusive.
397     *
398     * @return The specified subsequence.
399     *
400     * @throws  IndexOutOfBoundsException if {@code start} or {@code end} are negative, if {@code end} is greater than
401     * {@code length()}, or if {@code start} is greater than {@code end}.
402     */
403    public CharSequence subSequence( final int start, final int end )
404    {
405        return this.text.subSequence( start, end );
406    }
407
408    /**
409     * Returns a string containing the characters in this sequence in the same order as this sequence. The length of the
410     * string will be the length of this sequence.
411     *
412     * @return A string consisting of exactly this sequence of characters.
413     */
414    public String toString()
415    {
416        return this.text;
417    }
418
419    /**
420     * Compares this object with the specified object for order.  Returns a negative integer, zero, or a positive
421     * integer as this object is less than, equal to, or greater than the specified object.
422     *
423     * @param o The Object to be compared.
424     * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or greater than
425     * the specified object.
426     *
427     * @throws NullPointerException if {@code o} is {@code null}.
428     * @throws ClassCastException if the specified object's type prevents it from being compared to this Object.
429     */
430    public int compareTo( final Object o )
431    {
432        if ( o == null )
433        {
434            throw new NullPointerException( "o" );
435        }
436        if ( !( o instanceof AlphaNumericText27 ) )
437        {
438            throw new ClassCastException( o.getClass().getName() );
439        }
440
441        final AlphaNumericText27 that = (AlphaNumericText27) o;
442
443        int result = 0;
444
445        if ( !this.equals( o ) )
446        {
447            if ( this.text == null )
448            {
449                result = that.text == null ? 0 : -1;
450            }
451            else
452            {
453                result = that.text == null ? 1 : this.text.compareTo( that.text );
454            }
455        }
456
457        return result;
458    }
459
460    /**
461     * Indicates whether some other object is equal to this one.
462     *
463     * @param o The reference object with which to compare.
464     *
465     * @return {@code true} if this object is the same as {@code o}; {@code false} otherwise.
466     */
467    public boolean equals( final Object o )
468    {
469        boolean ret = o == this;
470
471        if ( !ret && o instanceof AlphaNumericText27 )
472        {
473            final AlphaNumericText27 that = (AlphaNumericText27) o;
474            ret = this.text == null ? that.text == null : this.text.equals( that.text );
475        }
476
477        return ret;
478    }
479
480    /**
481     * Returns a hash code value for this object.
482     *
483     * @return A hash code value for this object.
484     */
485    public int hashCode()
486    {
487        return this.text == null ? 0 : this.text.hashCode();
488    }
489
490}