/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */

package org.apache.jdo.tck.util.signature;

import java.lang.reflect.Array;
import java.lang.reflect.Member;
import java.lang.reflect.Modifier;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Constructor;

import java.util.Iterator;
import java.util.Set;
import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.TreeSet;
import java.util.HashSet;

import java.text.ParseException;

import java.io.PrintWriter;
import java.io.IOException;
import java.io.File;
import java.io.FileReader;
import java.io.LineNumberReader;


/**
 * Tests classes for correct signatures.
 *
 * @author Martin Zaun
 */
public class SignatureVerifier {
    /** The new-line character on this system. */
    static protected final String NL = System.getProperty("line.separator");

    /** All modifiers defined in java.lang.reflect.Modifier.
     * This field is used to filter out vm-specific modifiers 
     * such as found in Sun's vm to identify Enum and Annotation.
     */
    static protected final int ALL_MODIFIERS = 
            Modifier.ABSTRACT |
            Modifier.FINAL |
            Modifier.INTERFACE |
            Modifier.NATIVE |
            Modifier.PRIVATE |
            Modifier.PROTECTED |
            Modifier.PUBLIC |
            Modifier.STATIC |
            Modifier.STRICT |
            Modifier.SYNCHRONIZED |
            Modifier.TRANSIENT |
            Modifier.VOLATILE;

    /** Pseudo modifier for annotation */
    static protected final int ANNOTATION = 0x2000;

    /** Pseudo modifier for enum */
    static protected final int ENUM = 0x4000;

    /** A writer for standard output. */
    protected final PrintWriter log;

    /** Flag to print error output only. */
    private boolean quiet;

    /** Flag to also print verbose output. */
    private boolean verbose;

    /** The parse to be used for parsing signature descriptor files. */
    protected final Parser parser = new Parser();

    /** The classloader to be used for loading types. */
    protected final ClassLoader classLoader;

    /** The currently tested Class. */
    protected Class cls;
    
    /** All untested, declared members of the current class. */
    protected final Set members = new HashSet();

    /** Collects names of loadable classes. */
    private final Set loading = new TreeSet();

    /** Collects names of unloadable classes. */
    private final Set notLoading = new TreeSet();

    /** Counts tested features (class, constructor, fields, methods). */
    private int tested;

    /** Counts missing members (constructor, fields, methods). */
    private int missing;

    /** Counts non-matching features (class, constructor, fields, methods). */
    private int mismatch;

    /** Counts matching features (class, constructor, fields, methods). */
    private int matching;

    /** Counts public non-standard members (constructor, fields, methods). */
    private int nonStandard;

    /** Counts other reported problems (e.g., accessing field values). */
    private int otherProblems;

    /** Constructs a test instance. */
    public SignatureVerifier(ClassLoader loader, PrintWriter log,
                             boolean quiet, boolean verbose) {
        classLoader = this.getClass().getClassLoader();
        this.log = log;
        this.quiet = quiet;
        this.verbose = (!quiet && verbose);
    }    

    /** Constructs a test instance. */
    public SignatureVerifier(PrintWriter log,
                             boolean quiet, boolean verbose) {
        this(SignatureVerifier.class.getClassLoader(), log, quiet, verbose);
    }

    // ----------------------------------------------------------------------
    // Local Logging Methods
    // ----------------------------------------------------------------------

    /** Prints an error message. */
    protected void logError(String msg) {
        log.println(msg);
    }

    /** Prints an info message. */
    protected void logInfo(String msg) {
        if (!quiet) {
            log.println(msg);
        }
    }

    /** Prints a verbose message. */
    protected void logVerbose(String msg) {
        if (verbose) {
            log.println(msg);
        }
    }

    // ----------------------------------------------------------------------
    // Test Methods
    // ----------------------------------------------------------------------

    /**
     * Tests the signature of classes (in the specified classloader) against
     * a list of signature descriptor files; returns with a status code.
     * @param descrFileNames list of signature descriptor file names
     * @return zero if all tests have passed and no problems were detected
     */
    public int test(List descrFileNames)
        throws IOException, ParseException {
        // check argument
        if (descrFileNames == null || descrFileNames.isEmpty()) {
            final String m = ("ERROR: No signature descriptor file to parse.");
            logError(m);
            return -1;
        }
        
        // clear statistics
        loading.clear();
        notLoading.clear();
        tested = 0;
        missing = 0;
        mismatch = 0;
        matching = 0;
        nonStandard = 0;
        otherProblems = 0;

        // process descriptor files
        parser.parse(descrFileNames);
        report();
        
        // return a positive value in case of any problems
        return (notLoading.size() + missing + mismatch + nonStandard
                + otherProblems);
    }

    /**
     * Reports the results of the last signature test run.
     */
    public void report() {
        logInfo("");
        logInfo("Signature Test Results");
        logInfo("======================");
        logInfo("");
        logInfo("    tested features:          " + tested);
        logInfo("");
        logInfo("Successes:");
        logInfo("    matching features:        " + matching);
        logInfo("    loadable classes:         " + loading.size());
        logInfo("");
        logInfo("Failures:");
        logInfo("    missing features:         " + missing);
        logInfo("    non-matching features:    " + mismatch);
        logInfo("    non-standard features:    " + nonStandard);
        logInfo("    unloadable classes:       " + notLoading.size());
        logInfo("    other problems:           " + otherProblems);
        logInfo("");
    }
    
    // ----------------------------------------------------------------------
    // Test Logic
    // ----------------------------------------------------------------------

    /** Handles class loading problems. */
    protected void handleNotLoading(Throwable t) {
        notLoading.add(t.getMessage().replace('/', '.'));
        final String m = ("--- failed loading class;" + NL
                          + "    caught: " + t);
        logError(m);
    }

    /** Handles missing members. */
    protected void handleMissing(String msg, String exp) {
        missing++;
        final String m = ("--- " + msg + NL
                          + "    expected: " + exp + NL
                          + "    class:    " + Formatter.toString(cls));
        logError(m);
    }

