/* Copyright (c) 2001 Oracle Corporation */
package oracle.portal.provider.v2.render.http;

import java.io.*;

import javax.servlet.*;
import javax.servlet.http.*;

import oracle.portal.provider.v2.*;
import oracle.portal.provider.v2.render.*;
import oracle.portal.provider.v2.utils.ValidationError;
import oracle.portal.utils.xml.v2.*;
import oracle.portal.provider.v2.http.HttpCommonConstants;
import oracle.portal.log.LogManager;

import oracle.webdb.provider.v2.utils.http.ProviderRequestWrapper;
import oracle.webdb.provider.v2.utils.http.TextResponseWrapper;
import oracle.webdb.utils.SimpleStringBuffer;

/**
 * When maintained by a {@link oracle.portal.provider.v2.render.RenderManager},
 * a <code>ResourceRenderer</code> renders a single page's contents from
 * resource known to the servlet container. Examples of such resources include
 * JSPs, servlets and static HTML files. Any resource can be used as long as
 * the servlet container can return a valid
 * <code>javax.servlet.RequestDispatcher</code> for it. The resource can be
 * located in the same servlet context as the current provider or a different
 * one.
 */
public class ResourceRenderer extends BaseManagedRenderer
{
    /**
     * Error message indicating the target resource generated an error.
     */
    private static final String TARGET_ERROR_MESSAGE =
        "Error occurred while rendering portlet - " +
        "see provider log file for details";

    // Context containing the resource
    private String mContext = null;

    // URI identifying the resource
    private String mResourcePath = null;

    // The character set to be used for decoding, overriding the default
    // behaviour, i.e, use the character set specified in the content type
    // request header or ISO-8859-1
    private String mCharSet;

    /**
     * Null Constructor. The {@link BaseManagedRenderer#preInitialize(Object)}
     * method should be called after creating an object with this constructor
     * and the {@link BaseManagedRenderer#postInitialize()} method should be
     * called after setting up page properties with the various accessor
     * methods. Normally a {@link DefaultNodeHandler} takes care of these
     * details.
     */
    public ResourceRenderer()
    {
        super();
    }

    /**
     * Creates and initializes a <code>ResourceRenderer</code> owned by the
     * given {@link RenderManager}, to render from the resource at the
     * given path.
     *
     * @param <code>RenderManager</code> that owns this page
     * @param path the path to the resource.
     * @exception PortletException if an error occurs while initializing the
     * object
     */
    public ResourceRenderer(RenderManager parent, String path)
        throws PortletException
    {
        try
        {
            preInitialize(parent);
            setResourcePath(path);
            postInitialize();
        }
        catch (NodeHandlerException e)
        {
            throw new PortletException(e);
        }
    }

    /**
     * Gets the context root of the <code>javax.servlet.ServletContext</code>
     * containing the resource. If the resource is located in the same context
     * as the current provider, this method returns
     * <code>null</code>.
     *
     * @return the context root, or <code>null</code> if the context is the
     * same as the current provider.
     */
    public String getContext()
    {
        return mContext;
    }

    /**
     * Sets the context root of the <code>javax.servlet.ServletContext</code>
     * containing the resource. This should only be set if the resource is
     * located in a <i>different</i> servelet context than the current
     * provider.
     *
     * @param context context root known to the server.
     */
    public void setContext(String context)
    {
        mContext = context;
        // Make sure the string starts with a separator, and doesn't end with
        // one
        if(!mContext.startsWith("/"))
        {
            mContext = "/" + mContext;
        }
        if(mContext.endsWith("/"))
        {
            mContext = mContext.substring(0, mContext.length()-1);
        }
    }

    /**
     * Gets the path to the resource used for rendering. This path is relative
     * to the context root of the J2EE Application.
     *
     * @return the resource path, relative to the context root.
     */
    public String getResourcePath()
    {
        return mResourcePath;
    }

    /**
     * Sets the path of the resource used for rendering. This path should be
     * relative to the context root of the J2EE Application. It should fully
     * locate the resource so, in the case of JSPs and static files, should
     * include the file name.
     *
     * @param path resource location.
     */
    public void setResourcePath(String path)
    {
        mResourcePath = path;
        // Make sure the string starts with a separator
        if(!mResourcePath.startsWith("/"))
        {
            mResourcePath = "/" + mResourcePath;
        }
    }

    /**
     * Sets the character set to be used for rendering resource content.
     *
     * Overrides the default behaviour where the character set is defined
     * in the content type request header or defaults to ISO-8859-1.
     */
    public void setCharSet(String charSet)
    {
        mCharSet = charSet;
    }

