 /* Copyright (c) 1999, 2001 Oracle Corporation. All Rights Reserved */
package oracle.portal.provider.v2.render.http;

import java.util.*;
import java.io.*;
import java.net.*;

import javax.servlet.http.*;

import oracle.portal.provider.v2.*;
import oracle.portal.provider.v2.render.PortletRenderer;
import oracle.portal.provider.v2.render.RenderContext;
import oracle.portal.provider.v2.render.PortletRenderRequest;
import oracle.portal.provider.v2.render.PortletRendererUtil;
import oracle.portal.provider.v2.http.ServletProviderSession;
import oracle.portal.provider.v2.http.HttpCommonConstants;
import oracle.portal.provider.v2.personalize.PortletReference;
import oracle.portal.provider.v2.personalize.PortletPersonalizationManager;
import oracle.portal.provider.v2.security.PortletSecurityManager;
import oracle.webdb.provider.v2.utils.http.ProviderRequestWrapper;
import oracle.webdb.utils.SimpleStringBuffer;

/**
 * <code>ServletPortletRenderRequest</code> is the JPDK's base implementation
 * of the {@link oracle.portal.provider.v2.render.PortletRenderRequest}
 * interface. It represents a {@link
 * oracle.portal.provider.v2.render.PortletRenderRequest} running in a servlet
 * environment.
 */
public class ServletPortletRenderRequest implements PortletRenderRequest
{
    private ProviderRequestWrapper mServletRequest;
    private HttpServletResponse mServletResponse;
    private PortletReference mRef;
    private ProviderUser mUser;
    private RenderContext mRenderContext;
    private String[] mAcceptContentTypes;
    private String mContentType = PortletRenderer.TYPE_HTML;
    // For now, we won't allow access to this variable through public methods,
    // as we want to start translating into a standard character encoding
    private String mCharacterEncoding = PortletRenderer.DEFAULT_CHARACTER_ENCODING;
    private ResourceBundle mProviderResource;

    // If this is set, the getWriter() methods will return the writer from this
    // object.
    private HttpServletResponse mOverriddenResponse;


    /**
     * Constructs a <code>ServletPortletRenderRequest</code>.
     *
     */
    public ServletPortletRenderRequest(PortletReference ref,
                                       ProviderUser user,
                                       RenderContext rc)
    {
        mRef = ref;
        mUser = user;
        mRenderContext = rc;

        // Create our own private copy of the attribute map, so that we can
        // mutate it
        Map attributes = new HashMap(rc.getAttributes());

        // Get hold of the vital request and response objects
        mServletRequest = (ProviderRequestWrapper)attributes.remove(HttpCommonConstants.SERVLET_REQUEST);
        if(mServletRequest == null)
        {
            throw new RuntimeException("ServletPortletRenderRequest constructor expects " +
                                       HttpCommonConstants.SERVLET_REQUEST + " attribute");
        }
        // Make the servlet request available as an attribute of this portlet render request
        mServletRequest.setAttribute(HttpCommonConstants.SERVLET_REQUEST, mServletRequest);

        mServletResponse = (HttpServletResponse)attributes.remove(HttpCommonConstants.SERVLET_RESPONSE);
        if(mServletResponse == null)
        {
            throw new RuntimeException("ServletPortletRenderRequest constructor expects " +
                                       HttpCommonConstants.SERVLET_RESPONSE + " attribute");
        }
        // Although we make the servlet response available as an attribute, we
        // ensure that the user can't get the content type or character
        // encoding out of sync by exposing a 'wrapped response' whose content
        // type setter calls through to the render request.
        mServletRequest.setAttribute(HttpCommonConstants.SERVLET_RESPONSE,
                                     new HttpServletResponseWrapper(mServletResponse)
                                         {
                                             public void setContentType(String type)
                                             {
                                                 ServletPortletRenderRequest.this.setContentType(type);
                                             }
                                         }
                                     );

        // Add the servlet url to the request attributes to be used 
        // by the invalidation API.
        SimpleStringBuffer url = new SimpleStringBuffer(100)
                           .append(mServletRequest.getContextPath())
                           .append(mServletRequest.getServletPath());

        // If the service id was used to register the provider, the path info
        // will be empty.
        String pathInfo = mServletRequest.getPathInfo();
        if (pathInfo != null)
        {
            url.append(pathInfo);
        }

        mServletRequest.setAttribute(HttpCommonConstants.ORIGINAL_PROVIDER_PATH,
                                     url.toString());
 
        // Make all the remaining attributes servlet request attributes
        Iterator i=attributes.entrySet().iterator();
        while(i.hasNext())
        {
            Map.Entry entry = (Map.Entry)i.next();
            mServletRequest.setAttribute((String)entry.getKey(), entry.getValue());
        }

        // Write self as attribute in case we get forwarded to a servlet and
        // they need to get back to us.
        mServletRequest.setAttribute(HttpCommonConstants.PORTLET_RENDER_REQUEST, this);

        // Set up the accept content types, charsets and languages arrays
        setAcceptContentTypes(mServletRequest);
        setDefaultCharacterEncoding(mServletRequest);
    }

