JavaTypeName.java

/*
 *   Copyright (C) Christian Schulte <cs@schulte.it>, 2012-235
 *   All rights reserved.
 *
 *   Redistribution and use in source and binary forms, with or without
 *   modification, are permitted provided that the following conditions
 *   are met:
 *
 *     o Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *
 *     o Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in
 *       the documentation and/or other materials provided with the
 *       distribution.
 *
 *   THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 *   INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 *   AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 *   THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
 *   INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 *   NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 *   DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 *   THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 *   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 *   THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 *   $JOMC: JavaTypeName.java 5043 2015-05-27 07:03:39Z schulte $
 *
 */
package org.jomc.model;

import java.io.Serializable;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.text.MessageFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;

/**
 * Data type of a Java type name.
 * <p>
 * This class supports parsing of Java type names as specified in the
 * Java Language Specification - Java SE 7 Edition - Chapters 3.8ff, 6.5 and 18.
 * </p>
 * <p>
 * <i>Please note that this class will move to package {@code org.jomc.util} in JOMC 2.0.</i>
 * </p>
 *
 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
 * @version $JOMC: JavaTypeName.java 5043 2015-05-27 07:03:39Z schulte $
 * @see #parse(java.lang.String)
 * @see #valueOf(java.lang.String)
 * @since 1.4
 */
public final class JavaTypeName implements Serializable
{

    /**
     * Data type of an argument of a parameterized Java type name.
     *
     * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
     * @version $JOMC: JavaTypeName.java 5043 2015-05-27 07:03:39Z schulte $
     * @since 1.4
     */
    public static final class Argument implements Serializable
    {

        /**
         * Flag indicating the argument is a wildcard.
         *
         * @serial
         */
        private boolean wildcard;

        /**
         * The wildcard bounds of the argument.
         *
         * @serial
         */
        private String wildcardBounds;

        /**
         * The type name of the argument.
         *
         * @serial
         */
        private JavaTypeName typeName;

        /**
         * Cached string representation.
         */
        private transient String cachedString;

        /**
         * Serial version UID for backwards compatibility with 1.4.x object streams.
         */
        private static final long serialVersionUID = -6515267147665760819L;

        /**
         * Create a new {@code Argument} instance.
         */
        private Argument()
        {
            super();
        }

        /**
         * Gets a flag indicating the argument is a wildcard argument.
         *
         * @return {@code true}, if the argument is a wildcard argument; {@code false}, else.
         */
        public boolean isWildcard()
        {
            return this.wildcard;
        }

        /**
         * Gets the wildcard bounds of the argument.
         *
         * @return The wildcard bounds of the argument or {@code null}.
         */
        public String getWildcardBounds()
        {
            return this.wildcardBounds;
        }

        /**
         * Gets the type name of the argument.
         *
         * @return The type name of the argument or {@code null}, if the argument is a wildcard argument.
         */
        public JavaTypeName getTypeName()
        {
            return this.typeName;
        }

        /**
         * Creates a string representation of the instance.
         *
         * @return A string representation of the instance.
         */
        @Override
        public String toString()
        {
            if ( this.cachedString == null )
            {
                final StringBuilder builder = new StringBuilder( 128 );

                if ( this.isWildcard() )
                {
                    builder.append( "?" );

                    if ( this.getWildcardBounds() != null && this.getTypeName() != null )
                    {
                        builder.append( " " ).append( this.getWildcardBounds() ).append( " " ).
                            append( this.getTypeName() );

                    }
                }
                else
                {
                    builder.append( this.getTypeName() );
                }

                this.cachedString = builder.toString();
            }

            return this.cachedString;
        }

    }

    /**
     * Java type name of class {@code Boolean}.
     *
     * @see Boolean
     */
    public static final JavaTypeName BOOLEAN;

    /**
     * Java type name of basic type {@code boolean}.
     *
     * @see Boolean#TYPE
     */
    public static final JavaTypeName BOOLEAN_TYPE;

    /**
     * Java type name of class {@code Byte}.
     *
     * @see Byte
     */
    public static final JavaTypeName BYTE;

    /**
     * Java type name of basic type {@code byte}.
     *
     * @see Byte#TYPE
     */
    public static final JavaTypeName BYTE_TYPE;

    /**
     * Java type name of class {@code Character}.
     *
     * @see Character
     */
    public static final JavaTypeName CHARACTER;

    /**
     * Java type name of basic type {@code char}.
     *
     * @see Character#TYPE
     */
    public static final JavaTypeName CHARACTER_TYPE;

    /**
     * Java type name of class {@code Double}.
     *
     * @see Double
     */
    public static final JavaTypeName DOUBLE;

    /**
     * Java type name of basic type {@code double}.
     *
     * @see Double#TYPE
     */
    public static final JavaTypeName DOUBLE_TYPE;

    /**
     * Java type name of class {@code Float}.
     *
     * @see Float
     */
    public static final JavaTypeName FLOAT;

    /**
     * Java type name of basic type {@code float}.
     *
     * @see Float#TYPE
     */
    public static final JavaTypeName FLOAT_TYPE;