    /** Handles non-matching features. */
    protected void handleMismatch(String msg, String exp, String fnd) {
        mismatch++;
        final String m = ("--- " + msg + NL
                          + "    expected: " + exp + NL
                          + "    found:    " + fnd + NL
                          + "    class:    " + Formatter.toString(cls));
        logError(m);
    }

    /** Handles public non-standard features. */
    protected void handleNonStandard(String msg, String fnd) {
        nonStandard++;
        final String m = ("--- " + msg + NL
                          + "    found:    " + fnd + NL
                          + "    class:    " + Formatter.toString(cls));
        logError(m);
    }

    /** Handles other problems. */
    protected void handleProblem(String msg, String exp) {
        otherProblems++;
        final String m = ("--- " + msg + NL
                          + "    expected: " + exp + NL
                          + "    class:    " + Formatter.toString(cls));
        logError(m);
    }

    /** Handles a perfect feature match. */
    protected void handleMatch(String msg, String fnd) {
        matching++;
        final String m = ("+++ " + msg + fnd);
        logVerbose(m);
    }

    /** Returns the class objects for given (Java) user type names. */
    protected Class[] getClasses(String[] userTypeName) {
        final Class[] cls = new Class[userTypeName.length];
        for (int i = userTypeName.length - 1; i >= 0; i--) {
            cls[i] = getClass(userTypeName[i]);
        }
        return cls;
    }
    
    /** Returns the class object for a given (Java) user type name. */
    protected Class getClass(String userTypeName) {
        // use helper for retrieving class objects for primitive types
        Class cls = TypeHelper.primitiveClass(userTypeName);
        if (cls != null) {
            return cls;
        }
        
        // load class
        try {
            final String r = TypeHelper.reflectionTypeName(userTypeName);
            cls = Class.forName(r, false, classLoader);
            loading.add(userTypeName);
        } catch (LinkageError err) {
            handleNotLoading(err);
        } catch (ClassNotFoundException ex) {
            handleNotLoading(ex);
        }
        return cls;
    }

    /** Check an expected value expression (the value of a static field
     *  or the default value of an annotation method), comparing it to
     *  the actual value from the Field or Method object.
     *  Only supports primitive, enum, empty array of enum, empty array of
     *  annotation, and String.
     * @param value the String form of the value from the signature file
     * @param type the type as declared in the class
     * @param actual the actual value
     * @return the description of the expected value, or null if ok
     * @throws java.lang.NumberFormatException
     */
    protected String checkValue(String value, String type, Object actual) 
            throws NumberFormatException {
        // note array type
        boolean isArray = false;
        if (type.endsWith("[]")) {
            isArray = true;
            type = type.substring(0, type.length() - 2);
            // remove { from beginning and } from end
            value = value.substring(1, value.length() - 1);
        }
        // first check primitive type
        final Object exp;
        Class expClass;
        final boolean ok;
        if (type.equals("byte")) {
            if (isArray) {
                ok = actual.getClass().getComponentType().equals(byte.class);
            } else {
                ok = Byte.valueOf(value).equals(actual);
            }
        } else if (type.equals("short")) {
            if (isArray) {
                ok = actual.getClass().getComponentType().equals(short.class);
            } else {
                ok = Short.valueOf(value).equals(actual);
            }
        } else if (type.equals("int")) {
            if (isArray) {
                ok = actual.getClass().getComponentType().equals(int.class);
            } else {
                ok = Integer.valueOf(value).equals(actual);
            }
        } else if (type.equals("long")) {
            if (isArray) {
                ok = actual.getClass().getComponentType().equals(long.class);
            } else {
                ok = Long.valueOf(value).equals(actual);
            }
        } else if (type.equals("float")) {
            if (isArray) {
                ok = actual.getClass().getComponentType().equals(float.class);
            } else {
                ok = Float.valueOf(value).equals(actual);
            }
        } else if (type.equals("double")) {
            if (isArray) {
                ok = actual.getClass().getComponentType().equals(double.class);
            } else {
                ok = Double.valueOf(value).equals(actual);
            }
        } else if (type.equals("char")) {
            if (isArray) {
                ok = actual.getClass().getComponentType().equals(char.class);
            } else {
                ok = new Character(value.charAt(1)).equals(actual);
            }
        // next check Class
        } else if (type.equals("java.lang.Class")) {
            if (isArray) {
                ok = actual.getClass().getComponentType().equals(Class.class);
            } else {
                // strip ".class" from type name
                int offset = value.indexOf(".class");
                value = value.substring(0, offset);
                ok = getClass(value).equals(actual);
            }
        // next check String
        } else if (type.equals("java.lang.String")) {
            if (isArray) {
                ok = actual.getClass().getComponentType().equals(String.class);
            } else {
                // cut off '\"' chars at begin and end
                final String s;
                if (value.length() > 1) {
                    s = value.substring(1, value.length() - 1);
                } else {
                    s = "";
                }
                ok = (String.valueOf(s)).equals(actual);
            }
        // now check non-java.lang annotations and enums
        } else {
            expClass = getClass(type);
            if (isArray) {
                // check if the actual component type is the right class
                // don't check the actual values because only empty arrays
                // are supported.
                ok = actual.getClass().getComponentType().equals(expClass);
            } else if (expClass.isAnnotation()) {
                // check whether the type isAssignableFrom the class of the actual value, 
                // if type is an annotation. The actual value is a dynamic proxy, 
                // so an equals comparison does not work. 
                ok = expClass.isAssignableFrom(actual.getClass());
            } else {
                // get the actual value which must be a static class.field
                Object expectedValue = null;
                try {
                    // now get actual value
                    // separate value name into class and field name
                    int lastDot = value.lastIndexOf(".");
                    String expectedClassName = value.substring(0, lastDot);
                    String expectedFieldName = 
                            value.substring(lastDot + 1, value.length());
                    // get Class object from class name
                    Class expectedClass = getClass(expectedClassName);
                    if (expectedClass == null) 
                        throw new ClassNotFoundException();
                    // get Field object from Class and field name
                    Field expectedField = 
                            expectedClass.getField(expectedFieldName);
                    expectedValue = expectedField.get(null);
                } catch (NoSuchFieldException ex) {
                    handleNotLoading(ex);
                } catch (SecurityException ex) {
                    handleNotLoading(ex);
                } catch (IllegalArgumentException ex) {
                    handleNotLoading(ex);
                } catch (IllegalAccessException ex) {
                    handleNotLoading(ex);
                } catch (ClassNotFoundException ex) {
                    handleNotLoading(ex);
                }
                ok = expectedValue.equals(actual);
            }
        }
        // return message if not ok
        if (ok) return null;
        else return value;
    }

