/*
 * Copyright 2003-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.
 */

package org.apache.velocity.tools.view.servlet;

import java.io.File;
import java.io.InputStream;
import java.util.HashMap;

import javax.servlet.ServletContext;

import org.apache.commons.collections.ExtendedProperties;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.runtime.resource.Resource;
import org.apache.velocity.runtime.resource.loader.ResourceLoader;

/**
 * Resource loader that uses the ServletContext of a webapp to
 * load Velocity templates.  (it's much easier to use with servlets than
 * the standard FileResourceLoader, in particular the use of war files
 * is transparent).
 *
 * The default search path is '/' (relative to the webapp root), but
 * you can change this behaviour by specifying one or more paths
 * by mean of as many webapp.resource.loader.path properties as needed
 * in the velocity.properties file.
 *
 * All paths must be relative to the root of the webapp.
 *
 * To enable caching and cache refreshing the webapp.resource.loader.cache and
 * webapp.resource.loader.modificationCheckInterval properties need to be
 * set in the velocity.properties file ... auto-reloading of global macros
 * requires the webapp.resource.loader.cache property to be set to 'false'.
 *
 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
 * @author <a href="mailto:nathan@esha.com">Nathan Bubna</a>
 * @author <a href="mailto:claude@savoirweb.com">Claude Brisson</a>
 * @version $Id: WebappLoader.java 304 2005-10-20 12:21:53Z malcolm_edgar $  */

public class WebappLoader extends ResourceLoader
{

    /** The root paths for templates (relative to webapp's root). */
    protected String[] paths = null;
    protected HashMap templatePaths = null;

    protected ServletContext servletContext = null;

    /**
     *  This is abstract in the base class, so we need it.
     *  <br>
     *  NOTE: this expects that the ServletContext has already
     *        been placed in the runtime's application attributes
     *        under its full class name (i.e. "javax.servlet.ServletContext").
     *
     * @param configuration the {@link ExtendedProperties} associated with
     *        this resource loader.
     */
    public void init(ExtendedProperties configuration)
    {
        log.info("WebappLoader : initialization starting.");

        /* get configured paths */
        paths = configuration.getStringArray("path");
        if (paths == null || paths.length == 0)
        {
            paths = new String[1];
            paths[0] = "/";
        }
        else
        {
            /* make sure the paths end with a '/' */
            for (int i=0; i < paths.length; i++)
            {
                if (!paths[i].endsWith("/"))
                {
                    paths[i] += '/';
                }
                log.info("WebappLoader : added template path - '" + paths[i] + "'");
            }
        }

        /* get the ServletContext */
        Object obj = rsvc.getApplicationAttribute(ServletContext.class.getName());
        if (obj instanceof ServletContext)
        {
            servletContext = (ServletContext)obj;
        }
        else
        {
            log.error("WebappLoader : unable to retrieve ServletContext");
        }

        /* init the template paths map */
        templatePaths = new HashMap();

        log.info("WebappLoader : initialization complete.");
    }

    /**
     * Get an InputStream so that the Runtime can build a
     * template with it.
     *
     * @param name name of template to get
     * @return InputStream containing the template
     * @throws ResourceNotFoundException if template not found
     *         in  classpath.
     */
    public synchronized InputStream getResourceStream( String name )
        throws ResourceNotFoundException
    {
        InputStream result = null;

        if (name == null || name.length() == 0)
        {
            throw new ResourceNotFoundException ("WebappLoader : No template name provided");
        }

        /* since the paths always ends in '/',
         * make sure the name never starts with one */
        while (name.startsWith("/"))
        {
            name = name.substring(1);
        }

        Exception exception = null;
        for (int i = 0; i < paths.length; i++)
        {
            try
            {
                result = servletContext.getResourceAsStream(paths[i] + name);

                /* save the path and exit the loop if we found the template */
                if (result != null)
                {
                    templatePaths.put(name, paths[i]);
                    break;
                }
            }
            catch (Exception e)
            {
                /* only save the first one for later throwing */
                if (exception == null)
                {
                    exception = e;
                }
            }
        }

        /* if we never found the template */
        if (result == null)
        {
            String msg;
            if (exception == null)
            {
                msg = "WebappLoader : Resource '" + name + "' not found.";
            }
            else
            {
                msg = exception.getMessage();
            }
            /* convert to a general Velocity ResourceNotFoundException */
            throw new ResourceNotFoundException(msg);
        }

        return result;
    }

    /**
     * Checks to see if a resource has been deleted, moved or modified.
     *
     * @param resource Resource  The resource to check for modification
     * @return boolean  True if the resource has been modified
     */
    public boolean isSourceModified(Resource resource)
    {
        String rootPath = servletContext.getRealPath("/");
        if(rootPath == null) {
           /*
            * rootPath is null if the servlet container cannot translate the
            * virtual path to a real path for any reason (such as when the
            * content is being made available from a .war archive)
            */
            return false;
        }

        /* first, try getting the previously found file */
        String fileName = resource.getName();
        String savedPath = (String)templatePaths.get(fileName);
        File cachedFile = new File(rootPath + savedPath, fileName);
        if (!cachedFile.exists())
        {
            /* then the source has been moved and/or deleted */
            return true;
        }

        /* check to see if the file can now be found elsewhere
         * before it is found in the previously saved path */
        File currentFile = null;
        for (int i = 0; i < paths.length; i++)
        {
            currentFile = new File(rootPath + paths[i], fileName);
            if (currentFile.canRead())
            {
                /* stop at the first resource found
                 * (just like in getResourceStream()) */
                break;
            }
        }

        /* if the current is the cached and it is readable */
        if (cachedFile.equals(currentFile) && cachedFile.canRead())
        {
            /* then (and only then) do we compare the last modified values */
            return (cachedFile.lastModified() != resource.getLastModified());
        }
        else
        {
            /* we found a new file for the resource
             * or the resource is no longer readable. */
            return true;
        }
    }

    /**
     * Checks to see when a resource was last modified
     *
     * @param resource Resource the resource to check
     * @return long The time when the resource was last modified or 0 if the file can't be read
     */
    public long getLastModified(Resource resource)
    {
        String rootPath = servletContext.getRealPath("/");
        if(rootPath == null) {
           /*
            * rootPath is null if the servlet container cannot translate the
            * virtual path to a real path for any reason (such as when the
            * content is being made available from a .war archive)
            */
            return 0;
        }

        String path = (String)templatePaths.get(resource.getName());
        File file = new File(rootPath + path, resource.getName());
        if (file.canRead())
        {
            return file.lastModified();
        }
        else
        {
            return 0;
        }
    }
}