    /**
     * Java type name of class {@code Integer}.
     *
     * @see Integer
     */
    public static final JavaTypeName INTEGER;

    /**
     * Java type name of basic type {@code int}.
     *
     * @see Integer#TYPE
     */
    public static final JavaTypeName INTEGER_TYPE;

    /**
     * Java type name of class {@code Long}.
     *
     * @see Long
     */
    public static final JavaTypeName LONG;

    /**
     * Java type name of basic type {@code long}.
     *
     * @see Long#TYPE
     */
    public static final JavaTypeName LONG_TYPE;

    /**
     * Java type name of class {@code Short}.
     *
     * @see Short
     */
    public static final JavaTypeName SHORT;

    /**
     * Java type name of basic type {@code short}.
     *
     * @see Short#TYPE
     */
    public static final JavaTypeName SHORT_TYPE;

    /**
     * The array dimension of the type name.
     *
     * @serial
     */
    private int dimension;

    /**
     * The flag indicating the type name denotes a primitive type.
     *
     * @serial
     */
    private boolean primitive;

    /**
     * The class name of the type name.
     *
     * @serial
     */
    private String className;

    /**
     * The qualified package name of the type name.
     *
     * @serial
     */
    private String packageName;

    /**
     * The qualified name of the type name.
     *
     * @serial
     */
    private String qualifiedName;

    /**
     * The simple name of the type name.
     *
     * @serial
     */
    private String simpleName;

    /**
     * The arguments of the type name.
     *
     * @serial
     */
    private volatile List<Argument> arguments;

    /**
     * Cached string representation.
     */
    private transient String cachedString;

    /**
     * Cached instances.
     */
    private static volatile Reference<Map<String, JavaTypeName>> cache;

    /**
     * Mappings of basic type name to class name encoding.
     */
    private static final Map<String, String> CLASSNAME_ENCODINGS = new HashMap<String, String>( 8 );

    /**
     * Serial version UID for backwards compatibility with 1.4.x object streams.
     */
    private static final long serialVersionUID = -4258949347035910249L;

    static
    {
        CLASSNAME_ENCODINGS.put( "boolean", "Z" );
        CLASSNAME_ENCODINGS.put( "byte", "B" );
        CLASSNAME_ENCODINGS.put( "char", "C" );
        CLASSNAME_ENCODINGS.put( "double", "D" );
        CLASSNAME_ENCODINGS.put( "float", "F" );
        CLASSNAME_ENCODINGS.put( "int", "I" );
        CLASSNAME_ENCODINGS.put( "long", "J" );
        CLASSNAME_ENCODINGS.put( "short", "S" );

        BOOLEAN = JavaTypeName.valueOf( Boolean.class.getName() );
        BOOLEAN_TYPE = JavaTypeName.valueOf( Boolean.TYPE.getName() );
        BYTE = JavaTypeName.valueOf( Byte.class.getName() );
        BYTE_TYPE = JavaTypeName.valueOf( Byte.TYPE.getName() );
        CHARACTER = JavaTypeName.valueOf( Character.class.getName() );
        CHARACTER_TYPE = JavaTypeName.valueOf( Character.TYPE.getName() );
        DOUBLE = JavaTypeName.valueOf( Double.class.getName() );
        DOUBLE_TYPE = JavaTypeName.valueOf( Double.TYPE.getName() );
        FLOAT = JavaTypeName.valueOf( Float.class.getName() );
        FLOAT_TYPE = JavaTypeName.valueOf( Float.TYPE.getName() );
        INTEGER = JavaTypeName.valueOf( Integer.class.getName() );
        INTEGER_TYPE = JavaTypeName.valueOf( Integer.TYPE.getName() );
        LONG = JavaTypeName.valueOf( Long.class.getName() );
        LONG_TYPE = JavaTypeName.valueOf( Long.TYPE.getName() );
        SHORT = JavaTypeName.valueOf( Short.class.getName() );
        SHORT_TYPE = JavaTypeName.valueOf( Short.TYPE.getName() );
    }

    /**
     * Creates a new {@code JavaTypeName} instance.
     */
    private JavaTypeName()
    {
        super();
    }

    /**
     * Gets the {@code Class} object of the type using a given class loader.
     *
     * @param classLoader The class loader to use for loading the {@code Class} object to return or {@code null}, to
     * load that {@code Class} object using the platform's bootstrap class loader.
     * @param initialize Flag indicating initialization to be performed on the loaded {@code Class} object.
     *
     * @return The {@code Class} object of the type.
     *
     * @throws ClassNotFoundException if the {@code Class} object of the type is not found searching
     * {@code classLoader}.
     *
     * @see Class#forName(java.lang.String, boolean, java.lang.ClassLoader)
     */
    public Class<?> getClass( final ClassLoader classLoader, final boolean initialize ) throws ClassNotFoundException
    {
        Class<?> javaClass = null;

        if ( this.isArray() )
        {
            javaClass = Class.forName( this.getClassName(), initialize, classLoader );
        }
        else if ( this.isPrimitive() )
        {
            if ( BOOLEAN_TYPE.equals( this ) )
            {
                javaClass = Boolean.TYPE;
            }
            else if ( BYTE_TYPE.equals( this ) )
            {
                javaClass = Byte.TYPE;
            }
            else if ( CHARACTER_TYPE.equals( this ) )
            {
                javaClass = Character.TYPE;
            }
            else if ( DOUBLE_TYPE.equals( this ) )
            {
                javaClass = Double.TYPE;
            }
            else if ( FLOAT_TYPE.equals( this ) )
            {
                javaClass = Float.TYPE;
            }
            else if ( INTEGER_TYPE.equals( this ) )
            {
                javaClass = Integer.TYPE;
            }
            else if ( LONG_TYPE.equals( this ) )
            {
                javaClass = Long.TYPE;
            }
            else if ( SHORT_TYPE.equals( this ) )
            {
                javaClass = Short.TYPE;
            }
            else
            {
                throw new AssertionError( this );
            }
        }
        else
        {
            javaClass = Class.forName( this.getClassName(), initialize, classLoader );
        }

        return javaClass;
    }

