package org.apache.maven.j2ee;

/* ====================================================================
 *   Copyright 2001-2004 The Apache Software Foundation.
 *
 *   Licensed 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.
 * ====================================================================
 */

import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;

import org.apache.maven.j2ee.war.FormLoginConfig;

/**
 * A task to validate a war file. The following is checked:
 * <ol>
 *      <li>The war file exists</li>
 *      <li>The war file is readable</li>
 *      <li>The war file has a web.xml (warning)</li>
 *      <li>Servlets defined by a <code>&lt;servlet&gt;<code> tag are loadable
 *          from the war file and <strong>not</strong> the classpath</li>
 *      <li>JSPs defined by a <code>&lt;servlet&gt;<code> tag exist in the war
 *      </li>
 *      <li>Taglibs defined by a <code>&lt;taglib&gt;</code> have a <code>
 *          &lt;taglib-location&gt;</code> that exists in the war</li>
 *      <li>Error pages specified by a <code>&lt;location&gt;</code> nested 
 *          within an <code>&lt;error-page&gt;</code> element must exist in the
 *          war file</li>
 *      <li>Login and error pages specified in the <code>&lt;form-login-config
 *          &gt;</code> element must exist in the war file</li>
 * </ol>
 * @author  dIon Gillard
 * @version $Id$
 */
public class WarValidator
{

    /** name of the war file to be validated */
    private String warFileName;
    /** broadcaster to help with events */
    private ValidationBroadcaster broadcaster = new ValidationBroadcaster();
    /** status listener to keep track of errors etc */
    private ValidationStatusListener status = new ValidationStatusListener();
    /** 
     * whether or not the build process should fail if a validation error occurs
     */
    private boolean failOnError = true;


    //--- Constructors ---------------------------------------------------------
    /** 
     * Creates a new instance of WarValidator
     */
    public WarValidator()
    {
        addValidationListener(getStatus());
    }

    //--- Methods --------------------------------------------------------------
    /**
     * Provides access to the status listener that is automatically attached
     * to the validation
     * 
     * @return Value of property status.
     */
    public ValidationStatusListener getStatus()
    {
        return status;
    }

    /**
     * Perform the validation.
     * 
     * @throws IllegalStateException when any error occurs
     */
    public void execute()
    {
        if (getWarFileName() == null)
        {
            throw new NullPointerException("war file name should not be null");
        }
        validate();
        if (getStatus().isError() && isFailOnError())
        {
            throw new IllegalStateException("Errors occurred during validation. "
                + "Messages should have been provided");
        }
    }

    /**
     * validate the provided war file
     */
    public void validate()
    {
        try
        {
            startValidation();
            validateFile();
            if (!getStatus().isError())
            {
                validateWarContents();
            }
        }
        finally
        {
            endValidation();
        }
    }

    /**
     * Start validation - issue a started event, and check the war file is
     * ok from a filesystem perspective - exists and is readable
     */
    protected void startValidation()
    {
        getBroadcaster().fireStartedEvent(new ValidationEvent(this,
            getWarFileName(), "war validation started"));
    }
    
    /**
     * Hook point for subclasses to add validations
     */
    protected void validateWarContents()
    {
        validateWebXml();
    }
    
    /**
     * End validation - fire an ended event
     */
    protected void endValidation()
    {
        getBroadcaster().fireEndedEvent(new ValidationEvent(this,
            getWarFileName(), "war validation ended"));
    }
    
    /**
     * validate the war file can be read and exists
     */
    private void validateFile()
    {
        File warFile = new File(getWarFileName());

        if (!warFile.exists())
        {
            error("File does not exist");
            return;
        }
        if (!warFile.canRead())
        {
            error("File can't be read");
            return;
        }
    }

    /** 
     * Validate the web.xml entry in the provided jar file.
     */
    private void validateWebXml()
    {
        WarFile war = null;
        try
        {
            war = new WarFile(getWarFileName());
            if (war.getWebXmlEntry() == null)
            {
                warning("web.xml entry not found");
                return;
            }
            validateServlets(war);
            validateJSPs(war);
            validateTaglibs(war);
            validateErrorPages(war);
            validateFormLoginConfig(war);
        }
        catch (IOException ioe)
        {
            error("Error opening war file for web.xml - possibly missing "
                + "manifest");
        }
    }