    /** Validates a field against a prescribed signature. */
    protected void checkField(int mods, String type, String name,
                              String value) {
        tested++;
        type = TypeHelper.qualifiedUserTypeName(type);

        // get field
        final Field field;
        try {
            field = cls.getDeclaredField(name);
        } catch (NoSuchFieldException ex) {
            handleMissing(
                "missing field: ",
                Formatter.toString(mods, type, name, value));
            return;
        } catch (LinkageError err) {
            handleNotLoading(err);
            return;
        }

        // check modifiers
        if (cls.isInterface()) {
            // fields interfaces are implicitly public, static, and final
            mods |= Modifier.PUBLIC;
            mods |= Modifier.STATIC;
            mods |= Modifier.FINAL;
        }
        if (mods != convertModifiers(field)) {
            handleMismatch(
                "field declaration: non-matching modifiers;",
                Formatter.toString(mods, type, name, null),
                Formatter.toString(field, null));
        }
            
        // check type
        if (!TypeHelper.isNameMatch(type, field.getType())) {
            handleMismatch(
                "field declaration: non-matching type;",
                Formatter.toString(mods, type, name, null),
                Formatter.toString(field, null));
        }

        // check field value if any
        Object actualValue = null;
        if (value != null) {
            // only support for public, static, and final fields
            final int m = (Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL);
            if ((mods & m) == 0) {
                handleProblem("field declaration: ignoring field value "
                              + "definition in descriptor file;",
                              Formatter.toString(mods, type, name, value));
            } else {
                // compare field's expected with found value
                try {
                    actualValue = field.get(null);
                } catch (IllegalAccessException ex) {
                    handleProblem("field declaration: cannot access field "
                                  + "value, exception: " + ex + ";",
                                  Formatter.toString(mods, type, name, value));
                }
                String error = checkValue(value, type, actualValue);
                if (error != null) {
                    handleMismatch(
                        "field declaration: non-matching values;",
                        Formatter.toString(mods, type, name, error),
                        Formatter.toString(field, "\"" + actualValue + "\""));
                }
            }
        }
        
        // field OK
        members.remove(field);
        handleMatch("has field: ", Formatter.toString(field, actualValue));
    }

    /** Validates a constructor against a prescribed signature. */
    protected void checkConstructor(int mods, String[] params,
                                    String[] excepts) {
        tested++;
        params = TypeHelper.qualifiedUserTypeNames(params);
        excepts = TypeHelper.qualifiedUserTypeNames(excepts);

        // get parameter classes
        final Class[] prms = getClasses(params);
        if (prms == null) {
            return;
        }

        // get constructor
        final Constructor ctor;
        try {
            ctor = cls.getDeclaredConstructor(prms);
        } catch (NoSuchMethodException ex) {
            String name = cls.getName();
            final int i = name.lastIndexOf('.');
            name = (i < 0 ? name : name.substring(i));
            handleMissing(
                "missing constructor: ",
                Formatter.toString(mods, null, name, params, excepts));
            return;
        } catch (LinkageError err) {
            handleNotLoading(err);
            return;
        }
        
        // check modifiers
        if (mods != ctor.getModifiers()) {
            handleMismatch(
                "constructor declaration: non-matching modifiers;",
                Formatter.toString(mods, null, cls.getName(), params, excepts),
                Formatter.toString(ctor));
        }

        // check exceptions
        if (!TypeHelper.isNameMatch(excepts, ctor.getExceptionTypes())) {
            handleMismatch(
                "method declaration: non-matching exceptions;",
                Formatter.toString(mods, null, cls.getName(), params, excepts),
                Formatter.toString(ctor));
        }

        // constructor OK
        members.remove(ctor);
        handleMatch("has constructor: ", Formatter.toString(ctor));
    }
    
    /** Validates a method against a prescribed signature. */
    protected void checkMethod(int mods, String result, String name,
                               String[] params, String[] excepts, 
                               String value) {
        tested++;
        params = TypeHelper.qualifiedUserTypeNames(params);
        excepts = TypeHelper.qualifiedUserTypeNames(excepts);
        result = TypeHelper.qualifiedUserTypeName(result);

        // get parameter classes
        final Class[] prms = getClasses(params);
        if (prms == null) {
            return;
        }

        // get method
        final Method method;
        try {
            method = cls.getDeclaredMethod(name, prms);
        } catch (NoSuchMethodException ex) {
            handleMissing(
                "missing method: ",
                Formatter.toString(mods, result, name, params, excepts));
            return;
        } catch (LinkageError err) {
            handleNotLoading(err);
            return;
        }
        
        // check modifiers
        if (cls.isInterface()) {
            // methods in interfaces are implicitly public and abstract final
            mods |= Modifier.PUBLIC;
            mods |= Modifier.ABSTRACT;
        }
        Class resultType = getClass(result);
        if (resultType.isAnnotation()) {
            // add ANNOTATION modifier if the result type is an annotation
            mods |= ANNOTATION;
        }
        if (mods != convertModifiers(method)) {
            handleMismatch(
                "method declaration: non-matching modifiers;",
                Formatter.toString(mods, result, name, params, excepts),
                Formatter.toString(method));
        }

        // check return type
        if (!TypeHelper.isNameMatch(result, method.getReturnType())) {
            handleMismatch(
                "method declaration: non-matching return type",
                Formatter.toString(mods, result, name, params, excepts),
                Formatter.toString(method));
        }

        // check exceptions
        if (!TypeHelper.isNameMatch(excepts, method.getExceptionTypes())) {
            handleMismatch(
                "method declaration: non-matching exceptions;",
                Formatter.toString(mods, result, name, params, excepts),
                Formatter.toString(method));
        }
        
        // check default value of an annotation element (method)
        if (value != null) {
            Object actualValue = null;
            try {
                actualValue = method.getDefaultValue();
            } catch (Exception ex) {
                handleProblem("method declaration: cannot access default "
                              + "value, exception: " + ex + ";",
                              Formatter.toString(mods, result, name, value));
            }
            // check value expected versus actual
            String wrong = checkValue(value, result, actualValue);

            if (wrong != null) {
                handleMismatch(
                    "method declaration: non-matching default value;",
                    Formatter.toString(mods, result, name + "() default ", wrong),
                    Formatter.toString(method));
            }
        }

        // method OK
        members.remove(method);
        handleMatch("has method: ", Formatter.toString(method));
    }