    /**
     * Gets the arguments of the type name.
     *
     * @return An unmodifiable list holding the arguments of the type name.
     */
    public List<Argument> getArguments()
    {
        if ( this.arguments == null )
        {
            this.arguments = new ArrayList<Argument>();
        }

        return this.arguments;
    }

    /**
     * Gets a flag indicating the type name denotes an array type.
     *
     * @return {@code true}, if the type name denotes an array type; {@code false}, else.
     *
     * @see Class#isArray()
     */
    public boolean isArray()
    {
        return this.dimension > 0;
    }

    /**
     * Gets a flag indicating the type name denotes a primitive type.
     *
     * @return {@code true}, if the type name denotes a primitive type; {@code false}, else.
     *
     * @see Class#isPrimitive()
     */
    public boolean isPrimitive()
    {
        return this.primitive;
    }

    /**
     * Gets a flag indicating the type name denotes a wrapper type of a primitive type.
     *
     * @return {@code true}, if the type name denotes a wrapper type of a primitive type; {@code false}, else.
     */
    public boolean isUnboxable()
    {
        // The Java Language Specification - Java SE 7 Edition - 5.1.8. Unboxing Conversion
        return BOOLEAN.equals( this )
                   || BYTE.equals( this )
                   || SHORT.equals( this )
                   || CHARACTER.equals( this )
                   || INTEGER.equals( this )
                   || LONG.equals( this )
                   || FLOAT.equals( this )
                   || DOUBLE.equals( this );

    }

    /**
     * Gets the type name.
     *
     * @param qualified {@code true}, to return a qualified name; {@code false}, to return a simple name.
     *
     * @return The type name.
     */
    public String getName( final boolean qualified )
    {
        return qualified
                   ? this.toString()
                   : this.getPackageName().length() > 0
                         ? this.toString().substring( this.getPackageName().length() + 1 )
                         : this.toString();

    }

    /**
     * Gets the class name of the type name.
     *
     * @return The class name of the type name.
     *
     * @see Class#getName()
     * @see Class#forName(java.lang.String)
     */
    public String getClassName()
    {
        return this.className;
    }

    /**
     * Gets the fully qualified package name of the type name.
     *
     * @return The fully qualified package name of the type name or an empty string, if the type name denotes a type
     * located in an unnamed package.
     *
     * @see #isUnnamedPackage()
     */
    public String getPackageName()
    {
        return this.packageName;
    }

    /**
     * Gets a flag indicating the type name denotes a type located in an unnamed package.
     *
     * @return {@code true}, if the type name denotes a type located in an unnamed package; {@code false}, else.
     *
     * @see #getPackageName()
     */
    public boolean isUnnamedPackage()
    {
        return this.getPackageName().length() == 0;
    }

    /**
     * Gets the fully qualified name of the type name.
     *
     * @return The fully qualified name of the type name.
     */
    public String getQualifiedName()
    {
        return this.qualifiedName;
    }

    /**
     * Gets the simple name of the type name.
     *
     * @return The simple name of the type name.
     */
    public String getSimpleName()
    {
        return this.simpleName;
    }

    /**
     * Gets the type name applying a boxing conversion.
     *
     * @return The converted type name or {@code null}, if the instance cannot be converted.
     *
     * @see #isArray()
     * @see #isPrimitive()
     */
    public JavaTypeName getBoxedName()
    {
        JavaTypeName boxedName = null;

        // The Java Language Specification - Java SE 7 Edition - 5.1.7. Boxing Conversion
        if ( BOOLEAN_TYPE.equals( this ) )
        {
            boxedName = BOOLEAN;
        }
        else if ( BYTE_TYPE.equals( this ) )
        {
            boxedName = BYTE;
        }
        else if ( SHORT_TYPE.equals( this ) )
        {
            boxedName = SHORT;
        }
        else if ( CHARACTER_TYPE.equals( this ) )
        {
            boxedName = CHARACTER;
        }
        else if ( INTEGER_TYPE.equals( this ) )
        {
            boxedName = INTEGER;
        }
        else if ( LONG_TYPE.equals( this ) )
        {
            boxedName = LONG;
        }
        else if ( FLOAT_TYPE.equals( this ) )
        {
            boxedName = FLOAT;
        }
        else if ( DOUBLE_TYPE.equals( this ) )
        {
            boxedName = DOUBLE;
        }

        return boxedName;
    }

