/* ====================================================================
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2000-2001 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Apache" and "Apache Software Foundation" and
 *     "Apache Jetspeed" must not be used to endorse or promote products
 *    derived from this software without prior written permission. For
 *    written permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache" or
 *    "Apache Jetspeed", nor may "Apache" appear in their name, without
 *    prior written permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package org.apache.jetspeed.services.template;

// java.io
import java.io.File;
import java.io.IOException;

// java.util
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Locale;
import java.util.StringTokenizer;

import javax.servlet.ServletConfig;

// turbine.util
import org.apache.turbine.util.Log;
import org.apache.turbine.util.RunData;
import org.apache.turbine.util.DynamicURI;

// turbine.services
import org.apache.turbine.services.Service;
import org.apache.turbine.services.TurbineBaseService;
import org.apache.turbine.services.TurbineServices;
import org.apache.turbine.services.InitializationException;
import org.apache.turbine.services.servlet.TurbineServlet;
import org.apache.turbine.services.localization.LocaleDetector;
import org.apache.turbine.services.resources.TurbineResources;
import org.apache.turbine.services.template.TurbineTemplate;

// jetspeed.capability
import org.apache.jetspeed.capability.CapabilityMap;

// jetspeed.util
import org.apache.jetspeed.util.MimeType;

// jetspeed.services
import org.apache.jetspeed.services.resources.JetspeedResources;
import org.apache.jetspeed.services.rundata.JetspeedRunData;
import org.apache.jetspeed.services.Profiler;

/**
 * <p>Implements all template location related operations.
 * Template location algorithms are different from the Velocity template location,
 * since Jetspeed has a specialized template directory structure.
 * This is a fix to get us through unti the TurbineTemplateService can locate
 * resources by NLS and mediatype. Then it can be removed</p>
 *
 * <p>The directory structure is currently layout out in the following order:
 *    /templateType/mediaType/LanguageCode/CountryCode</p>
 * <p>Example: /screens/html/en/US/resource.vm</p>
 *
 * @author <a href="mailto:taylor@apache.org">David Sean Taylor</a>
 * @author <a href="mailto:rapahel@apache.org">Raphael Luta</a>
 * @author <a href="mailto:paulsp@apache.org">Paul Spener</a>
 * @author <a href="mailto:kimptoc_mail@yahoo.com">Chris Kimpton</a>
 * @version $Id: JetspeedTemplateLocatorService.java,v 1.13 2002/11/08 10:05:19 raphael Exp $
 */