    /** Validates a class declaration against a prescribed signature. */
    protected void checkClass(int mods, String name,
                              String[] ext, String[] impl) {
        logVerbose("");
        logVerbose("testing " + Formatter.toString(mods, name, ext, impl));
        tested++;
        ext = TypeHelper.qualifiedUserTypeNames(ext);
        impl = TypeHelper.qualifiedUserTypeNames(impl);

        // get and assign currently processed class
        cls = getClass(name);
        if (cls == null) {
            return; // can't load class
        }  

        // collect all declared members of current class
        members.clear();
        try {
            members.addAll(Arrays.asList(cls.getDeclaredFields()));
            members.addAll(Arrays.asList(cls.getDeclaredConstructors()));
            members.addAll(Arrays.asList(cls.getDeclaredMethods()));
        } catch (LinkageError err) {
            handleNotLoading(err);
        }
      
        // check modifiers
        final boolean isInterface = ((mods & Modifier.INTERFACE) != 0);
        if (isInterface) {
            mods |= Modifier.ABSTRACT;
        }
        if (mods != convertModifiers(cls)) {
            handleMismatch(
                "class declaration: non-matching modifiers;",
                Formatter.toString(mods, name, ext, impl),
                Formatter.toString(cls));
        }

        // check superclass and extended/implemented interfaces
        final Class superclass = cls.getSuperclass();
        final Class[] interfaces = cls.getInterfaces();	
        if (isInterface) {
            //assert (impl.length == 0);
            if (!TypeHelper.isNameMatch(ext, interfaces)) {
                handleMismatch(
                    "interface declaration: non-matching interfaces;",
                    Formatter.toString(mods, name, ext, impl),
                    Formatter.toString(cls));
            }
        } else {
            //assert (ext.length <= 1);
            final String s = (ext.length == 0 ? "java.lang.Object" : ext[0]);
            if (!TypeHelper.isNameMatch(s, superclass)) {
                handleMismatch(
                    "class declaration: non-matching superclass;",
                    Formatter.toString(mods, name, ext, impl),
                    Formatter.toString(cls));
            }
            if (!TypeHelper.isNameMatch(impl, interfaces)) {
                handleMismatch(
                    "class declaration: non-matching interfaces;",
                    Formatter.toString(mods, name, ext, impl),
                    Formatter.toString(cls));
            }
        }

        handleMatch("has class: ", Formatter.toString(cls));
    }

    /** Runs checks on a class after its members have been validated. */
    protected void postCheckClass() {
        if (cls == null) {
            return; // nothing to do if class couldn't be loaded
        }

        // check for public non-standard members
        for (Iterator i = members.iterator(); i.hasNext();) {
            final Member m = (Member)i.next();
            if ((m.getModifiers() & Modifier.PUBLIC) != 0) {
                handleNonStandard("non-standard, public member;",
                                  Formatter.toString(m));
            }
        }
    }

    /** Return modifiers for the class, but handling enum and annotation
     * as if they had a special modifier.
     * @param cls the class
     * @return modifiers of the class with extra flags for enum and annotation
     */
    protected int convertModifiers(Class cls) {
        int result = cls.getModifiers();
        // first remove extraneous stuff
        result &= ALL_MODIFIERS;
        // if enum, set pseudo enum flag
        if (cls.isEnum())
            result |= ENUM;
        // if annotation, set pseudo annotation flag
        if (cls.isAnnotation()) 
            result |= ANNOTATION;
        return result;
    }

    /** Return modifiers for the method, but handling enum and annotation
     * as if they had a special modifier.
     * @param method the method
     * @return modifiers of the class with extra flags for enum and annotation
     */
    protected int convertModifiers(Method method) {
        int result = method.getModifiers();
        // first remove extraneous stuff
        result &= ALL_MODIFIERS;
        // if enum return type, set pseudo enum flag
        if (method.getReturnType().isEnum())
            result |= ENUM;
        // if annotation, set pseudo annotation flag
        if (method.getReturnType().isAnnotation()) 
            result |= ANNOTATION;
        // if return type is an enum class, un-set FINAL modifier in all methods
        // because in Java 5, methods are generated as final; in Java 6, not
        if (method.getDeclaringClass().isEnum()) 
            result &= ~Modifier.FINAL;
        return result;
    }

    /** Return modifiers for the field, but handling enum
     * as if it had a special modifier.
     * @param field the field
     * @return modifiers of the class with extra flag for enum
     */
    protected int convertModifiers(Field field) {
        int result = field.getModifiers();
        // first remove extraneous stuff
        result &= ALL_MODIFIERS;
        // if enum, set pseudo enum flag
        if (field.isEnumConstant())
            result |= ENUM;
        return result;
    }

    // ----------------------------------------------------------------------
    // Parser for Signature Descriptor Files
    // ----------------------------------------------------------------------

    /**
     * For parsing of signature descriptor files.
     */
    protected class Parser {
        /** The current descriptor file being parsed. */
        private File descriptorFile;

        /** The line number reader for the current descriptor file. */
        private LineNumberReader ir;