    /**
     * Gets the type name applying an unboxing conversion.
     *
     * @return The converted type name or {@code null}, if the instance cannot be converted.
     *
     * @see #isUnboxable()
     */
    public JavaTypeName getUnboxedName()
    {
        JavaTypeName unboxedName = null;

        // The Java Language Specification - Java SE 7 Edition - 5.1.8. Unboxing Conversion
        if ( BOOLEAN.equals( this ) )
        {
            unboxedName = BOOLEAN_TYPE;
        }
        else if ( BYTE.equals( this ) )
        {
            unboxedName = BYTE_TYPE;
        }
        else if ( SHORT.equals( this ) )
        {
            unboxedName = SHORT_TYPE;
        }
        else if ( CHARACTER.equals( this ) )
        {
            unboxedName = CHARACTER_TYPE;
        }
        else if ( INTEGER.equals( this ) )
        {
            unboxedName = INTEGER_TYPE;
        }
        else if ( LONG.equals( this ) )
        {
            unboxedName = LONG_TYPE;
        }
        else if ( FLOAT.equals( this ) )
        {
            unboxedName = FLOAT_TYPE;
        }
        else if ( DOUBLE.equals( this ) )
        {
            unboxedName = DOUBLE_TYPE;
        }

        return unboxedName;
    }

    /**
     * Creates a string representation of the instance.
     *
     * @return A string representation of the instance.
     */
    @Override
    public String toString()
    {
        if ( this.cachedString == null )
        {
            final StringBuilder builder = new StringBuilder( this.getQualifiedName() );

            if ( !this.getArguments().isEmpty() )
            {
                builder.append( "<" );

                for ( int i = 0, s0 = this.getArguments().size(); i < s0; i++ )
                {
                    builder.append( this.getArguments().get( i ) ).append( ", " );
                }

                builder.setLength( builder.length() - 2 );
                builder.append( ">" );
            }

            if ( this.isArray() )
            {
                final int idx = this.getQualifiedName().length() - this.dimension * "[]".length();
                builder.append( builder.substring( idx, this.getQualifiedName().length() ) );
                builder.delete( idx, this.getQualifiedName().length() );
            }

            this.cachedString = builder.toString();
        }

        return this.cachedString;
    }

    /**
     * Gets the hash code value of the object.
     *
     * @return The hash code value of the object.
     */
    @Override
    public int hashCode()
    {
        return this.toString().hashCode();
    }

    /**
     * Tests whether another object is compile-time equal to this object.
     *
     * @param o The object to compare.
     *
     * @return {@code true}, if {@code o} denotes the same compile-time type name than the object; {@code false}, else.
     */
    @Override
    public boolean equals( final Object o )
    {
        boolean equal = o == this;

        if ( !equal && o instanceof JavaTypeName )
        {
            equal = this.toString().equals( o.toString() );
        }

        return equal;
    }

    /**
     * Tests whether another object is runtime equal to this object.
     *
     * @param o The object to compare.
     *
     * @return {@code true}, if {@code o} denotes the same runtime type name than the object; {@code false}, else.
     */
    public boolean runtimeEquals( final Object o )
    {
        boolean equal = o == this;

        if ( !equal && o instanceof JavaTypeName )
        {
            final JavaTypeName that = (JavaTypeName) o;
            equal = this.getClassName().equals( that.getClassName() );
        }

        return equal;
    }

    /**
     * Parses text from the beginning of the given string to produce a {@code JavaTypeName} instance.
     *
     * @param text The text to parse.
     *
     * @return A {@code JavaTypeName} instance corresponding to {@code text}.
     *
     * @throws NullPointerException if {@code text} is {@code null}.
     * @throws ParseException if parsing fails.
     *
     * @see #valueOf(java.lang.String)
     */
    public static JavaTypeName parse( final String text ) throws ParseException
    {
        if ( text == null )
        {
            throw new NullPointerException( "text" );
        }

        return parse( text, false );
    }

    /**
     * Parses text from the beginning of the given string to produce a {@code JavaTypeName} instance.
     * <p>
     * Unlike the {@link #parse(String)} method, this method throws an {@code IllegalArgumentException} if parsing
     * fails.
     * </p>
     *
     * @param text The text to parse.
     *
     * @return A {@code JavaTypeName} instance corresponding to {@code text}.
     *
     * @throws NullPointerException if {@code text} is {@code null}.
     * @throws IllegalArgumentException if parsing fails.
     *
     * @see #parse(java.lang.String)
     */
    public static JavaTypeName valueOf( final String text ) throws IllegalArgumentException
    {
        if ( text == null )
        {
            throw new NullPointerException( "text" );
        }

        try
        {
            return parse( text, true );
        }
        catch ( final ParseException e )
        {
            throw new AssertionError( e );
        }
    }