    /**
     * Performs the extra steps required to initialize and validate a
     * <code>ResourceRenderer</code> object once its properties have been set
     * with the appropriate accessor methods. A <code>ResourceRenderer</code>
     * object must be supplied with a non null resource path.
     *
     * @exception NodeHandlerException if a resource path has not been
     * specified.
     */
    public void postInitialize() throws NodeHandlerException
    {
        super.postInitialize();
        if (mResourcePath == null)
        {
            throw new NodeHandlerException("A ResourceRenderer must be " +
                                           "supplied with a resource path");
        }
    }

    /**
     * In addition to setting default http headers for the response (see {@link
     * BaseManagedRenderer}), <code>ResourceRenderer</code> ensures the
     * <code>HttpServletResponse</code> is wrapped in the correct manner.
     *
     * @param pr a {@link
     * oracle.portal.provider.v2.render.PortletRenderRequest}.
     */
    public boolean prepareResponse(PortletRenderRequest pr)
        throws PortletException
    {
        // Make sure we invoke the superclass first.
        if (super.prepareResponse(pr))
        {
            // Only take action if the portlet is required to render
            // non-container markup.
            if (  !pr.getRenderContext().isCollapsed() 
               || !mParent.getRenderContainer() )
            {
                // Wrap the response in a protected method so that, if need be,
                // default collapse behaviour can be overriden.
                wrapResponse(pr);
            }
            return true;
        }
        return false;
    }

    /**
     * Render the specified page's contents from the required resource. This
     * resource may be any that the servlet container can return a valid
     * <code>javax.servlet.RequestDispatcher</code> for. Examples include JSPs,
     * servlets and static HTML files.
     *
     * @param pr a {@link oracle.portal.provider.v2.render.PortletRenderRequest}.
     */
    public void renderBody(PortletRenderRequest pr) throws PortletException
    {
        // Buffer for use when creating log/exception messages, saves doing
        // string concats.
        SimpleStringBuffer msg = new SimpleStringBuffer(200);

        // Get the ServletContext for this request.
        ServletContext context =
            (ServletContext)pr.getAttribute(HttpCommonConstants.SERVLET_CONTEXT);

        // Get the request and response objects for this request.
        ProviderRequestWrapper wrappedRequest =
            (ProviderRequestWrapper)pr.getAttribute(HttpCommonConstants.SERVLET_REQUEST);
        TextResponseWrapper wrappedResponse =
            (TextResponseWrapper)pr.getAttribute(HttpCommonConstants.SERVLET_RESPONSE);

        // Retrieve the current content of the wrapped response, for example
        // portlet container markup.
        String initialContent = null;
        try
        {
            initialContent = wrappedResponse.getData();
        }
        catch (IOException ioExp)
        {
            LogManager.severe("ResourceRenderer.renderBody - received " +
                              "IOException getting initial content", ioExp);
            throw new PortletException(ioExp);
        }

        // Ensure the destination resource will believe that the response is
        // empty.
        wrappedResponse.resetBuffer();

        // Enusre our reference is to the required context.
        if (mContext != null)
        {
            context = context.getContext(mContext);
            if (context == null)
            {
                msg.append("Unable to retrieve context: ").append(mContext);
                throw new PortletException(msg.toString());
            }
        }

        // Get the RequestDispatcher.
        RequestDispatcher dispatcher =
            context.getRequestDispatcher(mResourcePath);

        // If the RequestDispatcher is null, raise an Exception.
        if (dispatcher == null)
        {
            msg.append("Unable to obtain a RequestDispatcher for the "
                       + "resource: ").append(mResourcePath);
            throw new PortletException(msg.toString());
        }

        try
        {
            // Forward to the destination resource, passing our request and
            // response wrappers.
            dispatcher.forward(wrappedRequest, wrappedResponse);

            // get the status code from the wrapped response and set it on the
            // real response.
            int statusCode = wrappedResponse.getStatusCode();
            msg.append("Servlet/JSP returned status: ").append(statusCode);
            LogManager.debug(msg.toString());
            
            // The PortletRenderRequest no longer needs to know about an
            // overridden response. Hence we reset its state which enables us
            // to access the original response again.
            ((ServletPortletRenderRequest)pr).resetResponse();

            // Now get a writer from the real response to which we can write
            // the dispatchee's content.
            HttpServletResponse realResponse = (HttpServletResponse)
                pr.getAttribute(HttpCommonConstants.SERVLET_RESPONSE);
            PrintWriter out = realResponse.getWriter();

            if (statusCode >= HttpServletResponse.SC_BAD_REQUEST)
            {
                LogManager.debug("Processing error content from Servlet/JSP");
                
                realResponse.setStatus(HttpServletResponse.SC_OK);

                String contentType = getContentType().toLowerCase();
                if (contentType.equals(PortletRenderer.TYPE_HTML))
                {
                    renderHTMLErrorMessage(pr, out);
                }
                else if (contentType.equals(PortletRenderer.TYPE_MOBILE))
                {
                    renderMobileErrorMessage(out);
                }

                // log the content generated by the JSP or Servlet
                CharArrayWriter charWriter = new CharArrayWriter(1000);
                PrintWriter printWriter = new PrintWriter(charWriter);
                // Get the contents of the wrapped response, overriding the
                // character set for decoding the response if necessary.
                // This covers the case where the resource renderer is used
                // to render html files containing utf encoding. 
                printWriter.print(wrappedResponse.getData(mCharSet));
                
                msg.setLength(0);
                msg.append("ResourceRenderer.renderBody - Resource \"");
                msg.append(mResourcePath);
                msg.append("\" returned HTTP Status: ");
                msg.append(statusCode);
                msg.append(". Error message: ");
                msg.append(wrappedResponse.getErrorMessage());
                msg.append(". Content returned follows....");
                
                LogManager.severe(msg.toString());
                LogManager.severe(charWriter.toString());
            }
            else
            {
                LogManager.debug("Processing successful Servlet/JSP content");
                realResponse.setStatus(statusCode);    
            
                // Write the contents of the wrapped response to the original
                // response ensuring portlet header markup is included.
                if (initialContent != null)
                {
                    out.print(initialContent);
                }
                out.print(wrappedResponse.getData(mCharSet));
            }
        }
        catch (IOException ioe)
        {
            LogManager.severe("ResourceRenderer.renderBody - received IOException",
                              ioe);
            throw new PortletException(ioe);
        }
        catch (ServletException se)
        {
            Throwable t = se.getRootCause();
            msg.setLength(0);
            msg.append("ResourceRenderer.renderBody - recieved " +
                       "ServletException. Root cause is");
            LogManager.severe(msg.toString(), t);
            throw new PortletException(t);
        }
    }