        /** A look-ahead token to be read next. */
        private String nextToken;

        /** Returns an error message reporting an unextected end of file. */
        protected String msgUnexpectedEOF() {
            return ("unexpected end of file at line: " + ir.getLineNumber()
                    + ", file: " + descriptorFile.getPath());
        }

        /** Returns an error message reporting an unextected token. */
        protected String msgUnexpectedToken(String t) {
            return ("unexpected token: '" + t + "'"
                    + " in line: " + ir.getLineNumber()
                    + ", file: " + descriptorFile.getPath());
        }

        /** Retrieves the look-ahead token to be parsed next. */
        protected String getLookAhead() {
            final String t = nextToken;
            nextToken = null;
            return t;
        }

        /** Sets the look-ahead token to be parsed next. */
        protected void setLookAhead(String t) {
            //assert (nextToken == null);
            nextToken = t;
        }

        /**
         * Skips any "white space" and /* style comments
         * and returns whether there are more
         * characters to be parsed.
         */
        protected boolean skip() throws IOException {
            Character c;
            Character cnext;
            // Permitted states are white, commentSlashStar, commentSlashSlash
            String state = "white";
            final Character SLASH = '/';
            final Character STAR = '*';
            while (true) {
                c = peek();
                if (c == null)
                    return false;
                if (state.equals("commentSlashStar") && STAR.equals(c)) {
                    cnext = peek();
                    if (cnext == null)
                        return false;
                    if (SLASH.equals(cnext)) {
                        state = "white";
                    }
                    continue;
                }
                if (state.equals("white")) {
                    if (SLASH.equals(c)) {
                        cnext = peek();
                        if (cnext == null)
                            return false;
                        if (STAR.equals(cnext)) {
                            state = "commentSlashStar";
                            continue;
                        }
                        if (SLASH.equals(cnext)) {
                            state = "commentSlashSlash";
                            continue;
                        }
                    }
                    if (!Character.isWhitespace(c)) {
                        break;
                    }
                }
                if (state.equals("commentSlashSlash")
                    && (c == Character.LINE_SEPARATOR
                        || c == '\n'
                        || c == '\r')) {
                    state = "white";
                    continue;
                }
            }
            ir.reset();
            return true;
        }

        /**
         * Returns next char, or null if there is none
         */
        protected Character peek() throws IOException {
            ir.mark(1);
            int i;
            if ((i = ir.read()) < 0) {
                return null;
            }            
            return Character.valueOf((char)i);
        }

        /**
         * Scans for an (unqualified) identifier.
         * @return <code>null</code> if the next token is not an identifier
         */
        protected String scanIdentifier()
            throws IOException, ParseException {
            // parse stored token if any
            String t;
            if ((t = getLookAhead()) != null) {
                if (!Character.isJavaIdentifierStart(t.charAt(0))) {
                    setLookAhead(t); // not an identifier
                    return null;
                }
                return t;
            }

            // parse first char
            if (!skip()) {
                throw new ParseException(msgUnexpectedEOF(), 0);
            }
            ir.mark(1);
            char c = (char)ir.read();
            if (!Character.isJavaIdentifierStart(c)) {
                ir.reset(); // not start of an identifier
                return null;
            }
        
            // parse remaining chars
            final StringBuffer sb = new StringBuffer();
            do {
                sb.append(c); 
                int i;
                ir.mark(1);
                if ((i = ir.read()) < 0)
                    break;
                c = (char)i;
            } while (Character.isJavaIdentifierPart(c));
            ir.reset(); // not part of an identifier
            return sb.toString();
        }
    
        /**
         * Scans for a number literal.
         * @return <code>null</code> if the next token is not a number
         */
        protected String scanNumberLiteral()
            throws IOException, ParseException {
            // parse stored token if any
            String t;
            if ((t = getLookAhead()) != null) {
                if (!(t.charAt(0) == '-' || Character.isDigit(t.charAt(0)))) {
                    setLookAhead(t); // not a number literal
                    return null;
                }
                return t;
            }

            // parse first char
            if (!skip()) {
                throw new ParseException(msgUnexpectedEOF(), 0);
            }
            ir.mark(1);
            char c = (char)ir.read();
            if (Character.isDigit(c)) {
            } else if (c == '-') {
                skip();
            } else {
                ir.reset(); // not start of a number
                return null;
            }
        
            // parse remaining chars
            final StringBuffer sb = new StringBuffer();
            do {
                sb.append(c); 
                int i;
                ir.mark(1);
                if ((i = ir.read()) < 0)
                    break;
                c = (char)i;
            } while (Character.isLetterOrDigit(c) || c == '-' || c == '.');
            ir.reset(); // not part of a number
            return sb.toString();
        }
    
        /**
         * Scans for a character literal.
         * @return <code>null</code> if the next token is not a character
         */
        protected String scanCharacterLiteral()
            throws IOException, ParseException {
            // parse stored token if any
            String t;
            if ((t = getLookAhead()) != null) {
                if (t.charAt(0) != '\'') {
                    setLookAhead(t); // not a char literal
                    return null;
                }
                return t;
            }

            // parse first char
            if (!skip()) {
                throw new ParseException(msgUnexpectedEOF(), 0);
            }
            ir.mark(1);
            char c = (char)ir.read();
            if (c != '\'') {
                ir.reset(); // not start of a char literal
                return null;
            }
                    
            // parse remaining two chars
            final StringBuffer sb = new StringBuffer();
            for (int j = 0; j < 2; j++) {
                sb.append(c);
                int i;
                if ((i = ir.read()) < 0) {
                    throw new ParseException(msgUnexpectedEOF(), 0);
                }
                c = (char)i;
            }
            if (c != '\'') {
                throw new ParseException(msgUnexpectedToken(String.valueOf(c)),
                                         0);
            }
            sb.append(c); // keep '\'' part of a char literal
            return sb.toString();
        }
    