    /**
     * @return an object containing parameters to this <code>PortletRenderRequest</code>
     */
    public RenderContext getRenderContext()
    {
        return mRenderContext;
    }

    /**
     * @return an integer representing the render mode. Constants for each of
     * the render modes are defined in the {@link PortletRenderer} interface.
     */
    public int getMode()
    {
        return mRenderContext.getMode();
    }

    /**
     * @return the URL that invoked this portlet.  Portlets use this to
     * (re)write HREFs in their response as these references must be absolute
     * paths since the client browser considers the current target the
     * portal. Note: request parameters are always returned as part of the URLs
     * querystring regardless of whether the request was received via an HTTP
     * GET or POST.
     */
    public String getRequestURL() throws java.lang.IllegalStateException
    {
        String request = getScheme() + "://" + getServerName()+ ":" +
            getServerPort() + mServletRequest.getRequestURI();
        String qs = mServletRequest.getQueryStringFromBody();
        if (qs != null && qs.length() > 0)
        {
            request += "?" + qs;
        }
        return request;
    }

    /**
     * @return an array of mime-types in preference order being requested.
     * I.e.  the portal may pass a list of valid mime-types it accepts for the
     * target device.  The Portlet must emit its response in one of these
     * mime-types.  The list is ordered by reference with the first being the
     * highest preference.
     */
    public String[] getAcceptContentTypes()
        throws java.lang.IllegalStateException
    {
        if(mAcceptContentTypes == null)
        {
            // if the portal does not send a list of accept content types with
            // the request, generate a list of supported content types.
            // This is a rare situation most likely related to misconfiguration
            // of the portal mid-tier. Sequence of types is based on checkin
            // report for a mobile bug for a similar issue elsewhere in the
            // product
            mAcceptContentTypes = new String[3];
            mAcceptContentTypes[0] = PortletRenderer.TYPE_HTML;
            mAcceptContentTypes[1] = PortletRenderer.TYPE_MOBILE;
            mAcceptContentTypes[2] = PortletRenderer.TYPE_XML;
        }

        return mAcceptContentTypes;
    }

    /**
     * @return the most preferred mime-type beign requested.
     */
    public String getPreferredContentType()
        throws java.lang.IllegalStateException
    {
        return getAcceptContentTypes()[0];
    }

    /**
     *  @return the reference to the portlet instance we are intended to render.
     *  This is the object that represents what we are rendering.
     */
    public PortletReference getPortletReference()
    {
        return mRef;
    }

    /**
     *  @return the portlet instance we are intended to render.
     */
    public PortletInstance getPortletInstance()
    {
        return mRef.getPortletInstance();
    }

    /**
     * Gets the user making this request.
     */
    public ProviderUser getUser()
    {
        return mUser;
    }

