/*
 * 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.
 *
 * $Header:$
 */
package org.apache.beehive.netui.compiler;

import org.apache.beehive.netui.compiler.genmodel.GenStrutsApp;
import org.apache.beehive.netui.compiler.grammar.ActionGrammar;
import org.apache.beehive.netui.compiler.grammar.ExceptionHandlerGrammar;
import org.apache.beehive.netui.compiler.grammar.WebappPathType;
import org.apache.beehive.netui.compiler.typesystem.declaration.AnnotationInstance;
import org.apache.beehive.netui.compiler.typesystem.declaration.AnnotationValue;
import org.apache.beehive.netui.compiler.typesystem.declaration.ClassDeclaration;
import org.apache.beehive.netui.compiler.typesystem.declaration.FieldDeclaration;
import org.apache.beehive.netui.compiler.typesystem.declaration.MethodDeclaration;
import org.apache.beehive.netui.compiler.typesystem.declaration.Modifier;
import org.apache.beehive.netui.compiler.typesystem.declaration.PackageDeclaration;
import org.apache.beehive.netui.compiler.typesystem.declaration.ParameterDeclaration;
import org.apache.beehive.netui.compiler.typesystem.declaration.TypeDeclaration;
import org.apache.beehive.netui.compiler.typesystem.env.CoreAnnotationProcessorEnv;
import org.apache.beehive.netui.compiler.typesystem.type.ClassType;
import org.apache.beehive.netui.compiler.typesystem.type.TypeInstance;
import org.apache.beehive.netui.compiler.processor.SilentDiagnostics;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;