        /**
         * Scans for a string literal.
         * @return <code>null</code> if the next token is not a string
         */
        protected String scanStringLiteral()
            throws IOException, ParseException {
            // parse stored token if any
            String t;
            if ((t = getLookAhead()) != null) {
                if (t.charAt(0) != '\"') {
                    setLookAhead(t); // not a string literal
                    return null;
                }
                return t;
            }

            // parse first char
            if (!skip()) {
                throw new ParseException(msgUnexpectedEOF(), 0);
            }
            ir.mark(1);
            char c = (char)ir.read();
            if (c != '\"') {
                ir.reset(); // not start of a string literal
                return null;
            }
        
            // parse remaining chars
            final StringBuffer sb = new StringBuffer();
            do {
                sb.append(c);
                int i;
                if ((i = ir.read()) < 0) {
                    throw new ParseException(msgUnexpectedEOF(), 0);
                }
                c = (char)i;
            } while (c != '\"'); // not supported: nested '\"' char sequences
            sb.append(c); // keep '\"' part of a string literal
            return sb.toString();
        }
    
        /**
         * Scans for an array literal. 
         * Limitation: only the empty array "{}" can be scanned.
         * @return <code>null</code> if the next token is not an array
         */
        protected String scanArrayLiteral()
            throws IOException, ParseException {
            // parse stored token if any
            String t;
            if ((t = getLookAhead()) != null) {
                if (t.charAt(0) != '{' || t.charAt(1) != '}') {
                    setLookAhead(t); // not an array literal
                    return null;
                }
                return t;
            }

            // parse first char
            if (!skip()) {
                throw new ParseException(msgUnexpectedEOF(), 0);
            }
            ir.mark(1);
            char c = (char)ir.read();
            if (c != '{') {
                ir.reset(); // not start of an array literal
                return null;
            }
            c = (char)ir.read();
            if (c != '}') {
                ir.reset(); // not end of an array literal
                return null;
            }
        
            return "{}";
        }

        /**
         * Scans for an at-sign
         * @return <code>null</code> if the next token is not an at-sign
         */
        protected String scanAtSign()
            throws IOException, ParseException {
            // parse stored token if any
            String t;
            if ((t = getLookAhead()) != null) {
                if (t.charAt(0) != '@') {
                    setLookAhead(t); // not an at-sign
                    return null;
                }
                return t;
            }

            // parse first char
            if (!skip()) {
                throw new ParseException(msgUnexpectedEOF(), 0);
            }
            ir.mark(1);
            char c = (char)ir.read();
            if (c != '@') {
                ir.reset(); // not an at-sign
                return null;
            }

            return "@";
        }
    
        /**
         * Returns the next token to be parsed.
         * @return never <code>null</code>
         */
        protected String parseToken()
            throws IOException, ParseException {
            String t;
            if ((t = getLookAhead()) != null) {
            } else if ((t = scanIdentifier()) != null) {
            } else if ((t = scanNumberLiteral()) != null) {
            } else if ((t = scanStringLiteral()) != null) {
            } else if ((t = scanCharacterLiteral()) != null) {
            } else if ((t = scanAtSign()) != null) {
            } else {
                setLookAhead(t); // not an identifier, number, or string
                // next non-white char
                if (!skip()) {
                    throw new ParseException(msgUnexpectedEOF(), 0);
                }
                t = String.valueOf((char)ir.read());
            }
            //log.println("parseToken() : '" + t + "'");
            return t;
        }

        /**
         * Parses the next token and validates it against an expected one.
         * @return never <code>null</code>
         */
        protected String demandToken(String token)
            throws IOException, ParseException {
            final String t = parseToken();
            if (!t.equals(token)) {
                throw new ParseException(msgUnexpectedToken(t), 0);
            }
            return t;
        }
    
        /**
         * Parses a literal.
         * @return <code>null</code> if the next token is not a literal
         */
        protected String parseLiteral()
            throws IOException, ParseException {
            String t;
            if ((t = scanNumberLiteral()) != null) {
            } else if ((t = scanStringLiteral()) != null) {
            } else if ((t = scanCharacterLiteral()) != null) {
            } else if ((t = scanArrayLiteral()) != null) {
            }
            //log.println("parseLiteral() : '" + t + "'");
            return t;
        }

        /**
         * Parses the next token and validates that it is a literal.
         * @return never <code>null</code>
         */
        protected String demandLiteral()
            throws IOException, ParseException {
            final String l = parseLiteral();
            if (l == null) {
                throw new ParseException(msgUnexpectedToken(parseToken()), 0);
            }
            return l;
        }
    
        /**
         * Parses the next token and validates that it is a constant,
         * which is either a literal or a static member.
         * @return the description of the field
         */
        protected String demandConstant()
            throws IOException, ParseException {
            final String literal = parseLiteral();
            if (literal != null) {
                return literal;
            }
            final String field = demandIdentifier();
            if (field == null) {
                throw new ParseException(msgUnexpectedToken(parseToken()), 0);
            }
            return field;
        }

        /**
         * Parses an annotation which is an at-sign followed by a fully qualified interface name.
         * @return <code>null</code> if the next token is not an annotation, otherwise return 
         * the annotation type name without the at-sign.
         */
        protected String parseAnnotation()
            throws IOException, ParseException {
            String t;
            // parse stored token if any
            if ((t = getLookAhead()) != null) {
                if (t.charAt(0) != '@') {
                    setLookAhead(t); // not a annotation
                    return null;
                }
            } else if ((t = scanAtSign()) != null) {
            } else {
                return null;
            }

            final String identifier = demandIdentifier();
            if (identifier == null) {
                throw new ParseException(msgUnexpectedToken(parseToken()), 0);
            }

            return identifier;
        }

        /**
         * Parses the default value specification of an annotation element and validates 
         * that it is an element value, which is either an annotation or a constant.
         * @return never <code>null</code>
         */
        protected String demandElementValue()
            throws IOException, ParseException { 
            final String annotation = parseAnnotation();
            if (annotation != null) {
                return annotation;
            }
            final String constant = demandConstant();
            if (constant == null) {
                throw new ParseException(msgUnexpectedToken(parseToken()), 0);
            }
            return constant;
        }
    