    /**
     *  If this is a mobile request iAS Wireless will have overwritten some
     *  of the client's original HTTP headers: specifically the Accept and the
     *  UserAgent header.  These routines give you access to the original values.
     *  Note: if the values haven't been overwritten then its assumed that the
     *  standard headers contain the original values.  In this situation these
     *  methods return the values for the standard headers.  In this way these
     *  methods always give you access to the header values as they originated
     *  from the client whether or not they have been overwritten.
     *
     *  @param name the header name you want the original value for.  I.e. if
     *  you pass "Accept" you will receive the original value of "Accept"
     *  whether its been overwritten or not.
     */
    public String getOriginalHeader(String name)
    {
        String h = null;
        if (name.equalsIgnoreCase(HttpCommonConstants.HTTP_ACCEPT_CONTENT_TYPE))
        {
            h = mServletRequest.getHeader(
                HttpCommonConstants.IASWIRELESS_ORIGINAL_ACCEPT_HEADER);
            if (h == null)
                h = mServletRequest.getHeader(HttpCommonConstants.HTTP_ACCEPT_CONTENT_TYPE);
            return h;
        }
        else if (name.equalsIgnoreCase(HttpCommonConstants.HTTP_USER_AGENT))
        {
            h = mServletRequest.getHeader(
                HttpCommonConstants.IASWIRELESS_ORIGINAL_USERAGENT_HEADER);
            if (h == null)
                h = mServletRequest.getHeader(HttpCommonConstants.HTTP_USER_AGENT);
            return h;
        }
        else
            return mServletRequest.getHeader(name);
    }

    /* convenience routines -- each can be accessed indirectly via the
       PortletInstance */

    /**
     * Gets the ProviderInstance associated with this request.  This is equivalent to
     * calling getPortletInstance().getProviderInstance().
     */
    public ProviderInstance getProviderInstance()
    {
        return mRef.getProviderInstance();
    }

    /**
     * Gets the PortletDefinition associated with this request.  This is equivalent to
     * calling getPortletInstance().getPortletDefinition().
     */
    public PortletDefinition getPortletDefinition()
    {
        return mRef.getPortletDefinition();
    }

    /**
     * The scheme we are running in.  Most commonly this will return
     * <b>http</b>.  Return value will be <i>null</i> if not running in any
     * scheme.  This can occur if running outside the context of a request as
     * in a batch program filling a cache on the server.
     */
    public String getScheme()
    {
        return mServletRequest.getScheme();
    }

    /**
     * The user agent that originated this request.  May be null if user-agent
     * is local.
     */
    public String getUserAgent()
    {
        return mServletRequest.getHeader(HttpCommonConstants.HTTP_USER_AGENT);
    }

    /**
     * Server name.  Used to reconstruct absolute paths back to server.
     */
    public String getServerName()
    {
        return mServletRequest.getServerName();
    }

    /**
     * Port used by this server. Used to reconstruct absolute paths back to
     * server.
     */
    public int getServerPort()
    {
        return mServletRequest.getServerPort();
    }

    /**
     * Returns the request parameter of the given name.  If there are
     * duplicates the first value is returned.  Returned Parameter values are
     * encoded using the RequestContext's encoding.  By default this is
     * ISO-8859-1.
     *
     * @param name the name of the parameter whose value is requested.
     */
    public String getParameter(String name)
    {
        return mServletRequest.getParameter(name);
    }

    /**
     * Returns the qualified request parameter of the given non-qualified name.
     * Because all portlets on a page share the same query string, portlets
     * will often fully qualify their parameter names to the portlet instance
     * to ensure the instance can distinguish its parameters from the others.
     * 
     * Utility methods are
     * provided in PortletRendererUtil and HttpPortletRendererUtil to construct
     * these fully qualified names.<p> Fully-qualified parameter names are
     * somewhat inconvenient to use because they are dynamic.  I.e. they are
     * isntance-based.  This method helps to overcome this inconvenience by
     * allowing you to retrieve the fully qualified parameter name value
     * passing only the non-qualified name.<p>
     *
     * As with getParameter(), if there are duplicates the first value is
     * returned.  Returned Parameter values are encoded using the
     * RequestContext's encoding.  By default this is ISO-8859-1.
     *
     * @param name the unqualfied name of the parameter whose value is
     * requested.
     * @return the value corresponding to the fully qualified name constructed
     * by prepending the portlet reference information to the passed parameter
     * name.
     */
    public String getQualifiedParameter(String name)
    {
        try
        {
            // the presence of the portlet page reference
            // will indicate that we were called by a 902+ PPE
            // otherwise it will throw an exception
            String pref = getRenderContext().getPortletPageReference();

            // 902+ PPE

            if (  getPortletDefinition().getPassAllUrlParams()==true 
               || PortletRenderer.MODE_SHOW != getMode())
            {
                // get by qualified name

                return getParameter(PortletRendererUtil.portletParameter(this, name));
            }
            else
            {
                // get by unqualified name

                return getParameter(name);
            }
        }
        catch( IllegalStateException e )
        {
            // 309 PPE - get by qualified name

            return getParameter(PortletRendererUtil.portletParameter(this, name));
        }
    }

