/*
 * File   : $Source: /usr/local/cvs/opencms/src/org/opencms/jsp/CmsJspTagInclude.java,v $
 * Date   : $Date: 2008-07-01 12:00:39 $
 * Version: $Revision: 1.45 $
 *
 * This library is part of OpenCms -
 * the Open Source Content Management System
 *
 * Copyright (c) 2002 - 2008 Alkacon Software GmbH (http://www.alkacon.com)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * For further information about Alkacon Software GmbH, please see the
 * company website: http://www.alkacon.com
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.opencms.jsp;

import org.opencms.file.CmsFile;
import org.opencms.file.CmsObject;
import org.opencms.file.CmsPropertyDefinition;
import org.opencms.flex.CmsFlexController;
import org.opencms.flex.CmsFlexResponse;
import org.opencms.loader.CmsLoaderException;
import org.opencms.loader.I_CmsResourceLoader;
import org.opencms.loader.I_CmsResourceStringDumpLoader;
import org.opencms.main.CmsException;
import org.opencms.main.OpenCms;
import org.opencms.staticexport.CmsLinkManager;
import org.opencms.util.CmsStringUtil;
import org.opencms.workplace.editors.directedit.CmsDirectEditParams;

import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.BodyTagSupport;

/**
 * Implementation of the <code>&lt;cms:include/&gt;</code> tag,
 * used to include another OpenCms managed resource in a JSP.<p>
 *
 * @author Alexander Kandzior 
 * 
 * @version $Revision: 1.45 $ 
 * 
 * @since 6.0.0 
 */
public class CmsJspTagInclude extends BodyTagSupport implements I_CmsJspTagParamParent {

    /** Serial version UID required for safe serialization. */
    private static final long serialVersionUID = 705978510743164951L;

    /** The value of the "attribute" attribute. */
    private String m_attribute;

    /** The value of the "cacheable" attribute. */
    private boolean m_cacheable;

    /** The value of the "editable" attribute. */
    private boolean m_editable;

    /** The value of the "element" attribute. */
    private String m_element;

    /** Hashmap to save parameters to the include in. */
    private Map m_parameterMap;

    /** The value of the "property" attribute. */
    private String m_property;

    /** The value of the "suffix" attribute. */
    private String m_suffix;

    /** The value of the "page" attribute. */
    private String m_target;

    /**
     * Empty constructor, required for attribute value initialization.<p>
     */
    public CmsJspTagInclude() {

        super();
        m_cacheable = true;
    }

    /**
     * Adds parameters to a parameter Map that can be used for a http request.<p>
     * 
     * @param parameters the Map to add the parameters to
     * @param name the name to add
     * @param value the value to add
     * @param overwrite if <code>true</code>, a parameter in the map will be overwritten by
     *      a parameter with the same name, otherwise the request will have multiple parameters 
     *      with the same name (which is possible in http requests)
     */
    public static void addParameter(Map parameters, String name, String value, boolean overwrite) {

        // No null values allowed in parameters
        if ((parameters == null) || (name == null) || (value == null)) {
            return;
        }

        // Check if the parameter name (key) exists
        if (parameters.containsKey(name) && (!overwrite)) {
            // Yes: Check name values if value exists, if so do nothing, else add new value
            String[] values = (String[])parameters.get(name);
            String[] newValues = new String[values.length + 1];
            System.arraycopy(values, 0, newValues, 0, values.length);
            newValues[values.length] = value;
            parameters.put(name, newValues);
        } else {
            // No: Add new parameter name / value pair
            String[] values = new String[] {value};
            parameters.put(name, values);
        }
    }

    /**
     * Includes the selected target.<p>
     * 
     * @param context the current JSP page context
     * @param target the target for the include, might be <code>null</code>
     * @param element the element to select form the target might be <code>null</code>
     * @param editable flag to indicate if the target is editable
     * @param paramMap a map of parameters for the include, will be merged with the request 
     *      parameters, might be <code>null</code>
     * @param req the current request
     * @param res the current response
     *
     * @throws JspException in case something goes wrong
     */
    public static void includeTagAction(
        PageContext context,
        String target,
        String element,
        boolean editable,
        Map paramMap,
        ServletRequest req,
        ServletResponse res) throws JspException {

        // no locale and no cachable parameter are used by default
        includeTagAction(context, target, element, null, editable, true, paramMap, req, res);
    }