    private static JavaTypeName parse( final String text, boolean runtimeException ) throws ParseException
    {
        Map<String, JavaTypeName> map = cache == null ? null : cache.get();

        if ( map == null )
        {
            map = new HashMap<String, JavaTypeName>( 128 );
            cache = new SoftReference<Map<String, JavaTypeName>>( map );
        }

        synchronized ( map )
        {
            JavaTypeName javaType = map.get( text );

            if ( javaType == null )
            {
                javaType = new JavaTypeName();
                parseType( javaType, text, runtimeException );

                javaType.arguments = javaType.arguments != null
                                         ? Collections.unmodifiableList( javaType.arguments )
                                         : Collections.<Argument>emptyList();

                final String name = javaType.getName( true );
                final JavaTypeName existingInstance = map.get( name );

                if ( existingInstance != null )
                {
                    map.put( text, existingInstance );
                    javaType = existingInstance;
                }
                else
                {
                    map.put( text, javaType );
                    map.put( name, javaType );
                }
            }

            return javaType;
        }
    }

    /**
     * JLS - Java SE 7 Edition - Chapter 18. Syntax
     * <pre>
     * Type:
     *     BasicType {[]}
     *     ReferenceType  {[]}
     * </pre>
     *
     * @see #parseReferenceType(org.jomc.model.JavaTypeName.Tokenizer, org.jomc.model.JavaTypeName, boolean, boolean)
     */
    private static void parseType( final JavaTypeName t, final String text, final boolean runtimeException )
        throws ParseException
    {
        final Tokenizer tokenizer = new Tokenizer( text, runtimeException );
        boolean basic_type_or_reference_type_seen = false;
        boolean lpar_seen = false;
        Token token;

        while ( ( token = tokenizer.next() ) != null )
        {
            switch ( token.getKind() )
            {
                case Tokenizer.TK_BASIC_TYPE:
                    if ( basic_type_or_reference_type_seen || !CLASSNAME_ENCODINGS.containsKey( token.getValue() ) )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    basic_type_or_reference_type_seen = true;
                    t.className = token.getValue();
                    t.qualifiedName = token.getValue();
                    t.simpleName = token.getValue();
                    t.packageName = "";
                    t.primitive = true;
                    break;

                case Tokenizer.TK_IDENTIFIER:
                    if ( basic_type_or_reference_type_seen )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    basic_type_or_reference_type_seen = true;
                    tokenizer.back();
                    parseReferenceType( tokenizer, t, false, runtimeException );
                    break;

                case Tokenizer.TK_LPAR:
                    if ( !basic_type_or_reference_type_seen || lpar_seen )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    lpar_seen = true;
                    break;

                case Tokenizer.TK_RPAR:
                    if ( !( basic_type_or_reference_type_seen && lpar_seen ) )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    lpar_seen = false;
                    t.dimension++;
                    t.className = "[" + t.className;
                    t.qualifiedName += "[]";
                    t.simpleName += "[]";
                    break;

                default:
                    if ( runtimeException )
                    {
                        throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                    }
                    else
                    {
                        throw createInvalidTokenParseException( tokenizer.input(), token );
                    }

            }
        }

        if ( !basic_type_or_reference_type_seen || lpar_seen )
        {
            if ( runtimeException )
            {
                throw createUnexpectedEndOfInputIllegalArgumentException( tokenizer.input(), tokenizer.length() );
            }
            else
            {
                throw createUnexpectedEndOfInputParseException( tokenizer.input(), tokenizer.length() );
            }
        }

        if ( t.dimension > 0 )
        {
            if ( t.primitive )
            {
                t.className = new StringBuilder( t.className.length() ).
                    append( t.className.substring( 0, t.dimension ) ).
                    append( CLASSNAME_ENCODINGS.get( t.className.substring( t.dimension ) ) ).toString();

            }
            else
            {
                t.className = new StringBuilder( t.className.length() ).
                    append( t.className.substring( 0, t.dimension ) ).
                    append( "L" ).append( t.className.substring( t.dimension ) ).append( ";" ).toString();

            }
        }

        t.arguments = Collections.unmodifiableList( t.getArguments() );
    }