        /**
         * Parses any available Java modifiers.
         * @return an int value with the parsed modifiers' bit set
         */
        protected int parseModifiers()
            throws IOException, ParseException {
            int m = 0;
            while (true) {
                // parse known modifiers
                final String t = parseToken();
                if      (t.equals("abstract")) m |= Modifier.ABSTRACT;
                else if (t.equals("annotation")) m |= 
                        (ANNOTATION + Modifier.ABSTRACT + Modifier.INTERFACE);
                else if (t.equals("enum")) m |= ENUM;
                else if (t.equals("final")) m |= Modifier.FINAL;
                else if (t.equals("interface")) m |= Modifier.INTERFACE;
                else if (t.equals("native")) m |= Modifier.NATIVE;
                else if (t.equals("private")) m |= Modifier.PRIVATE;
                else if (t.equals("protected")) m |= Modifier.PROTECTED;
                else if (t.equals("public")) m |= Modifier.PUBLIC;
                else if (t.equals("static")) m |= Modifier.STATIC;
                else if (t.equals("strictfp")) m |= Modifier.STRICT;
                else if (t.equals("synchronized")) m |= Modifier.SYNCHRONIZED;
                else if (t.equals("transient")) m |= Modifier.TRANSIENT;
                else if (t.equals("varargs")) m |= Modifier.TRANSIENT;
                else if (t.equals("volatile")) m |= Modifier.VOLATILE;
                else {
                    setLookAhead(t); // not a modifier
                    break;
                }
            }
            //log.println("parseModifiers() : '" + Modifier.toString(m) + "'");
            return m;
        }
    
        /**
         * Parses a (qualified) identifier.
         * @return <code>null</code> if the next token is not an identifier
         */
        protected String parseIdentifier()
            throws IOException, ParseException {
            String t = scanIdentifier();
            if (t != null) {
                // parse dot-connected identifiers
                final StringBuffer id = new StringBuffer(t);
                String tt = parseToken();
                while (tt.equals(".")) {
                    id.append(".");
                    tt = parseIdentifier();
                    if (tt == null) {
                        throw new ParseException(msgUnexpectedToken(tt), 0);
                    }
                    id.append(tt);
                    tt = parseToken();
                }
                setLookAhead(tt); // not a dot token
                t = id.toString();
            }
            //log.println("parseIdentifier() : '" + t + "'");
            return t;
        }

        /**
         * Parses the next token(s) and validates that it is an identifier.
         * @return never <code>null</code>
         */
        protected String demandIdentifier()
            throws IOException, ParseException {
            final String id = parseIdentifier();
            if (id == null) {
                throw new ParseException(msgUnexpectedToken(parseToken()), 0);
            }
            return id;
        }
    
        /**
         * Parses a comma-separated list of identifiers.
         * @return never <code>null</code>
         */
        protected String[] demandIdentifierList()
            throws IOException, ParseException {
            final ArrayList ids = new ArrayList();
            ids.add(demandIdentifier());
            String t;
            while ((t = parseToken()).equals(",")) {
                ids.add(demandIdentifier());
            }
            setLookAhead(t); // not an identifier
            return (String[])ids.toArray(new String[ids.size()]);
        }
    
        /**
         * Parses a type expression.
         * @return <code>null</code> if the next token is not a type
         */
        protected String parseType()
            throws IOException, ParseException {
            String t = parseIdentifier();
            if (t != null) {
                // parse array dimensions
                final StringBuffer type = new StringBuffer(t);
                while ((t = parseToken()).equals("[")) {
                    demandToken("]");
                    type.append("[]");
                }
                setLookAhead(t); // not an open bracket token
                t = type.toString();
            }
            //log.println("parseType() : '" + t + "'");
            return t;
        }
    
        /**
         * Parses the next token and validates that it is a type expression.
         * @return never <code>null</code>
         */
        protected String demandType()
            throws IOException, ParseException {
            final String id = parseType();
            if (id == null) {
                throw new ParseException(msgUnexpectedToken(parseToken()), 0);
            }
            return id;
        }
    
        /**
         * Parses a comma-separated parameter list.
         * @return never <code>null</code>
         */
        protected String[] parseParameterList()
            throws IOException, ParseException {
            final ArrayList types = new ArrayList();
            String t = parseType();
            if (t != null) {
                types.add(t);
                parseIdentifier(); // optional parameter name
                while ((t = parseToken()).equals(",")) {
                    types.add(demandType());
                    parseIdentifier(); // optional parameter name
                }
                setLookAhead(t); // not a comma token
            }
            return (String[])types.toArray(new String[types.size()]);
        }
    
        /**
         * Parses a class member declaration and provides the information
         * to a field, constructor, or method handler.
         * @return <code>null</code> if there's no member declaration
         */
        protected String parseMember()
            throws IOException, ParseException {
            // parse optional modifiers, type, and member name
            final int mods = parseModifiers();
            final String typeOrName = parseType();
            if (typeOrName == null) {
                if (mods != 0) {
                    throw new ParseException(msgUnexpectedEOF(), 0);
                }
                return null; // no member to parse
            }
            final String memberName = parseIdentifier(); // null if constructor
            
            // parse optional field value or parameter+exception list
            final String value;
            final String[] params;
            final String[] excepts;
            {
                final String tvp = parseToken();
                if (tvp.equals(";")) {
                    value = null;
                    params = null;
                    excepts = null;
                } else if (tvp.equals("=")) {
                    // parse field value
                    value = demandLiteral();
                    demandToken(";");
                    params = null;
                    excepts = null;
                } else if (tvp.equals("(")) {
                    // parse optional parameter and exception list
                    params = parseParameterList();
                    demandToken(")");
                    final String tt = parseToken();
                    if (tt.equals("throws")) {
                        excepts = demandIdentifierList();
                        demandToken(";");
                        value = null;
                    } else if (tt.equals(";")) {
                        excepts = new String[]{};
                        value = null;
                    } else if (tt.equals("default")) {
                        value = demandElementValue();
                        demandToken(";");
                        excepts = new String[]{};
                    } else {
                        throw new ParseException(msgUnexpectedToken(tt), 0);
                    }
                } else {
                    throw new ParseException(msgUnexpectedToken(tvp), 0);
                }
            }
        
            // verify field, constructor, or method
            String name = memberName;
            if (params == null) {
                checkField(mods, typeOrName, memberName, value);
            } else {
                if (memberName == null) {
                    name = typeOrName;
                    checkConstructor(mods, params, excepts);
                } else {
                    checkMethod(mods, typeOrName, memberName, params, excepts, value);
                }
            }

            //log.println("parseMember() : " + name);
            return name;
        }