    /**
     * Includes the selected target.<p>
     * 
     * @param context the current JSP page context
     * @param target the target for the include, might be <code>null</code>
     * @param element the element to select form the target, might be <code>null</code>
     * @param locale the locale to use for the selected element, might be <code>null</code> 
     * @param editable flag to indicate if the target is editable
     * @param cacheable flag to indicate if the target should be cacheable in the Flex cache
     * @param paramMap a map of parameters for the include, will be merged with the request 
     *      parameters, might be <code>null</code>
     * @param req the current request
     * @param res the current response
     *
     * @throws JspException in case something goes wrong
     */
    public static void includeTagAction(
        PageContext context,
        String target,
        String element,
        Locale locale,
        boolean editable,
        boolean cacheable,
        Map paramMap,
        ServletRequest req,
        ServletResponse res) throws JspException {

        // the Flex controller provides access to the internal OpenCms structures
        CmsFlexController controller = CmsFlexController.getController(req);

        if (target == null) {
            // set target to default
            target = controller.getCmsObject().getRequestContext().getUri();
        }

        // resolve possible relative URI
        target = CmsLinkManager.getAbsoluteUri(target, controller.getCurrentRequest().getElementUri());

        try {
            // now resolve additional include extensions that might be required for special loader implementations
            target = OpenCms.getResourceManager().resolveIncludeExtensions(
                target,
                element,
                editable,
                paramMap,
                req,
                res);
            // check if the target actually exists in the OpenCms VFS
            controller.getCmsObject().readResource(target);
        } catch (CmsException e) {
            // store exception in controller and discontinue
            controller.setThrowable(e, target);
            throw new JspException(e);
        }

        // include direct edit "start" element (if enabled)
        boolean directEditOpen = editable
            && CmsJspTagEditable.startDirectEdit(context, new CmsDirectEditParams(target, element));

        // save old parameters from request
        Map oldParameterMap = req.getParameterMap();
        try {
            // each include will have it's unique map of parameters
            Map parameterMap = (paramMap == null) ? new HashMap() : new HashMap(paramMap);
            if (cacheable && (element != null)) {
                // add template element selector for JSP templates (only required if cacheable)
                addParameter(parameterMap, I_CmsResourceLoader.PARAMETER_ELEMENT, element, true);
            }
            // add parameters to set the correct element
            controller.getCurrentRequest().addParameterMap(parameterMap);
            if (cacheable) {
                // use include with cache
                includeActionWithCache(controller, context, target, parameterMap, req, res);
            } else {
                // no cache required
                includeActionNoCache(controller, context, target, element, locale, req, res);
            }
        } finally {
            // restore old parameter map (if required)
            if (oldParameterMap != null) {
                controller.getCurrentRequest().setParameterMap(oldParameterMap);
            }
        }

        // include direct edit "end" element (if required)
        if (directEditOpen) {
            CmsJspTagEditable.endDirectEdit(context);
        }
    }

    /**
     * Includes the selected target without caching.<p>
     * 
     * @param controller the current JSP controller
     * @param context the current JSP page context
     * @param target the target for the include
     * @param element the element to select form the target
     * @param locale the locale to select from the target
     * @param req the current request
     * @param res the current response
     *
     * @throws JspException in case something goes wrong
     */
    private static void includeActionNoCache(
        CmsFlexController controller,
        PageContext context,
        String target,
        String element,
        Locale locale,
        ServletRequest req,
        ServletResponse res) throws JspException {

        try {
            // include is not cachable 
            CmsFile file = controller.getCmsObject().readFile(target);
            CmsObject cms = controller.getCmsObject();
            if (locale == null) {
                locale = cms.getRequestContext().getLocale();
            }
            // get the loader for the requested file 
            I_CmsResourceLoader loader = OpenCms.getResourceManager().getLoader(file);
            String content;
            if (loader instanceof I_CmsResourceStringDumpLoader) {
                // loader can provide content as a String
                I_CmsResourceStringDumpLoader strLoader = (I_CmsResourceStringDumpLoader)loader;
                content = strLoader.dumpAsString(cms, file, element, locale, req, res);
            } else {
                if (!(req instanceof HttpServletRequest) || !(res instanceof HttpServletResponse)) {
                    // http type is required for loader (no refactoring to avoid changes to interface)
                    CmsLoaderException e = new CmsLoaderException(Messages.get().container(
                        Messages.ERR_BAD_REQUEST_RESPONSE_0));
                    throw new JspException(e);
                }
                // get the bytes from the loader and convert them to a String
                byte[] result = loader.dump(
                    cms,
                    file,
                    element,
                    locale,
                    (HttpServletRequest)req,
                    (HttpServletResponse)res);
                // use the encoding from the property or the system default if not available
                String encoding = cms.readPropertyObject(file, CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING, true).getValue(
                    OpenCms.getSystemInfo().getDefaultEncoding());
                // If the included target issued a redirect null will be returned from loader 
                if (result == null) {
                    result = new byte[0];
                }
                content = new String(result, encoding);
            }
            // write the content String to the JSP output writer
            context.getOut().print(content);

        } catch (ServletException e) {
            // store original Exception in controller in order to display it later
            Throwable t = (e.getRootCause() != null) ? e.getRootCause() : e;
            t = controller.setThrowable(t, target);
            throw new JspException(t);
        } catch (IOException e) {
            // store original Exception in controller in order to display it later
            Throwable t = controller.setThrowable(e, target);
            throw new JspException(t);
        } catch (CmsException e) {
            // store original Exception in controller in order to display it later
            Throwable t = controller.setThrowable(e, target);
            throw new JspException(t);
        }
    }