    /**
     * JLS - Java SE 7 Edition - Chapter 18. Syntax
     * <pre>
     * ReferenceType:
     *      Identifier [TypeArguments] { . Identifier [TypeArguments] }
     * </pre>
     *
     * @see #parseTypeArguments(org.jomc.model.JavaTypeName.Tokenizer, org.jomc.model.JavaTypeName, boolean)
     */
    private static void parseReferenceType( final Tokenizer tokenizer, final JavaTypeName t,
                                            final boolean in_type_arguments, final boolean runtimeException )
        throws ParseException
    {
        final StringBuilder classNameBuilder = new StringBuilder( tokenizer.input().length() );
        final StringBuilder typeNameBuilder = new StringBuilder( tokenizer.input().length() );
        boolean identifier_seen = false;
        boolean type_arguments_seen = false;
        Token token;

        while ( ( token = tokenizer.next() ) != null )
        {
            switch ( token.getKind() )
            {
                case Tokenizer.TK_IDENTIFIER:
                    if ( identifier_seen || type_arguments_seen )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    identifier_seen = true;
                    type_arguments_seen = false;
                    t.simpleName = token.getValue();
                    t.packageName = typeNameBuilder.length() > 0
                                        ? typeNameBuilder.substring( 0, typeNameBuilder.length() - 1 )
                                        : "";

                    classNameBuilder.append( token.getValue() );
                    typeNameBuilder.append( token.getValue() );
                    break;

                case Tokenizer.TK_DOT:
                    if ( !( identifier_seen || type_arguments_seen ) )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    identifier_seen = false;
                    type_arguments_seen = false;
                    classNameBuilder.append( token.getValue() );
                    typeNameBuilder.append( token.getValue() );
                    break;

                case Tokenizer.TK_LT:
                    if ( !identifier_seen )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    identifier_seen = false;
                    type_arguments_seen = true;
                    tokenizer.back();
                    parseTypeArguments( tokenizer, t, runtimeException );
                    break;

                case Tokenizer.TK_LPAR:
                    if ( !( identifier_seen || type_arguments_seen ) || in_type_arguments )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    tokenizer.back();
                    t.className = classNameBuilder.toString();
                    t.qualifiedName = typeNameBuilder.toString();
                    return;

                case Tokenizer.TK_COMMA:
                case Tokenizer.TK_GT:
                    if ( !( identifier_seen || type_arguments_seen ) || !in_type_arguments )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    tokenizer.back();
                    t.className = classNameBuilder.toString();
                    t.qualifiedName = typeNameBuilder.toString();
                    return;

                default:
                    if ( runtimeException )
                    {
                        throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                    }
                    else
                    {
                        throw createInvalidTokenParseException( tokenizer.input(), token );
                    }

            }
        }

        if ( !( identifier_seen || type_arguments_seen ) )
        {
            if ( runtimeException )
            {
                throw createUnexpectedEndOfInputIllegalArgumentException( tokenizer.input(), tokenizer.length() );
            }
            else
            {
                throw createUnexpectedEndOfInputParseException( tokenizer.input(), tokenizer.length() );
            }
        }

        t.className = classNameBuilder.toString();
        t.qualifiedName = typeNameBuilder.toString();
    }

    /**
     * JLS - Java SE 7 Edition - Chapter 18. Syntax
     * <pre>
     * TypeArguments:
     *      &lt; TypeArgument { , TypeArgument } &gt;
     * </pre>
     *
     * @see #parseTypeArgument(org.jomc.model.JavaTypeName.Tokenizer, org.jomc.model.JavaTypeName, boolean)
     */
    private static void parseTypeArguments( final Tokenizer tokenizer, final JavaTypeName t,
                                            final boolean runtimeException )
        throws ParseException
    {
        boolean lt_seen = false;
        boolean argument_seen = false;
        Token token;

        while ( ( token = tokenizer.next() ) != null )
        {
            switch ( token.getKind() )
            {
                case Tokenizer.TK_LT:
                    if ( lt_seen || argument_seen )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    lt_seen = true;
                    argument_seen = false;
                    break;

                case Tokenizer.TK_GT:
                    if ( !argument_seen )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    return;

                case Tokenizer.TK_COMMA:
                    if ( !argument_seen )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    argument_seen = false;
                    break;

                case Tokenizer.TK_IDENTIFIER:
                    if ( !lt_seen || argument_seen )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    argument_seen = true;
                    tokenizer.back();
                    parseTypeArgument( tokenizer, t, runtimeException );
                    break;

                case Tokenizer.TK_QM:
                    if ( !lt_seen || argument_seen )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    argument_seen = true;
                    tokenizer.back();
                    parseTypeArgument( tokenizer, t, runtimeException );
                    break;

                default:
                    if ( runtimeException )
                    {
                        throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                    }
                    else
                    {
                        throw createInvalidTokenParseException( tokenizer.input(), token );
                    }

            }
        }

        if ( runtimeException )
        {
            throw createUnexpectedEndOfInputIllegalArgumentException( tokenizer.input(), tokenizer.length() );
        }
        else
        {
            throw createUnexpectedEndOfInputParseException( tokenizer.input(), tokenizer.length() );
        }
    }