        /**
         * Parses a class definition and provides the information
         * to a handler.
         * @return <code>null</code> if there's no class definition
         */
        @SuppressWarnings("empty-statement")
        protected String parseClass()
            throws IOException, ParseException {
            // parse optional modifiers, class token, and class name
            if (!skip()) {
                return null; // eof, no class to parse
            }
            final int mods = parseModifiers();
            final String tc = parseToken();
            if (!tc.equals("class")) { // token 'interface' parsed as modifier
                setLookAhead(tc);
            }
            final String name = demandIdentifier();
        
            // parse optional extends and implements clauses
            final String[] ext;
            final String[] impl;
            {
                String tei = parseToken();
                if (tei.equals("extends")) {
                    ext = demandIdentifierList();
                    tei = parseToken();
                } else {
                    ext = new String[]{};
                }
                if (((mods & Modifier.INTERFACE) == 0)
                    && tei.equals("implements")) {
                    impl = demandIdentifierList();
                    tei = parseToken();
                } else {
                    impl = new String[]{};
                }
                if (!tei.equals("{")) {
                    throw new ParseException(msgUnexpectedToken(tei), 0);
                }
            }
        
            // verify class header
            checkClass(mods, name, ext, impl);

            // process members; empty-statement intentional
            while (parseMember() != null);
            demandToken("}");

            // verify class
            postCheckClass();

            //log.println("parseClass() : " + name);
            return name;
        }

        /**
         * Parses a list of signature descriptor files and processes
         * the class definitions.
         * @param descrFileNames list of signature descriptor file names
         */
        @SuppressWarnings("empty-statement")
        public void parse(List descrFileNames)
            throws IOException, ParseException {        
            for (Iterator i = descrFileNames.iterator(); i.hasNext();) {
                final String fileName = (String)i.next();
                logInfo("");
                logInfo("parsing descriptor file: " + fileName);
                try {
                    descriptorFile = new File(fileName);
                    ir = new LineNumberReader(new FileReader(descriptorFile));
                    ir.setLineNumber(1);
                    setLookAhead(null);
                    // empty-statement intentional
                    while (parseClass() != null);
                } finally {
                    descriptorFile = null;
                    if (ir != null) {
                        ir.close();
                    }
                    setLookAhead(null);
                }
            }
        }
    }
    
    // ----------------------------------------------------------------------
    // Stand-Alone Command-Line Interface
    // ----------------------------------------------------------------------

    /** A writer for standard output. */
    static protected PrintWriter out = new PrintWriter(System.out, true);

    /** Command line arguments */
    static public final List descrFileNames = new ArrayList();

    /** Command line option 'quiet'.*/
    static private boolean optionQuiet;

    /** Command line option 'verbose'.*/
    static private boolean optionVerbose;

    /** Prints the CLI usage. */
    static public void printUsage() {
        out.println();
        out.println("usage: SignatureVerifier [options] arguments");
        out.println("options:");
        out.println("  [-h|--help               print usage and exit]");
        out.println("  [-q|--quiet              only print error messages]");
        out.println("  [-v|--verbose            print verbose messages]");
        out.println("arguments:");
        out.println("  <signature descriptor file>...");
        out.println();
    }

    /** Parses command line arguments. */
    static public int parseArgs(String args[]) {
        out.println("parse main() arguments");

        // parse this class' options and arguments
        for (int i = 0; i < args.length; i++) {
            if (args[i] == null) {
                continue;
            }
            if (args[i].equalsIgnoreCase("-h")
                || args[i].equalsIgnoreCase("--help")) {
                return -1;
            }
            if (args[i].equalsIgnoreCase("-v")
                || args[i].equalsIgnoreCase("--verbose")) {
                optionVerbose = true;
                continue;
            }
            if (args[i].equalsIgnoreCase("-q")
                || args[i].equalsIgnoreCase("--quiet")) {
                optionQuiet = true;
                continue;
            }
            if (args[i].startsWith("-")) {
                out.println("Usage Error: unknown option " + args[i]);
                return -1;
            }
            // collect argument
            descrFileNames.add(args[i]);
        }

        // print args
        if (false) {
            out.println("descrFileNames = {");
            for (Iterator i = descrFileNames.iterator(); i.hasNext();) {
                out.println("                     " + i.next());
            }
            out.println("                 }");
            out.println("optionQuiet    = " + optionQuiet);
            out.println("optionVerbose  = " + optionVerbose);
        }

        // check args
        if (descrFileNames.isEmpty()) {
            out.println("Usage Error: Missing argument "
                        + "<signature descriptor file>...");
            return -1;
        }
        return 0;
    }

    /** Runs the signature test and exits with a status code. */
    static public void main(String[] args) {
        out.println("run SignatureVerifier ...");
        if (parseArgs(args) != 0) {
            printUsage();
            out.println("abort.");
            System.exit(-1);
        }

        int status = 0;
        try {
            final SignatureVerifier s
                = new SignatureVerifier(out, optionQuiet, optionVerbose);
            status = s.test(descrFileNames);
        } catch (IOException ex) {
            out.println("ERROR: exception caught: " + ex);
            //ex.printStackTrace();
            out.println("abort.");
            System.exit(-2);
        } catch (ParseException ex) {
            out.println("ERROR: exception caught: " + ex);
            //ex.printStackTrace();
            out.println("abort.");
            System.exit(-3);
        }

        out.println();
        out.println("done.");
        System.exit(status);
    }
}