    /** 
     * Validate the servlets defined in the war file (as defined by a
     * <code>&lt;servlet&gt;</code> tag in web.xml), making sure that their
     * class defined can be loaded from the war and is not part of the 
     * external classpath
     * 
     * @param war the war file to validate
     * @throws IOException when there are any issues reading the war file
     */
    private void validateServlets(WarFile war) throws IOException
    {
        Map servlets = war.getServlets();
        if (servlets.size() != 0)
        {
            ClassLoader classLoader = new WarClassLoader(war, getClass().
                getClassLoader());
            String className = null;
            Map.Entry entry = null;
            for (Iterator entries = servlets.entrySet().iterator();
                entries.hasNext();)
            {
                entry = (Map.Entry) entries.next();
                className = (String) entry.getValue();
                info("validating servlet name: " + entry.getKey() + " class: " 
                    + className);
                // check each servlet by loading the class
                validateClass(className, classLoader);
            }
        }
    }

    /**
     * Validate the jsps defined in the war file (as defined by a
     * <code>&lt;servlet&gt;</code> tag with a nested <code>&lt;jsp-file&gt;
     * </code> in web.xml), making sure that the resource specifed by
     * <code>&lt;jsp-file&gt;</code> exists in the war file
     * 
     * @param war the war file to validate
     * @throws IOException when there are any issues reading the war file
     */
    private void validateJSPs(WarFile war) throws IOException
    {
        Map jsps = war.getJSPs();
        if (jsps.size() != 0)
        {
            Map.Entry entry = null;
            for (Iterator entries = jsps.entrySet().iterator();
                entries.hasNext();)
            {
                entry = (Map.Entry) entries.next();
                String jspFile = (String) entry.getValue();
                info("validating servlet name: " + entry.getKey()
                    + " jsp file: " + jspFile);

                if (!war.hasFile(jspFile))
                {
                    error("JSP File: '" + jspFile + "' not found");
                }
            }
        }
    }
    
    /** 
     * Validate that the given className can be loaded by the given
     * {@link ClasssLoader}
     * 
     * @param className the name of a class to attempt loading
     * @param loader a {@link ClassLoader class loader}
     */
    protected void validateClass(String className, ClassLoader loader)
    {
        try
        {
            Class clazz = loader.loadClass(className);
            if (clazz.getClassLoader() != loader)
            {
                // loaded from classpath - a no no.
                error("class (" + className + ") loaded from system classpath " 
                    + "rather than war file");
            }
        }
        catch (ClassNotFoundException e)
        {
            error("class (" + className + ") not found ");
        }
        catch (NoClassDefFoundError error)
        {
            error("class (" + className + ") was found, but a referenced class "
                + "was missing: " + error.getMessage());
        }

    }

    /** 
     * Validate the taglibs defined in the war file (as defined by a
     * <code>&lt;taglib&gt;</code> tag in web.xml), making sure that the 
     * resource specifed by <code>&lt;taglib-location&gt;</code> exists in the 
     * war file
     * 
     * @param war the war file to validate
     * @throws IOException when there are any issues reading the war file
     */
    private void validateTaglibs(WarFile war) throws IOException
    {
        Map taglibs = war.getTaglibs();
        if (taglibs.size() != 0)
        {
            Map.Entry entry = null;
            for (Iterator entries = taglibs.entrySet().iterator();
                entries.hasNext();)
            {
                entry = (Map.Entry) entries.next();
                String uri = (String) entry.getKey();
                String location = (String) entry.getValue();
                info("validating taglib uri: " + uri);
                if (!war.hasFile(location))
                {
                    error("Taglib location: '" + location + "' not found");
                }
            }
        }
    }