    /**
     * <dl><dt>JLS - Java SE 7 Edition - Chapter 18. Syntax</dt>
     * <dd><pre>
     * TypeArgument:
     *      ReferenceType
     *      ? [ ( extends | super ) ReferenceType ]
     * </pre></dd>
     * <dt>JLS - Java SE 7 Edition - Chapter 4.5.1. Type Arguments and Wildcards</dt>
     * <dd><pre>
     * TypeArgument:
     *      ReferenceType
     *      Wildcard
     *
     * Wildcard:
     *      ? WildcardBounds<i>opt</i>
     *
     * WildcardBounds:
     *      extends ReferenceType
     *      super ReferenceType
     * </pre></dd></dl>
     */
    private static void parseTypeArgument( final Tokenizer tokenizer, final JavaTypeName t,
                                           final boolean runtimeException )
        throws ParseException
    {
        boolean qm_seen = false;
        boolean keyword_seen = false;
        Token token;

        final Argument argument = new Argument();
        t.getArguments().add( argument );

        while ( ( token = tokenizer.next() ) != null )
        {
            switch ( token.getKind() )
            {
                case Tokenizer.TK_IDENTIFIER:
                    if ( qm_seen && !keyword_seen )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    tokenizer.back();
                    argument.typeName = new JavaTypeName();
                    parseReferenceType( tokenizer, argument.getTypeName(), true, runtimeException );
                    return;

                case Tokenizer.TK_QM:
                    if ( qm_seen )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    qm_seen = true;
                    argument.wildcard = true;
                    break;

                case Tokenizer.TK_KEYWORD:
                    if ( !qm_seen || keyword_seen
                             || !( "extends".equals( token.getValue() ) || "super".equals( token.getValue() ) ) )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    keyword_seen = true;
                    argument.wildcardBounds = token.getValue();
                    break;

                case Tokenizer.TK_COMMA:
                case Tokenizer.TK_GT:
                    if ( !qm_seen || keyword_seen )
                    {
                        if ( runtimeException )
                        {
                            throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                        }
                        else
                        {
                            throw createInvalidTokenParseException( tokenizer.input(), token );
                        }
                    }
                    tokenizer.back();
                    return;

                default:
                    if ( runtimeException )
                    {
                        throw createInvalidTokenIllegalArgumentException( tokenizer.input(), token );
                    }
                    else
                    {
                        throw createInvalidTokenParseException( tokenizer.input(), token );
                    }

            }
        }

        if ( runtimeException )
        {
            throw createUnexpectedEndOfInputIllegalArgumentException( tokenizer.input(), tokenizer.length() );
        }
        else
        {
            throw createUnexpectedEndOfInputParseException( tokenizer.input(), tokenizer.length() );
        }
    }

    private static ParseException createInvalidTokenParseException( final String input, final Token token )
    {
        if ( token.getValue().length() > 1 )
        {
            return new ParseException( getMessage( "invalidWord", input, token.getValue(),
                                                   token.getPosition() ), token.getPosition() );

        }
        else
        {
            return new ParseException( getMessage( "invalidCharacter", input, token.getValue(),
                                                   token.getPosition() ), token.getPosition() );

        }
    }

    private static IllegalArgumentException createInvalidTokenIllegalArgumentException( final String input,
                                                                                        final Token token )
    {
        if ( token.getValue().length() > 1 )
        {
            return new IllegalArgumentException( getMessage( "invalidWord", input, token.getValue(),
                                                             token.getPosition() ) );

        }
        else
        {
            return new IllegalArgumentException( getMessage( "invalidCharacter", input, token.getValue(),
                                                             token.getPosition() ) );

        }
    }

    private static ParseException createUnexpectedEndOfInputParseException( final String input,
                                                                            final int length )
    {
        return new ParseException( getMessage( "unexpectedEndOfInput", input, length ), length );
    }

    private static IllegalArgumentException createUnexpectedEndOfInputIllegalArgumentException( final String input,
                                                                                                final int length )
    {
        return new IllegalArgumentException( getMessage( "unexpectedEndOfInput", input, length ) );
    }

    private static String getMessage( final String key, final Object... args )
    {
        return MessageFormat.format( ResourceBundle.getBundle(
            JavaTypeName.class.getName().replace( '.', '/' ), Locale.getDefault() ).
            getString( key ), args );

    }

    private static final class Token
    {

        private int kind;

        private final int position;

        private final String value;

        private Token( final int kind, final int position, final String value )
        {
            super();
            this.kind = kind;
            this.position = position;
            this.value = value;
        }

        private int getKind()
        {
            return this.kind;
        }

        private int getPosition()
        {
            return this.position;
        }

        private String getValue()
        {
            return this.value;
        }

    }

    private static final class Tokenizer
    {

        private static final int TK_BASIC_TYPE = 1;

        private static final int TK_KEYWORD = 2;

        private static final int TK_LITERAL = 3;

        private static final int TK_IDENTIFIER = 4;

        private static final int TK_LPAR = 5;

        private static final int TK_RPAR = 6;

        private static final int TK_LT = 7;

        private static final int TK_GT = 8;

        private static final int TK_COMMA = 9;

        private static final int TK_DOT = 10;

        private static final int TK_QM = 11;

        private final String input;

        private int token;

        private final List<Token> tokens;

        private int length;

        private Tokenizer( final String input, final boolean runtimeException ) throws ParseException
        {
            super();
            this.input = input;
            this.token = 0;
            this.tokens = tokenize( input, runtimeException );

            if ( !this.tokens.isEmpty() )
            {
                final Token last = this.tokens.get( this.tokens.size() - 1 );
                this.length = last.getPosition() + last.getValue().length();
            }
        }

        private String input()
        {
            return this.input;
        }

        private Token next()
        {
            final int idx = this.token++;
            return idx < this.tokens.size() ? this.tokens.get( idx ) : null;
        }

        private void back()
        {
            this.token--;
        }

        private int length()
        {
            return this.length;
        }

        private static List<Token> tokenize( final String input, final boolean runtimeException )
            throws ParseException
        {
            final List<Token> list = new LinkedList<Token>();
            final ParsePosition pos = new ParsePosition( 0 );

            for ( Token t = nextToken( pos, input, runtimeException );
                  t != null;
                  t = nextToken( pos, input, runtimeException ) )
            {
                list.add( t );
            }

            return Collections.unmodifiableList( list );
        }