public abstract class FlowControllerChecker
        extends BaseChecker
        implements JpfLanguageConstants
{
    private AnnotationGrammar _controllerGrammar;
    private AnnotationGrammar _actionGrammar;
    private AnnotationGrammar _exceptionHandlerGrammar;
    private AnnotationGrammar _actionGrammarSilentDiagnostics;
    private AnnotationGrammar _exceptionHandlerGrammarSilentDiagnostics;
    private FormBeanChecker _formBeanChecker;
    private Map _checkResultMap;
    
    protected FlowControllerChecker( CoreAnnotationProcessorEnv env, FlowControllerInfo fcInfo, Diagnostics diags )
    {
        super( env, fcInfo, diags );
    }
    
    protected void doAdditionalClassChecks( ClassDeclaration jpfClass )
    {
    }
    
    protected Map getCheckResultMap()
    {
        return _checkResultMap;
    }
    
    protected abstract String getDesiredBaseClass( ClassDeclaration jclass );
    
    protected abstract AnnotationGrammar getControllerGrammar();
    
    public Map onCheck( ClassDeclaration jclass )
            throws FatalCompileTimeException
    {
        FlowControllerInfo fcInfo = getFCSourceFileInfo();
        
        _checkResultMap = new HashMap();
        _controllerGrammar = getControllerGrammar();
        _actionGrammar = new ActionGrammar( getEnv(), getDiagnostics(), getRuntimeVersionChecker(), fcInfo );
        _exceptionHandlerGrammar =
                new ExceptionHandlerGrammar( getEnv(), getDiagnostics(), getRuntimeVersionChecker(), fcInfo );
        _formBeanChecker = new FormBeanChecker( getEnv(), getDiagnostics() );
        
        SilentDiagnostics silentDiagnostics = new SilentDiagnostics();
        _actionGrammarSilentDiagnostics = new ActionGrammar( getEnv(), silentDiagnostics, getRuntimeVersionChecker(), fcInfo );
        _exceptionHandlerGrammarSilentDiagnostics =
                new ExceptionHandlerGrammar( getEnv(), silentDiagnostics, getRuntimeVersionChecker(), fcInfo );
        
        fcInfo.startBuild( getEnv(), jclass );
        
        try
        {
            return onCheckInternal( jclass );
        }
        finally
        {
            fcInfo.endBuild();
        }
    }
    
    private Map onCheckInternal( ClassDeclaration jclass )
            throws FatalCompileTimeException
    {
        FlowControllerInfo fcInfo = getFCSourceFileInfo();
        
        //
        // Check the base class.
        //
        String desiredBaseClass = getDesiredBaseClass( jclass );
        if ( desiredBaseClass != null && ! CompilerUtils.isAssignableFrom( desiredBaseClass, jclass, getEnv() ) )
        {
            getDiagnostics().addError( jclass, "error.does-not-extend-base", desiredBaseClass );
        }
        
        //
        // Check the annotations on the class.
        //
        startCheckClass( jclass );
        
        //
        // Check the fields.  Note that we're checking public and protected inherited fields, too.
        //
        Collection fields = CompilerUtils.getClassFields( jclass );
        
        for ( Iterator ii = fields.iterator(); ii.hasNext(); )  
        {
            FieldDeclaration field = ( FieldDeclaration ) ii.next();
            checkField( field, jclass );
        }

        //
        // Check the methods.  Note that we're checking public and protected inherited methods, too.
        //
        MethodDeclaration[] methods = CompilerUtils.getClassMethods( jclass, null );
        
        for ( int i = 0; i < methods.length; i++ )
        {
            MethodDeclaration method = methods[i];
            TypeDeclaration declaringType = method.getDeclaringType();
            
            //
            // Only add diagnostics if the method is in this class, or if it's inherited from a class that's *not* on
            // sourcepath (i.e., its SourcePosition is null).
            //
            if ( declaringType.equals( jclass ) || declaringType.getPosition() == null )
            {
                checkMethod( method, jclass, _actionGrammar, _exceptionHandlerGrammar );
            }
            else
            {
                //
                // We still want to run the checks, which aggregate information into the FlowControllerInfo.  We just
                // don't want diagnostics to be printed.
                //
                checkMethod( method, jclass, _actionGrammarSilentDiagnostics, _exceptionHandlerGrammarSilentDiagnostics );
            }
        }

        //
        // Check the inner classes.
        //
        Collection innerTypes = CompilerUtils.getClassNestedTypes( jclass );
        
        for ( Iterator ii = innerTypes.iterator(); ii.hasNext(); )  
        {
            TypeDeclaration innerType = ( TypeDeclaration ) ii.next();
            if ( innerType instanceof ClassDeclaration ) checkInnerClass( ( ClassDeclaration ) innerType );
        }
        
        //
        // Run additional .jpf- or .app-specific checks.
        //
        doAdditionalClassChecks( jclass );
        
        //
        // Runtime performance enhancement: enable saving of previous-page and previous-action information based on
        // whether there were Forwards that contained navigateTo attributes.
        //
        enableNavigateTo( jclass, fcInfo.getMergedControllerAnnotation(), fcInfo );
        Map sharedFlowTypes = fcInfo.getSharedFlowTypes();
        
        if ( sharedFlowTypes != null )
        {
            for ( Iterator ii = sharedFlowTypes.values().iterator(); ii.hasNext(); )  
            {
                TypeDeclaration sharedFlowType = ( TypeDeclaration ) ii.next();
                //
                // Saving of previous-page/previous-action info must be enabled if any of the referenced shared flows
                // use this feature.
                //
                enableNavigateTo( sharedFlowType, new MergedControllerAnnotation( sharedFlowType ), fcInfo );
            }
        }
        
        endCheckClass( jclass );
        _checkResultMap.put( JpfLanguageConstants.ExtraInfoKeys.flowControllerInfo, fcInfo );
        return _checkResultMap;
    }
    
    private static void enableNavigateTo( TypeDeclaration flowControllerClass, MergedControllerAnnotation controllerAnn,
                                          FlowControllerInfo fcInfo )
    {
        //
        // Look through Forwards and SimpleActions in the Controller annotation.
        //
        enableNavigateTo( controllerAnn.getForwards(), fcInfo );
        enableNavigateTo( controllerAnn.getSimpleActions(), fcInfo );
        
        //
        // Look through Forwards on Action and ExceptionHandler methods.
        //
        MethodDeclaration[] methods = CompilerUtils.getClassMethods( flowControllerClass, null );
        
        for ( int i = 0; i < methods.length; i++ )
        {
            MethodDeclaration method = methods[i];
            AnnotationInstance ann = CompilerUtils.getAnnotation( method, ACTION_TAG_NAME );
            
            if ( ann != null )
            {
                enableNavigateTo( CompilerUtils.getAnnotation( ann, VALIDATION_ERROR_FORWARD_ATTR, true ), fcInfo );
            }
            
            if ( ann == null ) ann = CompilerUtils.getAnnotation( method, EXCEPTION_HANDLER_TAG_NAME );
            if ( ann != null ) enableNavigateTo( CompilerUtils.getAnnotationArray( ann, FORWARDS_ATTR, true ), fcInfo );
        }
    }
    
    private static void enableNavigateTo( Collection childAnnotations, FlowControllerInfo fcInfo )
    {
        if ( childAnnotations != null )
        {
            for ( Iterator ii = childAnnotations.iterator(); ii.hasNext(); )  
            {
                AnnotationInstance childAnnotation = ( AnnotationInstance ) ii.next();
                enableNavigateTo( childAnnotation, fcInfo );
            }
        }
    }
    
    private static void enableNavigateTo( AnnotationInstance ann, FlowControllerInfo fcInfo )
    {
        if ( ann == null ) return;
        String val = CompilerUtils.getEnumFieldName( ann, NAVIGATE_TO_ATTR, true );
        
        if ( val != null )
        {
            if ( val.equals( NAVIGATE_TO_CURRENT_PAGE_STR ) || val.equals( NAVIGATE_TO_PREVIOUS_PAGE_STR )
                 || val.equals( NAVIGATE_TO_PAGE_LEGACY_STR ) )
            {
                fcInfo.enableNavigateToPage();
            }
            else if ( val.equals( NAVIGATE_TO_PREVIOUS_ACTION_STR ) )
            {
                fcInfo.enableNavigateToAction();
            }
        }
    }
    
    protected void endCheckClass( ClassDeclaration jclass )
    {
    }
    
    protected abstract GenStrutsApp createStrutsApp( ClassDeclaration jclass )
        throws IOException, FatalCompileTimeException;
    
    protected void startCheckClass( ClassDeclaration jclass )
            throws FatalCompileTimeException
    {
        //
        // Check for basic things like writability of the struts-config file.
        //
        GenStrutsApp strutsApp = null;
        File strutsConfigFile = null;
        
        //
        // Make sure we can write to the struts-config XML file.
        //
        try
        {
            strutsApp = createStrutsApp( jclass );
            strutsConfigFile = strutsApp.getStrutsConfigFile();
        }
        catch ( IOException e )
        {
            // will be reported at generate time
        }
        
        if ( strutsConfigFile != null )
        {
            getFCSourceFileInfo().addReferencedFile( strutsConfigFile );
            
            if ( strutsConfigFile.exists() && strutsApp != null && ! strutsApp.canWrite() )
            {
                getDiagnostics().addError( jclass, "error.struts-config-not-writable", strutsConfigFile );
            }
        }
        
        getRuntimeVersionChecker().checkRuntimeVersion( VERSION_8_SP2_STRING, jclass, getDiagnostics(),
                                                        "warning.runtime-version", new Object[]{ PAGEFLOW_RUNTIME_JAR } );

        //
        // Check the Jpf.Controller annotation on this class.
        //
        AnnotationInstance controllerAnnotation = CompilerUtils.getAnnotation( jclass, CONTROLLER_TAG_NAME );
        if ( controllerAnnotation != null ) _controllerGrammar.check( controllerAnnotation, null, jclass );
        
        //
        // Check relative paths on Jpf.Catch, Jpf.Forward, and Jpf.SimpleAction annotations on superclasses.
        // If inheritLocalPaths is set to true on @Jpf.Controller, then we don't need to do this check, since
        // inherited paths will always resolve.
        //
        if ( ! getFCSourceFileInfo().getMergedControllerAnnotation().isInheritLocalPaths() )
        {
            checkInheritedRelativePaths( jclass );
        }

        // check for duplicate expressions in conditional forwards
        checkConditionalForwardsExpressions(jclass);
    }

    /**
     * Check relative paths in annotations inherited from a base class.
     */ 
    private void checkInheritedRelativePaths( ClassDeclaration jclass )
            throws FatalCompileTimeException
    {
        for ( ClassType type = jclass.getSuperclass();
              type != null && CompilerUtils.isAssignableFrom( FLOWCONTROLLER_BASE_CLASS, type, getEnv() );
              type = type.getSuperclass() )
        {
            TypeDeclaration decl = CompilerUtils.getDeclaration( type );
            
            //
            // Check simple actions in the Controller annotation.
            //
            List simpleActions =
                    CompilerUtils.getAnnotationArrayValue( decl, CONTROLLER_TAG_NAME, SIMPLE_ACTIONS_ATTR, true );
            
            if ( simpleActions != null )
            {
                for ( Iterator j = simpleActions.iterator(); j.hasNext(); )  
                {
                    AnnotationInstance i = ( AnnotationInstance ) j.next();
                    checkRelativePath( i, PATH_ATTR, jclass, decl, false );
                    List conditionalForwards = CompilerUtils.getAnnotationArray( i, CONDITIONAL_FORWARDS_ATTR, true );
                    
                    if ( conditionalForwards != null )
                    {
                        for ( Iterator k = conditionalForwards.iterator(); k.hasNext(); )  
                        {
                            AnnotationInstance ann = ( AnnotationInstance ) k.next();
                            checkRelativePath( ann, PATH_ATTR, jclass, decl, false );
                        }
                    }
                }
            }
            
            //
            // Check Forwards in the Controller annotation.
            //
            List forwards = CompilerUtils.getAnnotationArrayValue( decl, CONTROLLER_TAG_NAME, FORWARDS_ATTR, true );
            
            if ( forwards != null )
            {
                for ( Iterator ii = forwards.iterator(); ii.hasNext(); )  
                {
                    AnnotationInstance i = ( AnnotationInstance ) ii.next();
                    checkRelativePath( i, PATH_ATTR, jclass, decl, false );
                }
            }
            
            //
            // Check Catches in the Controller annotation.
            //
            List catches = CompilerUtils.getAnnotationArrayValue( decl, CONTROLLER_TAG_NAME, CATCHES_ATTR, true );
            
            if ( catches != null )
            {
                for ( Iterator j = catches.iterator(); j.hasNext(); )  
                {
                    AnnotationInstance i = ( AnnotationInstance ) j.next();
                    checkRelativePath( i, PATH_ATTR, jclass, decl, false );
                }
            }

            //
            // Check strutsMerge and validatorMerge in the Controller annotation.
            //
            AnnotationInstance controllerAnnotation = CompilerUtils.getAnnotation( decl, CONTROLLER_TAG_NAME );

            if ( controllerAnnotation != null )
            {
                checkRelativePath( controllerAnnotation, VALIDATOR_MERGE_ATTR, jclass, decl, true );
                checkRelativePath( controllerAnnotation, STRUTSMERGE_ATTR, jclass, decl, true );
            }
            
            //
            // Check Forwards and Catches on action methods and exception-handler methods.
            //
            MethodDeclaration[] methods = decl.getMethods();
            for ( int i = 0; i < methods.length; i++ )
            {
                MethodDeclaration method = methods[i];
                AnnotationInstance ann = CompilerUtils.getAnnotation( method, ACTION_TAG_NAME);
                if ( ann == null ) ann = CompilerUtils.getAnnotation( method, EXCEPTION_HANDLER_TAG_NAME );
                
                if ( ann != null )
                {
                    List methodForwards = CompilerUtils.getAnnotationArray( ann, FORWARDS_ATTR, true );
                    String methodName = method.getSimpleName();
                    
                    if ( methodForwards != null )
                    {
                        for ( Iterator j = methodForwards.iterator(); j.hasNext(); )
                        {
                            AnnotationInstance methodForward = ( AnnotationInstance ) j.next();
                            checkRelativePath( methodName, methodForward, PATH_ATTR, jclass, decl, false );
                        }
                    }
                    
                    List methodCatches = CompilerUtils.getAnnotationArray( ann, CATCHES_ATTR, true );
                    
                    if ( methodCatches != null )
                    {
                        for ( Iterator j = methodCatches.iterator(); j.hasNext(); )
                        {
                            AnnotationInstance methodCatch = ( AnnotationInstance ) j.next();
                            checkRelativePath( methodName, methodCatch, PATH_ATTR, jclass, decl, false );
                        }
                    }
                }
            }
        }
    }
    
    private void checkRelativePath( AnnotationInstance ann, String memberName, TypeDeclaration jclass,
                                    TypeDeclaration baseType, boolean isError )
            throws FatalCompileTimeException
    {
        if ( ann != null )
        {
            AnnotationValue pathVal = CompilerUtils.getAnnotationValue( ann, memberName, true );
            
            if ( pathVal != null )
            {
                String path = ( String ) pathVal.getValue();
                
                if ( path.length() > 0 && path.charAt( 0 ) != '/' && ! WebappPathType.relativePathExists( path, jclass, getEnv() ) )
                {
                    String[] args = {
                        path,
                        ANNOTATION_INTERFACE_PREFIX + ann.getAnnotationType().getDeclaration().getSimpleName(),
                        baseType.getQualifiedName()
                    };
                    
                    if ( isError )
                    {
                        getDiagnostics().addErrorArrayArgs( ann, "message.inherited-file-not-found", args );
                    }
                    else
                    {
                        getDiagnostics().addWarningArrayArgs( ann, "message.inherited-file-not-found", args );
                    }
                }
            }
        }
    }
    
    private void checkRelativePath( String methodName, AnnotationInstance ann, String memberName,
                                    TypeDeclaration jclass, TypeDeclaration baseType, boolean isError )
            throws FatalCompileTimeException
    {
        if ( ann != null )
        {
            AnnotationValue pathVal = CompilerUtils.getAnnotationValue( ann, memberName, true );
            
            if ( pathVal != null )
            {
                String path = ( String ) pathVal.getValue();
                
                if ( path.length() > 0 && path.charAt( 0 ) != '/' && ! WebappPathType.relativePathExists( path, jclass, getEnv() ) )
                {
                    String[] args = {
                        path,
                        ANNOTATION_INTERFACE_PREFIX + ann.getAnnotationType().getDeclaration().getSimpleName(),
                        methodName,
                        baseType.getQualifiedName()
                    };
                    
                    if ( isError )
                    {
                        getDiagnostics().addErrorArrayArgs( jclass, "message.method-inherited-file-not-found", args );
                    }
                    else
                    {
                        getDiagnostics().addWarningArrayArgs( jclass, "message.method-inherited-file-not-found", args );
                    }
                }
            }
        }
    }

    /**
     * Check for duplicate expressions in conditional forwards of the
     * simple action annotations.
     */
    private void checkConditionalForwardsExpressions(ClassDeclaration jclass)
            throws FatalCompileTimeException
    {
        // Get simple actions in the Controller annotation.
        List simpleActions =
                CompilerUtils.getAnnotationArrayValue(jclass, CONTROLLER_TAG_NAME, SIMPLE_ACTIONS_ATTR, true);

        if (simpleActions != null) {
            HashSet expressionsFound = new HashSet();
            for (Iterator j = simpleActions.iterator(); j.hasNext();) {
                AnnotationInstance sa = (AnnotationInstance) j.next();
                List conditionalForwards = CompilerUtils.getAnnotationArray(sa, CONDITIONAL_FORWARDS_ATTR, true);

                if (conditionalForwards != null) {
                    for (Iterator k = conditionalForwards.iterator(); k.hasNext();) {
                        AnnotationInstance cf = (AnnotationInstance) k.next();
                        String expression = CompilerUtils.getString(cf, CONDITION_ATTR, true);
                        assert expression != null;

                        if (expressionsFound.contains(expression)) {
                            String name = CompilerUtils.getString(sa, NAME_ATTR, true);
                            getDiagnostics().addWarning(cf, "warning.duplicate-conditional-forward-expression",expression, name);
                        }
                        else {
                            expressionsFound.add(expression);
                        }
                    }
                }
                expressionsFound.clear();
            }
        }
    }


    protected void checkField( FieldDeclaration field, TypeDeclaration jclass )
    {
        //
        // Only warn about nonserializable member data that's defined in this particular class.
        //
        if ( CompilerUtils.typesAreEqual( jclass, field.getDeclaringType() ) )
        {
            TypeInstance type = field.getType();
            
            if ( ! field.hasModifier( Modifier.TRANSIENT ) && ! field.hasModifier( Modifier.STATIC )
                 && type instanceof ClassType
                 && ! CompilerUtils.isAssignableFrom( SERIALIZABLE_CLASS_NAME, type, getEnv() ) )
            {
                getDiagnostics().addWarning( field, "warning.nonserializable-member-data" );
            }
        }
    }
    
    protected void checkMethod( MethodDeclaration method, ClassDeclaration jclass,
                                AnnotationGrammar actionGrammar, AnnotationGrammar exceptionHandlerGrammar )
            throws FatalCompileTimeException
    {
        AnnotationInstance[] annotations = method.getAnnotationInstances();
        
        for ( int i = 0; i < annotations.length; i++ )
        {
            AnnotationInstance annotation = annotations[i];
            String annotationName = CompilerUtils.getDeclaration( annotation.getAnnotationType() ).getSimpleName();
            
            if ( annotationName.equals( ACTION_TAG_NAME ) )
            {
                actionGrammar.check( annotation, null, method );

                if ( ! CompilerUtils.isAssignableFrom( FORWARD_CLASS_NAME, method.getReturnType(), getEnv() ) )
                {
                    getDiagnostics().addError( method, "error.method-wrong-return-type", FORWARD_CLASS_NAME );
                }
            }
            else if ( annotationName.equals( EXCEPTION_HANDLER_TAG_NAME ) )
            {
                exceptionHandlerGrammar.check( annotation, null, method );
                checkExceptionHandlerMethod( method );
            }
        }
    }

    /*
     * Run a check of form bean classes that might not otherwise
     * be processed. A @FormBean instance will get processed by the
     * PageFlowCoreAnnotationProcessor and FormBeanChecker. Otherwise,
     * run the class through the FormBeanChecker for other possible errors.
     */
    private void checkInnerClass(ClassDeclaration jclass)
            throws FatalCompileTimeException
    {
        boolean noFormBeanAnnotation = true;
        if (CompilerUtils.getAnnotation(jclass, FORM_BEAN_TAG_NAME, true) != null) {
            noFormBeanAnnotation = false;
        }
        else {
            //
            // check the methods for validatable property annotations that
            // would be processed elsewhere.
            //
            MethodDeclaration[] methods = CompilerUtils.getClassMethods(jclass, null);
            for (int i = 0; i < methods.length && noFormBeanAnnotation; i++) {
                MethodDeclaration method = methods[i];
                if (CompilerUtils.getAnnotation(method, VALIDATABLE_PROPERTY_TAG_NAME) != null) {
                    noFormBeanAnnotation = false;
                }
            }
        }

        if (noFormBeanAnnotation) {
            _formBeanChecker.check(jclass);
        }
    }

    private void checkExceptionHandlerMethod( MethodDeclaration method )
    {
        if ( ! CompilerUtils.isAssignableFrom( FORWARD_CLASS_NAME, method.getReturnType(), getEnv() ) )
        {
            getDiagnostics().addError( method, "error.method-wrong-return-type", FORWARD_CLASS_NAME );
        }

        ParameterDeclaration[] parameters = method.getParameters();
        
        if ( parameters.length == 4 )
        {
            if ( ! CompilerUtils.isAssignableFrom( THROWABLE_CLASS_NAME, parameters[0].getType(), getEnv() ) )
            {
                getDiagnostics().addError( method, "error.exception-method-wrong-exception-arg", THROWABLE_CLASS_NAME );
            }

            checkExceptionHandlerArgType( method, parameters, 1, STRING_CLASS_NAME );
            checkExceptionHandlerArgType( method, parameters, 2, STRING_CLASS_NAME );

            //
            // The use of org.apache.struts.action.ActionForm or org.apache.beehive.netui.pageflow.FormData as the
            // fourth argument is deprecated.  Forms can be any Object type now.
            //
            if ( CompilerUtils.isAssignableFrom( STRUTS_FORM_CLASS_NAME, parameters[3].getType(), getEnv() ) )
            {
                getDiagnostics().addWarning( method, "warning.exception-method-deprecated-form-arg" );
            }
            else
            {
                checkExceptionHandlerArgType( method, parameters, 3, OBJECT_CLASS_NAME );
            }
        }
        else
        {
            getDiagnostics().addError( method, "error.exception-method-wrong-arg-count", new Integer( 4 ) );
        }        
    }
    
    private void checkExceptionHandlerArgType( MethodDeclaration method, ParameterDeclaration[] parameters,
                                               int index, String className )
    {
        if ( ! CompilerUtils.isOfClass( parameters[ index ].getType(), className, getEnv() ) )
        {
            getDiagnostics().addError( method, "error.exception-method-wrong-arg-type", new Integer( index + 1 ),
                                       className );
        }
    }
    
    protected void checkForOverlappingClasses( ClassDeclaration jpfClass, String baseClass, String fileExtension,
                                               String errorKey )
    {
        File jpfFile = CompilerUtils.getSourceFile( jpfClass, true );
        File parentDir = jpfFile.getParentFile();
        PackageDeclaration pkg = jpfClass.getPackage();
        ClassDeclaration[] packageClasses = pkg.getClasses();
        Set overlapping = new HashSet();
        List overlappingFiles = new ArrayList();
        
        //
        // First go through the other classes in this package to look for other classes of this type.  Only one per
        // directory is allowed.
        //
        for ( int i = 0; i < packageClasses.length; i++ )
        {
            ClassDeclaration classDecl = packageClasses[i];
            if ( CompilerUtils.getAnnotation( classDecl, CONTROLLER_TAG_NAME ) != null
                 && CompilerUtils.isAssignableFrom( baseClass, classDecl, getEnv() ) )
            {
                File file = CompilerUtils.getSourceFile( classDecl, false );
                
                //
                // Add the dependency if it's a different file and if the file exists (it may have been deleted
                // sometime after the list of classes in this package got built.
                //
                if ( ! jpfFile.equals( file ) && file != null && file.exists() )
                {
                    overlapping.add( file.getName() );
                    overlappingFiles.add( file );
                }
            }
        }
        
        //
        // Additionally, we'll go through the parent directory to make sure there are no other files of this type. 
        // This is a double-check for the case where duplicate files have the same class names inside them, which means
        // that iterating through the list of package classes is hit or miss (only one of them will show up, and it may
        // be this class or the duplicate class).
        //
        File[] peers = parentDir.listFiles( new ExtensionFileFilter( fileExtension ) );
        
        if ( peers != null )    // make sure the directory hasn't been deleted while we're running
        {
            for ( int i = 0; i < peers.length; i++ )
            {
                File peer = peers[i];
                if ( ! peer.equals( jpfFile ) )
                {
                    String name = peer.getName();
                    
                    if ( ! overlapping.contains( name ) )
                    {
                        overlapping.add( name );
                        overlappingFiles.add( peer );
                    }
                }
            }
        }
        
        int len = overlapping.size();
        if ( len > 0 )
        {
            if ( len > 3 )
            {
                getDiagnostics().addErrorArrayArgs( jpfClass, errorKey, overlapping.toArray() );
            }
            else
            {
                getDiagnostics().addErrorArrayArgs( jpfClass, errorKey + len, overlapping.toArray() );
            }
        }
        
        getCheckResultMap().put( JpfLanguageConstants.ExtraInfoKeys.overlappingPageFlowFiles, overlappingFiles ); 
    }
    
    private static class ExtensionFileFilter implements FilenameFilter
    {
        private String _extension;
        
        public ExtensionFileFilter( String extension )
        {
            _extension = extension;
        }
        
        public boolean accept( File dir, String name )
        {
            return name.endsWith( _extension );
        }
    }

    protected FlowControllerInfo getFCSourceFileInfo()
    {
        return ( FlowControllerInfo ) super.getSourceFileInfo();
    }
}