    /** 
     * Validate the error pages defined in the war file (as defined by a
     * <code>&lt;error-page&gt;</code> tag in web.xml), making sure that the 
     * location specifed by the nested <code>&lt;location&gt;</code> exists in 
     * the war file
     * 
     * @param war the war file to validate
     * @throws IOException when there are any issues reading the war file
     */
     public void validateErrorPages(WarFile war) throws IOException
     {
        Map pages = war.getErrorPages();
        if (pages.size() != 0)
        {
            Map.Entry entry = null;
            for (Iterator entries = pages.entrySet().iterator();
                entries.hasNext();)
            {
                entry = (Map.Entry) entries.next();
                String errorQualifier = (String) entry.getKey();
                String location = (String) entry.getValue();
                info("validating error page for: " + errorQualifier);
                if (!war.hasFile(location))
                {
                    error("Error page location: '" + location + "' not found");
                }
            }
        }
     }

    /**
     * Validate that the <code>&lt;form-login-config&gt</code> element, if it
     * exists contains valid login and error pages
     * 
     * @param war the war file to validate
     * @throws IOException when there are any issues reading the war file
     */
    public void validateFormLoginConfig(WarFile war) throws IOException
    {
        FormLoginConfig config = war.getFormLoginConfig();
        if (config != null)
        {
            if (!war.hasFile(config.getLoginPage()))
            {
                info("<form-config-login> login-page location: '"
                    + config.getLoginPage() + "' not found");
            }
            if (!war.hasFile(config.getErrorPage()))
            {
                error("<form-config-login> error-page location: '"
                    + config.getErrorPage() + "' not found");
            }
        }
    }
    
    /**
     * add a listener to the list to be notified
     * 
     * @param listener a {@link ValidationListener}
     */
    public void addValidationListener(ValidationListener listener)
    {
        getBroadcaster().addValidationListener(listener);
    }

    /**
     * remove a listener from the list to be notified
     * 
     * @param listener a {@link ValidationListener}
     */
    public void removeValidationListener(ValidationListener listener)
    {
        getBroadcaster().removeValidationListener(listener);
    }

    /**
     * Getter for property warFileName.
     * 
     * @return Value of property warFileName.
     */
    public String getWarFileName()
    {
        return warFileName;
    }
    
    /**
     * Setter for property warFileName.
     * 
     * @param warFileName New value of property warFileName.
     */
    public void setWarFileName(String warFileName)
    {
        this.warFileName = warFileName;
    }    
    
    /**
     * Getter for property broadcaster.
     * 
     * @return Value of property broadcaster.
     */
    protected ValidationBroadcaster getBroadcaster()
    {
        return broadcaster;
    }
    
    /** 
     * Add a formatter to pick up events and display the output.
     * Used by ant when a nested formatter element is present.
     * 
     * @param formatter a class to format validation events
     */
    public void addFormatter(ValidationFormatter formatter)
    {
        addValidationListener((ValidationListener) formatter);
    }

    /**
     * Provide a string representation of the validator
     * 
     * @return "WarValidator(file)"
     */
    public String toString()
    {
        StringBuffer buffer = new StringBuffer("WarValidator");
        if (getWarFileName() != null)
        {
            buffer.append("(").append(getWarFileName()).append(")");
        }
        return buffer.toString();
    }
    
    /**
     * Whether the build process will fail if a validation error occurs.
     * 
     * @return Value of property failOnError.
     */
    public boolean isFailOnError()
    {
        return failOnError;
    }
    
    /**
     * Set whether the build process will fail if a validation error occurs.
     * 
     * @param failOnError New value of property failOnError.
     */
    public void setFailOnError(boolean failOnError)
    {
        this.failOnError = failOnError;
    }
    
    /**
     * Helper method to fire an information message using the broadcaster
     * 
     * @param message to be fired
     */
    protected void info(String message) 
    {
        getBroadcaster().fireInformationEvent(new ValidationEvent(this,
            getWarFileName(), message));
    }
    
    /**
     * Helper method to fire an error message using the broadcaster
     * 
     * @param message to be fired
     */
    protected void error(String message) 
    {
        getBroadcaster().fireErrorEvent(new ValidationEvent(this,
            getWarFileName(), message));
    }

    /**
     * Helper method to fire a warning message using the broadcaster
     * 
     * @param message to be fired
     */
    protected void warning(String message) 
    {
        getBroadcaster().fireWarningEvent(new ValidationEvent(this,
            getWarFileName(), message));
    }

}