        private static Token nextToken( final ParsePosition pos, final String str, final boolean runtimeException )
            throws ParseException
        {
            for ( final int s0 = str.length(); pos.getIndex() < s0; pos.setIndex( pos.getIndex() + 1 ) )
            {
                if ( !Character.isWhitespace( str.charAt( pos.getIndex() ) ) )
                {
                    break;
                }
            }

            int idx = pos.getIndex();
            Token token = null;

            if ( idx < str.length() )
            {
                // Check separator characters.
                switch ( str.charAt( idx ) )
                {
                    case ',':
                        token = new Token( TK_COMMA, idx, "," );
                        pos.setIndex( idx + 1 );
                        break;
                    case '.':
                        token = new Token( TK_DOT, idx, "." );
                        pos.setIndex( idx + 1 );
                        break;
                    case '<':
                        token = new Token( TK_LT, idx, "<" );
                        pos.setIndex( idx + 1 );
                        break;
                    case '>':
                        token = new Token( TK_GT, idx, ">" );
                        pos.setIndex( idx + 1 );
                        break;
                    case '[':
                        token = new Token( TK_LPAR, idx, "[" );
                        pos.setIndex( idx + 1 );
                        break;
                    case ']':
                        token = new Token( TK_RPAR, idx, "]" );
                        pos.setIndex( idx + 1 );
                        break;
                    case '?':
                        token = new Token( TK_QM, idx, "?" );
                        pos.setIndex( idx + 1 );
                        break;

                    default:
                        token = null;

                }

                // Check basic type.
                if ( token == null )
                {
                    for ( final String basicType : JavaLanguage.BASIC_TYPES )
                    {
                        if ( str.substring( idx ).startsWith( basicType ) )
                        {
                            idx += basicType.length();

                            if ( idx >= str.length()
                                     || !Character.isJavaIdentifierPart( str.charAt( idx ) ) )
                            {
                                token = new Token( TK_BASIC_TYPE, pos.getIndex(), basicType );
                                pos.setIndex( idx );
                                break;
                            }

                            idx -= basicType.length();
                        }
                    }
                }

                // Check keyword.
                if ( token == null )
                {
                    for ( final String keyword : JavaLanguage.KEYWORDS )
                    {
                        if ( str.substring( idx ).startsWith( keyword ) )
                        {
                            idx += keyword.length();

                            if ( idx >= str.length()
                                     || !Character.isJavaIdentifierPart( str.charAt( idx ) ) )
                            {
                                token = new Token( TK_KEYWORD, pos.getIndex(), keyword );
                                pos.setIndex( idx );
                                break;
                            }

                            idx -= keyword.length();
                        }
                    }
                }

                // Check boolean literals.
                if ( token == null )
                {
                    for ( final String literal : JavaLanguage.BOOLEAN_LITERALS )
                    {
                        if ( str.substring( idx ).startsWith( literal ) )
                        {
                            idx += literal.length();

                            if ( idx >= str.length()
                                     || !Character.isJavaIdentifierPart( str.charAt( idx ) ) )
                            {
                                token = new Token( TK_LITERAL, pos.getIndex(), literal );
                                pos.setIndex( idx );
                                break;
                            }

                            idx -= literal.length();
                        }
                    }
                }

                // Check null literal.
                if ( token == null )
                {
                    if ( str.substring( idx ).startsWith( JavaLanguage.NULL_LITERAL ) )
                    {
                        idx += JavaLanguage.NULL_LITERAL.length();

                        if ( idx >= str.length()
                                 || !Character.isJavaIdentifierPart( str.charAt( idx ) ) )
                        {
                            token = new Token( TK_LITERAL, pos.getIndex(), JavaLanguage.NULL_LITERAL );
                            pos.setIndex( idx );
                        }
                        else
                        {
                            idx -= JavaLanguage.NULL_LITERAL.length();
                        }
                    }
                }

                // Check identifier.
                if ( token == null )
                {
                    for ( final int s0 = str.length(); idx < s0; idx++ )
                    {
                        if ( !( idx == pos.getIndex()
                                ? Character.isJavaIdentifierStart( str.charAt( idx ) )
                                : Character.isJavaIdentifierPart( str.charAt( idx ) ) ) )
                        {
                            break;
                        }
                    }

                    if ( idx != pos.getIndex() )
                    {
                        token = new Token( TK_IDENTIFIER, pos.getIndex(), str.substring( pos.getIndex(), idx ) );
                        pos.setIndex( idx );
                    }
                }

                if ( token == null )
                {
                    final Token invalidToken =
                        new Token( Integer.MIN_VALUE, idx, Character.toString( str.charAt( idx ) ) );

                    if ( runtimeException )
                    {
                        throw createInvalidTokenIllegalArgumentException( str, invalidToken );
                    }
                    else
                    {
                        throw createInvalidTokenParseException( str, invalidToken );
                    }
                }
            }

            return token;
        }

    }

}