    /**
     * Validates the internal state of this <code>ResourceRenderer</code>
     * instance. Errors found are reported using the supplied {@link
     * oracle.portal.provider.v2.utils.ValidationError} instance.
     *
     * @param context The context of the calling instance.
     * @param errors The current list of errors.
     */
    public void validate(String context, ValidationError errors)            
    {                                                                       
        String ctx = ValidationError.newContext(context, "ResourceRenderer",
                                                null);
                            
        // call super type
        super.validate(ctx, errors);
                                                                            
        // Check the validity of the member variables                       
        if (mResourcePath == null)
        {                                                                   
            errors.addError(ctx, "resource path must not be null", null);
        }  
    }

    /**
     * Wraps the response in preparation for a portlet body rendition.
     *
     * @param pr a {@link
     * oracle.portal.provider.v2.render.PortletRenderRequest}
     */
    protected void wrapResponse(PortletRenderRequest pr)
    {
        // Wrap the original response. Ensuring the portlet request knows that
        // we have overriden it means that any attempt to set the content-type
        // and encoding by PortletContainerRenderers will be picked up
        // correctly by the wrapper.
        HttpServletResponse response = (HttpServletResponse)
            pr.getAttribute(HttpCommonConstants.SERVLET_RESPONSE);
        TextResponseWrapper wrappedResponse =
            new TextResponseWrapper(response);
        ((ServletPortletRenderRequest)pr).setResponse(wrappedResponse);
    }

    private void renderMobileErrorMessage(PrintWriter out)
    {
        // Discard the data written to the wrapped response as it is likely a
        // HTML error message. Generate something sensible for mobile xml.
        out.println("<SimpleText>");
        out.print("  <SimpleTextItem>");
        out.print(TARGET_ERROR_MESSAGE);
        out.println("</SimpleTextItem>");
        out.println("</SimpleText>");
    }

    private void renderHTMLErrorMessage(PortletRenderRequest pr,
                                        PrintWriter out)
        throws IOException
    {
        // discard the data written to the wrapped response because it
        // may be a fully formed HTML page (OJSP generates HTML error
        // pages) that will break the portal page if displayed on
        // Netscape.

        boolean renderErrorContainer = !mParent.getRenderContainer();

        // render a portlet header/footer around the error message if 
        // the portlet would normally
        // do this itself.
        if (renderErrorContainer)
        {
            PortletRendererUtil.renderPortletHeader(pr, out, "Error");
        }
        
        out.println("<TABLE>");
        out.println("<TR>");
        out.println("<TD ALIGN=\"RIGHT\">");
        out.println("<FONT color=\"#FF0000\" face=\"arial,helvetica\">");
        out.println("<B>");

        // FIXME - use locale to translate ERROR: when translations are available
        out.println("Error:");

        out.println("</B>");
        out.println("&nbsp");
        out.println("</FONT>");
        out.println("</TD>");

        out.println("<TD ALIGN=\"LEFT\">");
        out.println("<FONT face=\"arial,helvetica\">");
        out.println(TARGET_ERROR_MESSAGE);
        out.println("</FONT>");
        out.println("</TD>");
        out.println("</TR>");
        out.println("</TABLE>");
        
        if (renderErrorContainer)
        {
            PortletRendererUtil.renderPortletFooter(pr, out);
        }
    }

}