    /**
     * Includes the selected target using the Flex cache.<p>
     * 
     * @param controller the current JSP controller
     * @param context the current JSP page context
     * @param target the target for the include, might be <code>null</code>
     * @param parameterMap a map of parameters for the include
     * @param req the current request
     * @param res the current response
     *
     * @throws JspException in case something goes wrong
     */
    private static void includeActionWithCache(
        CmsFlexController controller,
        PageContext context,
        String target,
        Map parameterMap,
        ServletRequest req,
        ServletResponse res) throws JspException {

        try {
            // write out a FLEX_CACHE_DELIMITER char on the page, this is used as a parsing delimeter later
            context.getOut().print(CmsFlexResponse.FLEX_CACHE_DELIMITER);
            // add the target to the include list (the list will be initialized if it is currently empty)
            controller.getCurrentResponse().addToIncludeList(target, parameterMap);
            // now use the Flex dispatcher to include the target (this will also work for targets in the OpenCms VFS)
            controller.getCurrentRequest().getRequestDispatcher(target).include(req, res);
        } catch (ServletException e) {
            // store original Exception in controller in order to display it later
            Throwable t = (e.getRootCause() != null) ? e.getRootCause() : e;
            t = controller.setThrowable(t, target);
            throw new JspException(t);
        } catch (IOException e) {
            // store original Exception in controller in order to display it later
            Throwable t = controller.setThrowable(e, target);
            throw new JspException(t);
        }
    }

    /**
     * This methods adds parameters to the current request.<p>
     * 
     * Parameters added here will be treated like parameters from the 
     * HttpRequest on included pages.<p>
     * 
     * Remember that the value for a parameter in a HttpRequest is a 
     * String array, not just a simple String. If a parameter added here does
     * not already exist in the HttpRequest, it will be added. If a parameter 
     * exists, another value will be added to the array of values. If the 
     * value already exists for the parameter, nothing will be added, since a 
     * value can appear only once per parameter.<p>
     * 
     * @param name the name to add
     * @param value the value to add
     * @see org.opencms.jsp.I_CmsJspTagParamParent#addParameter(String, String)
     */
    public void addParameter(String name, String value) {

        // No null values allowed in parameters
        if ((name == null) || (value == null)) {
            return;
        }

        // Check if internal map exists, create new one if not
        if (m_parameterMap == null) {
            m_parameterMap = new HashMap();
        }

        addParameter(m_parameterMap, name, value, false);
    }

    /**
     * @return <code>EVAL_PAGE</code>
     * 
     * @see javax.servlet.jsp.tagext.Tag#doEndTag()
     * 
     * @throws JspException by interface default
     */
    public int doEndTag() throws JspException {

        ServletRequest req = pageContext.getRequest();
        ServletResponse res = pageContext.getResponse();

        if (CmsFlexController.isCmsRequest(req)) {
            // this will always be true if the page is called through OpenCms 
            CmsObject cms = CmsFlexController.getCmsObject(req);
            String target = null;

            // try to find out what to do
            if (m_target != null) {
                // option 1: target is set with "page" or "file" parameter
                target = m_target + getSuffix();
            } else if (m_property != null) {
                // option 2: target is set with "property" parameter
                try {
                    String prop = cms.readPropertyObject(cms.getRequestContext().getUri(), m_property, true).getValue();
                    if (prop != null) {
                        target = prop + getSuffix();
                    }
                } catch (RuntimeException e) {
                    // target must be null
                    target = null;
                } catch (Exception e) {
                    // target will be null
                    e = null;
                }
            } else if (m_attribute != null) {
                // option 3: target is set in "attribute" parameter
                try {
                    String attr = (String)req.getAttribute(m_attribute);
                    if (attr != null) {
                        target = attr + getSuffix();
                    }
                } catch (RuntimeException e) {
                    // target must be null
                    target = null;
                } catch (Exception e) {
                    // target will be null
                    e = null;
                }
            } else {
                // option 4: target might be set in body
                String body = null;
                if (getBodyContent() != null) {
                    body = getBodyContent().getString();
                    if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(body)) {
                        // target IS set in body
                        target = body + getSuffix();
                    }
                    // else target is not set at all, default will be used 
                }
            }

            // now perform the include action
            includeTagAction(pageContext, target, m_element, null, m_editable, m_cacheable, m_parameterMap, req, res);

            if (OpenCms.getSystemInfo().getServletContainerSettings().isReleaseTagsAfterEnd()) {
                // must call release here manually to make sure m_parameterMap is cleared
                release();
            } else {
                m_parameterMap = null;
            }
        }