    /**
     * Returns the values for the named request parameter.  Returned Parameter
     * values are encoded using the RequestContext's encoding.  By default this
     * is ISO-8859-1.
     *
     * @param name the name of the parameter whose value is requested.
     * @return an array of Strings; one element per value associated with this
     * parameter name.
     */
    public String[] getParameterValues(String name)
    {
        return mServletRequest.getParameterValues(name);
    }

    /**
     * Returns the qualified request parameter values of the given
     * non-qualified name.  Because all portlets on a page share the same query
     * string, portlets will often fully qualify their parameter names to the
     * portlet instance to ensure the instance can distinguish its parameters
     * from the others.
     * Utility methods are
     * provided in PortletRendererUtil and HttpPortletRendererUtil to construct
     * these fully qualified names.<p> Fully-qualified parameter names are
     * somewhat inconvenient to use because they are dynamic.  I.e. they are
     * isntance-based.  This method helps to overcome this inconvenience by
     * allowing you to retrieve the fully qualified parameter name value
     * passing only the non-qualified name.<p>
     *
     * Returned Parameter values are encoded using the RequestContext's
     * encoding.  By default this is ISO-8859-1.
     *
     * @param name the unqualfied name of the parameter whose value is
     * requested.
     * @return the values corresponding to the fully qualified name constructed
     * by prepending the portlet reference information to the passed parameter
     * name.
     */
    public String[] getQualifiedParameterValues(String name)
    {
        try
        {
            // the presence of the portlet page reference
            // will indicate that we were called by a 902+ PPE
            // otherwise it will throw an exception
            String pref = getRenderContext().getPortletPageReference();

            // 902+ PPE

            if (getPortletDefinition().getPassAllUrlParams()==true
               || PortletRenderer.MODE_SHOW != getMode())
            {
                // get by qualified name

                return getParameterValues(PortletRendererUtil.portletParameter(this, name));
            }
            else
            {
                // get by unqualified name

                return getParameterValues(name);
            }
        }
        catch( IllegalStateException e )
        {
            // 309 PPE - get by qualified name

            return getParameterValues(PortletRendererUtil.portletParameter(this, name));
        }
    }

    /**
     * Returns an enumeration containing the parameter names passed in this
     * request.
     */
    public Enumeration getParameterNames()
    {
        return mServletRequest.getParameterNames();
    }

    /**
     * Against a 309 Portal this returns an enumeration containing only those
     * parameter names that match this portlets fully qualified name.
     * The behaviour in 902 with passAllUrlParams==true is the same.
     * However in 902 with passAllUrlParams==false this returns all of the
     * parameter names passed to the portlet.
     *
     * In all cases you should use getParameter and getParameterValues with
     * the names returned in the enumeration in order to get value(s).
     */
    public Enumeration getQualifiedParameterNames()
    {
        String prefix;

        try
        {
            // the presence of the portlet page reference
            // will indicate that we were called by a 902+ PPE
            // otherwise it will throw an exception
            String pref = getRenderContext().getPortletPageReference();

            // 902+ PPE

            if (getPortletDefinition().getPassAllUrlParams()==true
               || PortletRenderer.MODE_SHOW != getMode())
            {
                // return qualified names (new format)

                prefix = "_piref" + pref + ".";
            }
            else
            {
                // return all parameter names

                return getParameterNames();
            }
        }
        catch( IllegalStateException e )
        {
            // 309 PPE - return qualified names (old format)

            prefix =  mRef.getProviderId() 
                   + "." + mRef.getPortletId() 
                   + "." + mRef.getInstanceName() 
                   + "."; 
        }


        // return parameter names that match the prefix

        Enumeration e = getParameterNames();
        Vector v = new Vector(20);
        while (e.hasMoreElements())
        {
            String s = (String)e.nextElement();
            if (s.startsWith(prefix))
            {
                v.addElement(s);
            }
        }
        return v.elements();
    }