public class JetspeedTemplateLocatorService   extends TurbineBaseService
    implements TemplateLocatorService
{
    private final static String CONFIG_TEMPLATE_ROOT    = ".templateRoot";
    private final static String DIR_SCREENS = "/screens";
    private final static String DIR_LAYOUTS = "/layouts";
    private final static String DIR_PORTLETS = "/portlets";
    private final static String DIR_CONTROLS = "/controls";
    private final static String DIR_CONTROLLERS = "/controllers";
    private final static String DIR_NAVIGATIONS = "/navigations";
    private final static String DIR_PARAMETERS = "/parameters";
    private final static String DIR_EMAILS = "/emails";
    private static final String PATH_SEPARATOR = "/";

    // messages
    private final static String MSG_MISSING_PARAMETER =
        "JetspeedTemplateLocatorService initialization failed. Missing parameter:";

    // the template root directories, webapp relative
    private String[] templateRoots;

    // use the name cache when looking up a template
    private boolean useNameCache = true;

    // template name cache
    private Map templateMap = null;

    /**
     * This is the early initialization method called by the
     * Turbine <code>Service</code> framework
     * @param conf The <code>ServletConfig</code>
     * @exception throws a <code>InitializationException</code> if the service
     * fails to initialize
     */
    public synchronized void init(ServletConfig conf) throws InitializationException {

        // already initialized
        if (getInit()) return;

        initConfiguration();

        // initialization done
        setInit(true);

     }

    public void init() throws InitializationException {
        Log.info( "Late init for JetspeedTemplateLocatorService called" );
        while( !getInit() ) {
            //Not yet...
            try {
                Thread.sleep( 100 );
                Log.info( "Waiting for init of JetspeedTemplateLocatorService..." );
            } catch (InterruptedException ie ) {
                Log.error( ie );
            }
        }
    }

    /**
     * This is the shutdown method called by the
     * Turbine <code>Service</code> framework
     */
    public void shutdown() {
    }

    /**
     * Locate a screen template using Jetspeed template location algorithm, searching by
     * mediatype and language criteria extracted from the request state in rundata.
     *
     * @param data The rundata for the request.
     * @param template The name of the template.
     *
     * @return The path relative to the screens directory for the requested screen template,
     *          or null if not found.
     */
    public String locateScreenTemplate(RunData data, String template)
    {
        String path = localizeTemplateName(data);

        String located = locateTemplate(data, DIR_SCREENS, path, template);
        if (null == located)
        {
            // Try to locate it directly on file system, perhaps it was recently added
            useNameCache = false;
            located = locateTemplate(data, DIR_SCREENS, path, template);
            if (null != located)
            {
                // add it to the map
                templateMap.put( located, null );
            }
            else
            {
                template = "/default." + getTemplateExtension(template);
                located = locateTemplate(data, DIR_SCREENS, path, template);
            }
            useNameCache = true;
        }

        return located;
    }

    /**
     * Locate a layout template using Jetspeed template location algorithm, searching by
     * mediatype and language criteria extracted from the request state in rundata.
     *
     * @param data The rundata for the request.
     * @param template The name of the template.
     *
     * @return The path relative to the layouts directory for the requested layout template,
     *          or null if not found.
     */
    public String locateLayoutTemplate(RunData data, String template)
    {
        String path = localizeTemplateName(data);

        String located = locateTemplate(data, DIR_LAYOUTS, path, template);
        if (null == located)
        {
            // Try to locate it directly on file system, perhaps it was recently added
            useNameCache = false;
            located = locateTemplate(data, DIR_LAYOUTS, path, template);
            if (null != located)
            {
                // add it to the map
                templateMap.put( located, null );
            }
            else
            {
                template = "/default." + getTemplateExtension(template);
                located = locateTemplate(data, DIR_LAYOUTS, path, template);
            }
            useNameCache = true;
        }

        return located;
    }

    /**
     * Locate a portlet template using Jetspeed template location algorithm, searching by
     * mediatype and language criteria extracted from the request state in rundata.
     *
     * @param data The rundata for the request.
     * @param template The name of the template.
     *
     * @return The path relative to the portlets directory for the requested portlet template,
     *          or null if not found.
     */
    public String locatePortletTemplate(RunData data, String template)
    {
        String path = localizeTemplateName(data);

        String located = locateTemplate(data, DIR_PORTLETS, path, template);
        if (null == located)
        {
            // Try to locate it directly on file system, perhaps it was recently added
            useNameCache = false;
            located = locateTemplate(data, DIR_PORTLETS, path, template);
            if (null != located)
            {
                // add it to the map
                templateMap.put( located, null );
            }
            useNameCache = true;
        }

        if (null != located)
            return DIR_PORTLETS + located;
        return null;
    }

    /**
     * Locate a control template using Jetspeed template location algorithm, searching by
     * mediatype and language criteria extracted from the request state in rundata.
     *
     * @param data The rundata for the request.
     * @param template The name of the template.
     *
     * @return The path relative to the controls directory for the requested control template,
     *          or null if not found.
     */
    public String locateControlTemplate(RunData data, String template)
    {
        String path = localizeTemplateName(data);

        String located = locateTemplate(data, DIR_CONTROLS, path, template);
        if (null == located)
        {
            // Try to locate it directly on file system, perhaps it was recently added
            useNameCache = false;
            located = locateTemplate(data, DIR_CONTROLS, path, template);
            if (null != located)
            {
                // add it to the map
                templateMap.put( located, null );
            }
            useNameCache = true;
        }

        if (null != located)
            return DIR_CONTROLS + located;
        return null;
    }

    /**
     * Locate a controller template using Jetspeed template location algorithm, searching by
     * mediatype and language criteria extracted from the request state in rundata.
     *
     * @param data The rundata for the request.
     * @param template The name of the template.
     *
     * @return The path relative to the controllers directory for the requested controller template,
     *          or null if not found.
     */
    public String locateControllerTemplate(RunData data, String template)
    {
        String path = localizeTemplateName(data);

        String located = locateTemplate(data, DIR_CONTROLLERS, path, template);
        if (null == located)
        {
            // Try to locate it directly on file system, perhaps it was recently added
            useNameCache = false;
            located = locateTemplate(data, DIR_CONTROLLERS, path, template);
            if (null != located)
            {
                // add it to the map
                templateMap.put( located, null );
            }
            useNameCache = true;
        }
        if (null != located)
            return DIR_CONTROLLERS + located;
        return null;
    }

        /**
     * Locate a controller template using Jetspeed template location algorithm, searching by
     * mediatype and language criteria extracted from the request state in rundata.
     *
     * @param data The rundata for the request.
     * @param template The name of the template.
     *
     * @return The path relative to the controllers directory for the requested controller template,
     *          or null if not found.
     */
    public String locateNavigationTemplate(RunData data, String template)
    {
        String path = localizeTemplateName(data);

        String located = locateTemplate(data, DIR_NAVIGATIONS, path, template);
        if (null == located)
        {
            // Try to locate it directly on file system, perhaps it was recently added
            useNameCache = false;
            located = locateTemplate(data, DIR_NAVIGATIONS, path, template);
            if (null != located)
            {
                // add it to the map
                templateMap.put( located, null );
            }
            useNameCache = true;
        }
        if (null != located)
            return located;
        return null;
    }

    /**
     * Locate an email template using Jetspeed template location algorithm, searching by
     * mediatype and language criteria extracted from the request state in rundata.
     *
     * @param data The rundata for the request.
     * @param template The name of the template.
     *
     * @return The path relative to the emails directory for the requested email template,
     *          or null if not found.
     */
    public String locateEmailTemplate(RunData data, String template)
    {
      return locateEmailTemplate( data, template, LocaleDetector.getLocale(data));
    }

    /**
     * Locate an email template using Jetspeed template location algorithm, searching by
     * mediatype and language.
     *
     * @param data The rundata for the request.
     * @param template The name of the template.
     * @param locale The name of the language.
     *
     * @return The path relative to the emails directory for the requested email template,
     *          or null if not found.
     */
    public String locateEmailTemplate(RunData data, String template, Locale locale)
    {
         String path = localizeTemplateName(data, locale);
         String located = locateTemplate(data, DIR_EMAILS, path, template);
         if (null == located)
         {
             // Try to locate it directly on file system, perhaps it was recently added
             useNameCache = false;
             located = locateTemplate(data, DIR_EMAILS, path, template);
             if (null != located)
             {
                 // add it to the map
                 templateMap.put( located, null );
             }
             useNameCache = true;
         }
         if (null != located)
             return DIR_EMAILS + located;
         return null;
     }

    /**
     * Locate a parameter style template using Jetspeed template location algorithm, searching by
     * mediatype and language criteria extracted from the request state in rundata.
     *
     * @param data The rundata for the request.
     * @param template The name of the template.
     *
     * @return The path relative to the portlets directory for the requested portlet template,
     *          or null if not found.
     */
    public String locateParameterTemplate(RunData data, String template)
    {
        String path = localizeTemplateName(data);

        String located = locateTemplate(data, DIR_PARAMETERS, path, template);
        if (null == located)
        {
            // Try to locate it directly on file system, perhaps it was recently added
            useNameCache = false;
            located = locateTemplate(data, DIR_PARAMETERS, path, template);
            if (null != located)
            {
                // add it to the map
                templateMap.put( located, null );
            }
            useNameCache = true;
        }

        if (null != located)
            return DIR_PARAMETERS + located;
        return null;
    }

    /**
     * General template location algorithm. Starts with the most specific resource,
     * including mediatype + nls specification, and fallsback to least specific.
     *
     * @param data The rundata for the request.
     * @param resourceType The path specific to the resource type sought (eg /screens).
     * @param path The fullest path to the template based on simple NLS/mediatype directory.
     * @param template The name of the template.
     *
     * @return the exact path to the template, or null if not found.
     */
    private String locateTemplate(RunData data, String resourceType, String path, String template)
    {
        String located = null;

        // Iterate through each of the template roots
        for (int i = 0; i < templateRoots.length; i++) {
            located = locateTemplate(data, resourceType, path, template, templateRoots[i]);
            if (located != null) break;
        }

        return located;
    }

    /**
     * General template location algorithm. Starts with the most specific resource,
     * including mediatype + nls specification, and fallsback to least specific.
     *
     * @param data The rundata for the request.
     * @param resourceType The path specific to the resource type sought (eg /screens).
     * @param path The fullest path to the template based on simple NLS/mediatype directory.
     * @param template The name of the template.
     * @param templateRoot The template root to be searched.
     *
     * @return the exact path to the template, or null if not found.
     */
    private String locateTemplate(RunData data, String resourceType, String path, String template, String templateRoot)
    {
        String finalPath;

        // make sure resourceType doesn't end with "/" but starts with "/"
        if (resourceType.endsWith(PATH_SEPARATOR))
        {
            resourceType = resourceType.substring(0, resourceType.length()-1);
        }
        if (!resourceType.startsWith(PATH_SEPARATOR))
        {
            resourceType = PATH_SEPARATOR + resourceType;
        }
        // make sure path doesn't end with "/" but starts with "/"
        if (path.endsWith(PATH_SEPARATOR))
        {
            path = path.substring(0, path.length()-1);
        }
        if (!path.startsWith(PATH_SEPARATOR))
        {
            path = PATH_SEPARATOR + path;
        }
        // make sure template starts with "/"
        if (!template.startsWith(PATH_SEPARATOR))
        {
            template = PATH_SEPARATOR + template;
        }

        StringBuffer fullPath = new StringBuffer( templateRoot );

        if (!templateRoot.endsWith(PATH_SEPARATOR))
            fullPath.append(PATH_SEPARATOR);
        fullPath.append(getTemplateExtension(template));

        fullPath.append(resourceType);

        String basePath = fullPath.toString();
        String realPath = null;
        String workingPath = null;

        do
        {
            workingPath = path+template;
            realPath = TurbineServlet.getRealPath( basePath + workingPath );

            // the current template exists, return the corresponding path
            if (templateExists(realPath))
            {
                Log.debug("template exists: "+realPath+" returning "+workingPath);
                return workingPath;
            }
            // else strip path of one of its components and loop
            int pt = path.lastIndexOf(PATH_SEPARATOR);
            if ( pt > -1 )
            {
                path = path.substring(0,pt);
            }
            else
            {
                path = null;
            }
        }
        while (path != null);

        return null;
    }

    /**
     * Helper function for template locator to find a localized (NLS) resource.
     * Considers both language and country resources.
     *
     * @param data The rundata for the request.
     *
     * @return The possible path to a localized template.
     */
    private String localizeTemplateName(RunData data)
    {
      return localizeTemplateName(data, null);
    }

    /**
     * Helper function for template locator to find a localized (NLS) resource.
     * Considers both language and country resources.
     *
     * @param data The rundata for the request.
     * @param locale The locale for the request.
     *
     * @return The possible path to a localized template.
     */
    private String localizeTemplateName(RunData data, Locale inLocale)
    {
        Locale tmplocale = null;
        if (inLocale != null)
        {
          tmplocale = inLocale;
        }
        else
        {
          tmplocale = LocaleDetector.getLocale(data);
        }
        // Get the locale store it in the user object
        if (tmplocale == null) {
            tmplocale = new Locale(
                     TurbineResources.getString("locale.default.language", "en"),
                     TurbineResources.getString("locale.default.country", "US"));
        }

        data.getUser().setTemp("locale", tmplocale);

        StringBuffer templatePath = new StringBuffer();

        // set the content type (including charset)
        String type = data.getParameters().getString(Profiler.PARAM_MEDIA_TYPE, null);
        CapabilityMap cm = ((JetspeedRunData)data).getCapability();
        if (null == type)
        {
            type = cm.getPreferredType().getCode();
        }
        //data.setContentType( cm.getPreferredType().toString() );
        if ((type != null) && (type.length() > 0))
            templatePath.append(PATH_SEPARATOR).append(type);

        // Grab the Locale from the temporary storage in the User object
        Locale locale = (Locale)data.getUser().getTemp("locale");
        String language = locale.getLanguage();
        String country = locale.getCountry();
        if ((language != null) && (language.length() > 0))
            templatePath.append(PATH_SEPARATOR).append(language);
        if ((country != null) && (country.length() > 0))
            templatePath.append(PATH_SEPARATOR).append(country);
        return  templatePath.toString();
    }

    /**
     * Returns the extension for the specified template
     *
     * @param template the template name to scan for an extension
     * @return the template extension if it exists or the default
     * template extension
     */
    private String getTemplateExtension(String template)
    {
        String ext = TurbineTemplate.getDefaultExtension();

        int idx = template.lastIndexOf(".");

        if (idx > 0)
        {
            ext = template.substring(idx+1);
        }

        return ext;
    }

    /**
     * Checks for the existence of a template resource given a key.
     * The key are absolute paths to the templates, and are cached
     * in a template cache for performance.
     *
     * @param key The absolute path to the template resource.
     *
     * @return True when the template is found, otherwise false.
     */
    public boolean templateExists(String templateKey)
    {
        if (null == templateKey) return false;
        if (useNameCache == true)
        {
            return templateMap.containsKey(templateKey);
        }
        return (new File(templateKey).exists());
    }

    /**
     * Loads the configuration parameters for this service from the
     * JetspeedResources.properties file.
     *
     * @exception throws a <code>InitializationException</code> if the service
     * fails to initialize
     */
    private void initConfiguration() throws InitializationException
    {

        templateRoots = JetspeedResources.getStringArray(TurbineServices.SERVICE_PREFIX
                                               + TemplateLocatorService.SERVICE_NAME
                                               + CONFIG_TEMPLATE_ROOT);
        if ((templateRoots == null) || (templateRoots.length == 0))
            throw new InitializationException(
                MSG_MISSING_PARAMETER  + CONFIG_TEMPLATE_ROOT);

        templateMap = new HashMap();

        for (int i = 0; i < templateRoots.length; i++) {
            String templateRoot = templateRoots[i];

            if (!templateRoot.endsWith(PATH_SEPARATOR)) {
                templateRoot = templateRoot + PATH_SEPARATOR;
            }

            if (Log.getLogger().isDebugEnabled()) Log.debug("Adding templateRoot:"+templateRoot);

            // traverse starting from the root template directory and add resources
            String templateRootPath = TurbineServlet.getRealPath( templateRoot );
            if (null != templateRootPath) {
                loadNameCache( templateRootPath, "" );
            }
        }
    }

    /**
     * Loads the template name cache map to accelerate template searches.
     *
     * @param path The template
     * @param name just the name of the resource
     */
    private void loadNameCache(String path, String name)
    {
        File file = new File(path);
        if(file.isFile()) {

            // add it to the map
            templateMap.put( path, null );

        } else if(file.isDirectory()) {

            if(!path.endsWith(File.separator))
                path += File.separator;

            String list[] = file.list();

            // Process all files recursivly
            for(int ix = 0; list != null && ix < list.length; ix++)
                loadNameCache(path + list[ix], list[ix]);
        }
    }


}