        return EVAL_PAGE;
    }

    /**
     * Returns <code>{@link #EVAL_BODY_BUFFERED}</code>.<p>
     * 
     * @return <code>{@link #EVAL_BODY_BUFFERED}</code>
     * 
     * @see javax.servlet.jsp.tagext.Tag#doStartTag()
     */
    public int doStartTag() {

        return EVAL_BODY_BUFFERED;
    }

    /**
     * Returns the attribute.<p>
     * 
     * @return the attribute
     */
    public String getAttribute() {

        return m_attribute != null ? m_attribute : "";
    }

    /**
     * Returns the cacheable flag.<p>
     * 
     * @return the cacheable flag
     */
    public String getCacheable() {

        return String.valueOf(m_cacheable);
    }

    /**
     * Returns the editable flag.<p>
     * 
     * @return the editable flag
     */
    public String getEditable() {

        return String.valueOf(m_editable);
    }

    /**
     * Returns the element.<p>
     * 
     * @return the element
     */
    public String getElement() {

        return m_element;
    }

    /**
     * Returns the value of <code>{@link #getPage()}</code>.<p>
     * 
     * @return the value of <code>{@link #getPage()}</code>
     * @see #getPage()
     */
    public String getFile() {

        return getPage();
    }

    /**
     * Returns the include page target.<p>
     * 
     * @return the include page target
     */
    public String getPage() {

        return m_target != null ? m_target : "";
    }

    /**
     * Returns the property.<p>
     * 
     * @return the property
     */
    public String getProperty() {

        return m_property != null ? m_property : "";
    }

    /**
     * Returns the suffix.<p>
     * 
     * @return the suffix
     */
    public String getSuffix() {

        return m_suffix != null ? m_suffix : "";
    }

    /**
     * @see javax.servlet.jsp.tagext.Tag#release()
     */
    public void release() {

        super.release();
        m_target = null;
        m_suffix = null;
        m_property = null;
        m_element = null;
        m_parameterMap = null;
        m_editable = false;
        m_cacheable = true;
    }

    /**
     * Sets the attribute.<p>
     * 
     * @param attribute the attribute to set
     */
    public void setAttribute(String attribute) {

        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(attribute)) {
            m_attribute = attribute;
        }
    }

    /**
     * Sets the cacheable flag.<p>
     *
     * Cachable is <code>true</code> by default.<p>
     * 
     * @param cacheable the flag to set
     */
    public void setCacheable(String cacheable) {

        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(cacheable)) {
            m_cacheable = Boolean.valueOf(cacheable).booleanValue();
        }
    }

    /**
     * Sets the editable flag.<p>
     * 
     * Editable is <code>false</code> by default.<p>
     * 
     * @param editable the flag to set
     */
    public void setEditable(String editable) {

        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(editable)) {
            m_editable = Boolean.valueOf(editable).booleanValue();
        }
    }

    /**
     * Sets the element.<p>
     * 
     * @param element the element to set
     */
    public void setElement(String element) {

        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(element)) {
            m_element = element;
        }
    }

    /**
     * Sets the file, same as using <code>setPage()</code>.<p>
     * 
     * @param file the file to set
     * @see #setPage(String)
     */
    public void setFile(String file) {

        setPage(file);
    }

    /**
     * Sets the include page target.<p>
     * 
     * @param target the target to set
     */
    public void setPage(String target) {

        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(target)) {
            m_target = target;
        }
    }

    /**
     * Sets the property.<p>
     * 
     * @param property the property to set
     */
    public void setProperty(String property) {

        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(property)) {
            m_property = property;
        }
    }

    /**
     * Sets the suffix.<p>
     * 
     * @param suffix the suffix to set
     */
    public void setSuffix(String suffix) {

        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(suffix)) {
            m_suffix = suffix.toLowerCase();
        }
    }
}