    /**
     * Sets the contentType for this request's response.  After the first call
     * to any of the getWriter() methods the content type cannot be changed.
     */
    public void setContentType(String contentType)
    {
        if (contentType == null)
            return;

        // save only the content type - not the character set
        int delimIndex = contentType.indexOf(";");
        if(delimIndex == -1)
        {
            mContentType = contentType;
        }
        else
        {
            mContentType = contentType.substring(0, delimIndex);
        }
        // Write the content type and default character encoding through to the
        // 'real' servlet response
        mServletResponse.setContentType(mContentType + ";charset=" + mCharacterEncoding);

        // If the portlet render request is being accessed from inside a
        // 'wrapped' servlet response, write through the content type
        // (INCLUDING character encoding) to that wrapped response
        if(mOverriddenResponse != null)
        {
            mOverriddenResponse.setContentType(contentType);
        }

    }

    /**
     * Gets the current contentType for this request's response.  After the
     * first call to any of the getWriter() methods the content type cannot be
     * changed.
     */
    public String getContentType()
    {
        return mContentType;
    }

    /**
     * Returns a PrintWriter used to emit this request's response. If
     * applicable the passed contentType is used to prepare the response
     * (header).  <p>Note: <p>if <b>null</b> is passed the content type of the
     * response is set to "text/html". The character encoding used is based on
     * the default encoding of the request.
     * <p> After the first call to any of the getWriter() methods the content
     * type cannot be changed.
     */
    public PrintWriter getWriter(String contentType) throws java.io.IOException
    {
        setContentType(contentType);
        return getWriter();
    }

    /**
     * Returns a PrintWriter used to emit this request's response. If
     * applicable the passed contentType is used to prepare the response
     * (header).  <p>Note: <p>Uses the content type specified by calling
     * setContentType() or defaults the content type of the response to
     * "text/html".  The character encoding used is based on the default
     * encoding of the request.
     * <p> After the first call to any of the getWriter() methods the content
     * type and character encoding cannot be changed.
     */
    public PrintWriter getWriter() throws java.io.IOException
    {
        if (mOverriddenResponse != null)
        {
            return mOverriddenResponse.getWriter();
        }
        else
        {
            return mServletResponse.getWriter();
        }
    }

    /**
     * The preferred Java Locale to be used for this response.
     */
    public Locale getLocale()
    {
        return mServletRequest.getLocale();
    }

    /**
     * Returns an Enumeration of Locale objects indicating, in decreasing order
     * starting with the preferred locale, the locales that are acceptable to
     * the client based on the Accept-Language header.
     */
    public Enumeration getLocales()
    {
        return mServletRequest.getLocales();
    }

    /**
     * Returns the Portal's default Locale.  Used by portlets/personalization
     * managers to properly default to the correct language when preferred
     * languages aren't supported.  This value may be <code>null</code>.  When
     * <code>null</code> or when the portlet doesn't support this locale the
     * Portlet's default Locale should be used.  This is accessible via the
     * Portlet interface (<code>getDefaultLocale()</code>).<p> Alternatively,
     * you might consider using the <code>getDefaultLocale()</code> method in
     * the <code>PortletReference</code> interface.  This method is defined to
     * return the Portlet's default locale if the Portal hasn't passed one.
     */
    public Locale getDefaultLocale()
    {
        return mRef.getPortletInstance().getDefaultLocale();
    }

    /**
     * Returns the Provider resource associated with the Locale of this
     * request.  This resource contains locale specific strings for the title
     * bar.
     */
    public ResourceBundle getProviderResource()
    {
        if(mProviderResource == null)
        {
            Locale l = getLocale();
            // We have to use the the current thread's 'context class loader'
            // (i.e. the web app's class loader) to load the resource, so that
            // it can be picked up from within the web app
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            if (l != null)
            {
                mProviderResource = ResourceBundle.getBundle("oracle.portal.provider.v2.resource.ProviderBundle",
                                                             l, cl);
            }
            else
            {
                mProviderResource = ResourceBundle.getBundle("oracle.portal.provider.v2.resource.ProviderBundle",
                                                             Locale.getDefault(), cl);
            }

        }
        return mProviderResource;
    }


    /**
     * A session is an object that maintains state across requests. We use the
     * Servlet Session abstraction as it really is Http independent and is the
     * most commonly used session object in this model anyway.
     *
     */
    public ProviderSession getSession()
    {
        return ServletProviderSession.getAttachedSession(mServletRequest);
    }

    /**
     * A session is an object that maintains state across requests. We use the
     * Servlet Session abstraction as it really is Http independent and is the
     * most commonly used session object in this model anyway.
     *
     */
    public Object[] getCookies()
    {
        return mServletRequest.getCookies();
    }


    /**
     * Data can be passed between object in a request via attr/value pairs hung
     * off the RequestContext.  Clients are encouraged to prefix attribute
     * names with fully qualified class names to avoid collisions.  E.g.
     * setAttribute("oracle.jdeveloper.chart.color", chartColor);
     *
     * @param name the name of the attribute.
     * @param value the value for the attribute.
     */
    public void setAttribute(String name, Object value)
    {
        mServletRequest.setAttribute(name, value);
    }


    /**
     * Data can be passed between object in a request via attr/value pairs hung
     * off the RequestContext.  Clients are encouraged to prefix attribute
     * names with fully qualified class names to avoid collisions.  E.g.
     * setAttribute("oracle.jdeveloper.chart.color", chartColor);
     *
     * @param name the name of the attribute.
     * @return value the value for the attribute.
     */
    public Object getAttribute(String name)
    {
        if(mOverriddenResponse != null &&
           name.equals(HttpCommonConstants.SERVLET_RESPONSE))
        {
            return mOverriddenResponse;
        }
        return mServletRequest.getAttribute(name);
    }

    /**
     * Get the names of the attributes of this
     * <code>ServletPortletRenderRequest</code> as an <code>Enumeration</code>
     * of <code>String</code>s.
     */
    public java.util.Enumeration getAttributeNames()
    {
        return mServletRequest.getAttributeNames();
    }

    /**
     * Removes an existing attribute.
     *
     * @param name the name of the attribute.
     */
    public void removeAttribute(String name)
    {
        mServletRequest.removeAttribute(name);
    }

    /**
     * Returns the character set that was used when the parameters of the
     * request were encoded.  Used when encoding parameters for inclusion in a
     * URL
     */
    public String getURLCharSet()
    {
        return mServletRequest.getURLCharSet();
    }

    // Used to set the response object from which the getWriter() methods
    // retrieve the PrintWriter.
    protected void setResponse(HttpServletResponse response)
    {
        mOverriddenResponse = response;
    }

    // Resets the response from which the getWriter() methods retrieve the
    // PrintWriter object back to the original response object.
    protected void resetResponse()
    {
        mOverriddenResponse = null;
    }

    protected void setAcceptContentTypes(HttpServletRequest request)
    {
        String ac = request.getHeader(HttpCommonConstants.HTTP_ACCEPT_CONTENT_TYPE);
        if (ac == null || ac.length() == 0)
        {
            return;
        }
        StringTokenizer st = new StringTokenizer(ac, ",");
        mAcceptContentTypes = new String[st.countTokens()];
        int i = 0;
        while (st.hasMoreTokens())
        {
            mAcceptContentTypes[i++] = st.nextToken();
        }
    }

    protected void setDefaultCharacterEncoding(HttpServletRequest request)
    {
        // Note that the character encoding we use to communicate back to the
        // portal is never exposed to the portlet. Non servlet/jsp portlets
        // only have access to a writer object and servlet/jsp based
        // implementations write into our response wrapper (thus allowing us to
        // convert to this character encoding if necessary).

        // Use the x-oracle-portal-content-charset if present. If not, just
        // leave to the default value of UTF8.
        String portalCharset =
            request.getHeader(HttpCommonConstants.HTTP_PORTAL_CONTENT_CHARSET);
        if (portalCharset != null)
        {
            mCharacterEncoding = portalCharset;
        }
    }

}
