// JigsawHttpServletRequest.java
// $Id: JigsawHttpServletRequest.java,v 1.46 1999/06/09 09:44:21 bmahe Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package org.w3c.jigsaw.servlet;

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

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

import org.w3c.util.*;
import org.w3c.jigsaw.http.*;
import org.w3c.jigsaw.forms.URLDecoder;
import org.w3c.jigsaw.forms.URLDecoderException;
import org.w3c.jigsaw.resources.VirtualHostResource;
import org.w3c.www.http.*;
import org.w3c.www.mime.*;
import org.w3c.jigsaw.auth.AuthFilter; // for auth infos access

import org.w3c.tools.resources.*;

class HeaderNames implements Enumeration {
    // The HeaderDescription enumeration
    Enumeration e = null;

    public boolean hasMoreElements() {
	return e.hasMoreElements();
    }

    public Object nextElement() {
	HeaderDescription d = (HeaderDescription) e.nextElement();
	return d.getName();
    }

    HeaderNames(Enumeration e) {
	this.e = e ;
    }

}

/**
 *  @author Alexandre Rafalovitch <alex@access.com.au>
 *  @author Anselm Baird-Smith <abaird@w3.org>
 *  @author Benoit Mahe <bmahe@sophia.inria.fr>
 */

public class JigsawHttpServletRequest implements HttpServletRequest {

    /**
     * The InputStream state codes.
     */

    /**
     * The initial state of the request InputStream 
     */
    private final static int STREAM_STATE_INITIAL = 0;

    /**
     * One reader has been created and is probably used.
     */
    private final static int STREAM_READER_USED = 1;

    /**
     * The input stream is used
     */
    private final static int INPUT_STREAM_USED = 2;

    /**
     * The inputstream state
     */
    private int stream_state = STREAM_STATE_INITIAL;

    public final static 
	String STATE_PARAMETERS = "org.w3c.jigsaw.servlet.stateParam";

    private static MimeType type = MimeType.APPLICATION_X_WWW_FORM_URLENCODED ;
    /** 
     * The initial request.
     */
    private Request request = null;
    /**
     * The attached servlet.
     */
    private Servlet servlet = null;
    /**
     * The lazyly computed queryParameters hashtable.
     */
    private Hashtable queryParameters = null;

    protected JigsawHttpServletResponse response = null;

    protected JigsawHttpSession httpSession = null;

    protected JigsawHttpSessionContext sessionContext = null;

    protected String requestedSessionID = null;

    private Hashtable mergeParameters(Hashtable source, Hashtable dest) {
	if (source == null)
	    return dest;
	if (dest != null) {
	    Enumeration e = source.keys();
	    while (e.hasMoreElements()) {
		String name = (String)e.nextElement();
		Object value = dest.get(name);
		if (value == null)
		    dest.put(name, source.get(name));
		else  if (value instanceof String[]) {
		    String oldValues [] = (String[])value;
		    String newValues [] = new String[oldValues.length+1];
		    System.arraycopy(oldValues,0,newValues,0,
				     oldValues.length);
		    newValues[oldValues.length] = (String)source.get(name);
		    dest.put(name,newValues);
		} else {
		    String newValues [] = new String[2];
		    newValues[0] = (String)source.get(name);
		    newValues[1] = (String)value;
		    dest.put(name,newValues);
		}
	    }
	    return dest;
	} else {
	    return source;
	}
    }

    private synchronized void prepareQueryParameters() {
	if( queryParameters != null )
	    return;
	Hashtable postParameters = null;
	// What kinf of parameters are we expecting ?
	if ( request.getMethod().equals("POST") ) {
	    postParameters = new Hashtable(2);
	    // POSTed parameters, check content type:
	    if ((! request.hasContentType())
		|| (type.match(request.getContentType()) < 0) ) 
		return;
	    // Get and decode the request entity:
	    URLDecoder dec = null;
	    try {
		InputStream in = request.getInputStream() ;
		// Notify the client that we are willing to continue
		Client client = request.getClient();
		if ( client != null ) 
		    client.sendContinue();
		dec = new URLDecoder (in, false);
		postParameters = dec.parse () ;
	    } catch (URLDecoderException e) {
		postParameters = null;
	    } catch (IOException ex) {
		postParameters = null;
	    }
	}
	// URL encoded parameters:
	String query = request.getQueryString();
	if (query != null) {
	    Reader qis = new StringReader(query);
	    try {
		queryParameters = new URLDecoder(qis, false).parse();
	    } catch (Exception ex) {
		throw new RuntimeException("Java implementation bug.");
	    }
	}
	queryParameters = mergeParameters(postParameters, queryParameters);
	// state parameters
	Hashtable param = (Hashtable)request.getState(STATE_PARAMETERS);
	queryParameters = mergeParameters(param, queryParameters);
    }

    protected String getURLParameter(String name) {
	Hashtable urlParameters = null;
	String query = request.getQueryString();
	if (query != null) {
	    Reader qis = new StringReader(query);
	    try {
		urlParameters = new URLDecoder(qis, false).parse();
		return (String) urlParameters.get(name);
	    } catch (Exception ex) {
		throw new RuntimeException("Java implementation bug.");
	    }
	}
	return null;
    }

    /**
     * Return the Charset parameter of content type
     * @return A String instance
     */
    public String getCharacterEncoding() {
	org.w3c.www.mime.MimeType type = request.getContentType();
	if ((type != null) && (type.hasParameter("charset"))) {
	    return type.getParameterValue("charset");
	}
	return "ISO-8859-1";
    }

    /**
     * ServletRequest implementation - Get the length of request data.
     * @return An int, or <strong>-1</strong>.
     */

    public int getContentLength()
    {
	return request.getContentLength();
    }

    /**
     * ServletRequest implementation - Get the type of the request's body.
     * @return A String encoded mime type, or <strong>null</strong>.
     */

    public String getContentType()
    {
	org.w3c.www.mime.MimeType t = request.getContentType();
	return (t == null) ? null : t.toString();
    }
    
    /**
     * ServletRequest implementation - Get the protocol of that request.
     * @return A String encoded version of the protocol.
     */

    public String getProtocol()
    {
	return request.getVersion();
    }
    
   /**
    * ServletRequest implementation - Get the name of queried server.
    * @return Name of server, as a String.
    */

   public String getServerName()
   {
       if (request.isProxy()) {
	   String host = request.getHost();
	   if (host != null)
	       return host.substring(0, host.lastIndexOf(':'));
       }
       return request.getClient().getServer().getHost();
   }

   /**
    * ServletRequest implementation - Get the port of queried server.
    * @return A port number (int).
    */

   public int getServerPort()
   {
       if (request.isProxy()) {
	   String host = request.getHost();
	   if (host != null) {
	       int idx = host.lastIndexOf(':');
	       if (idx == -1)
		   return 80;
	       return Integer.parseInt(host.substring(idx+1));
	   }
       }
       return request.getClient().getServer().getLocalPort();
   }

   /**
    * ServletRequest implementation - Get the IP address of requests's sender.
    * @return Numeric IP address, as a String.
    */

    public String getRemoteAddr()
    {
	return request.getClient().getInetAddress().getHostAddress();
    }

    /**
     * ServletRequest implementation - FQDN of request's sender.
     * @return Name of client's machine (FQDN).
     */

    public String getRemoteHost()
    {
	return request.getClient().getInetAddress().getHostName();
    }

    /**
     * ServletRequest implementation - Get real path.
     * Jigsaw realy has no notion of <em>translation</em> stricto
     * sensu (it has much better in fact ;-). This is a pain here.
     * @return the real path.
     * @deprecated since jsdk1.2
     */

    public String getRealPath(String name) {
	httpd             server  = request.getClient().getServer();
	ResourceReference rr_root = server.getRootReference();
	try {
	    FramedResource root = (FramedResource)rr_root.lock();
	    if (root instanceof VirtualHostResource) {
		//backward to the virtual host resource
		ResourceReference rr  = request.getTargetResource();
		ResourceReference rrp = null;
		while (true) {
		    try {
			FramedResource res = (FramedResource)rr.lock();
			rrp = res.getParent();
			if ((rrp == rr_root) || (rrp == null)) {
			    if (res.definesAttribute("directory")) {
				File dir = 
				    (File)res.getValue("directory", null);
				if (dir != null)
				    return (new File(dir, name)).
					getAbsolutePath();
			    }
			    return null;
			}
		    } catch (InvalidResourceException ex) {
			return null;
		    } finally {
			rr.unlock();
		    }
		    rr = rrp;
		}
	    } else if (root.definesAttribute("directory")) {
		File dir = (File)root.getValue("directory", null);
		if (dir != null)
		    return (new File(dir, name)).getAbsolutePath();
	    }
	    return null;
	} catch (InvalidResourceException ex) {
	    return null;
	} finally {
	    rr_root.unlock();
	}
    }

    protected ServletInputStream is = null;

    /**
     * Returns an input stream for reading binary data in the request body. 
     * @exception IllegalStateException if getReader has been called on 
     * this same request. 
     * @exception IOException on other I/O related errors. 
     * @see JigsawHttpServletRequest#getReader
     */    
    public ServletInputStream getInputStream()
	throws IOException
    {
	if (stream_state == STREAM_READER_USED)
	    throw new IllegalStateException("Reader used");
	stream_state = INPUT_STREAM_USED;
	return getJigsawInputStream();
    }
    
    /**
     * @exception IOException if an IO error occurs
     */ 
    protected ServletInputStream getJigsawInputStream()
	throws IOException
    {
	// If alredy computed return:
	if ( is != null )
	    return is;
	// Built it:
	InputStream stream = null;
	if ((stream = request.getInputStream()) == null)
	    stream = new ContentLengthInputStream(null, 0);
	return is = new JigsawServletInputStream(stream);
    }

    /**
     * ServletRequest implementation - Get a parameter value.
     * @return The String encoded value for the parameter.
     */

    public String getParameter(String name)
    {
	prepareQueryParameters();
	if ( queryParameters != null ) {
	    Object value = queryParameters.get(name);
	    if (value instanceof String[])
		return ((String[])value)[0];
	    else return (String)value;
	}
	else
	    return null;
    }

    /**
     * ServletRequest implementation - Get the parameters value.
     * @return The String array encoded value for the parameter.
     */
    
    public String[] getParameterValues(String parameter) {
	Vector V = new Vector(23);
	prepareQueryParameters();
	if (queryParameters == null) 
	    return null;
	Object value = queryParameters.get(parameter);
	if (value == null) return null;
	if (value instanceof String[])
	    return (String[])value;
	else {
	    String [] parameterValues = new String[1];
	    parameterValues[0] = (String)value;
	    return parameterValues;
	}
    }

    /**
     * ServletRequest implementation - List available parameters.
     * @return An enumeration of parameter names.
     */
    
    public Enumeration getParameterNames()
    {
	prepareQueryParameters();
	return ((queryParameters == null)
		? new EmptyEnumeration()
		: queryParameters.keys());
    }

    /**
     * ServletRequest implementation - Get an attribute of the request.
     * This closely match Jigsaw's notion of request state.
     * @param name The name of the attribute.
     * @return An object that gives the value of the attribute.
     */

    public Object getAttribute(String name) {
	return request.getState(name);
    }

    public void setAttribute(String name, Object object) {
	request.setState(name, object);
    }

    public Enumeration getAttributeNames() {
	return request.getStateNames();
    }

    /**
     * HttpServletRequest implementation - Get the request's method.
     * @return A String instance.
     */

    public  String getMethod()
    {
	return request.getMethod();
    }

    /**
     * HttpServletRequest implementation - Get the request's path info.
     * @return A String instance or <strong>null</strong>.
     */

    public  String getPathInfo()
    {
	return (String) request.getState(ServletWrapperFrame.STATE_EXTRA_PATH);
    }
    
    /**
     * HttpServletRequest implementation - Get the request's path translated.
     * @return A String instance or <strong>null</strong>.
     */
    
    public  String getPathTranslated()
    {
	String pathinfo = getPathInfo();
	if ( pathinfo != null )
	    return getRealPath(pathinfo);
	return null;
    }

    /**
     * HttpServletRequest implementation - Get the request's query string.
     * @return A String instance or <strong>null</strong>.
     */

    public  String getQueryString()
    {
	return request.getQueryString();
    }
    
    /**
     * HttpServletRequest implementation - Get the request's user (if any).
     * @return A String instance or <strong>null</strong>.
     */

    public String getRemoteUser()
    {
	return (String) request.getState(AuthFilter.STATE_AUTHUSER);
    }
    
    /**
     * HttpServletRequest implementation - Get the request's auth method.
     * @return A String instance or <strong>null</strong>.
     */

    public String getAuthType() {
	return (String) request.getState(AuthFilter.STATE_AUTHTYPE);
    }

    /**
     * HttpServletRequest implementation - Get a request header as a String.
     * @return A String instance or <strong>null</strong>.
     */

    public String getHeader(String name) {
	return request.getValue(name);
    }

    /**
     * HttpServletRequest implementation - Get a request header as an int.
     * @return An int, or <strong>-1</strong>.
     */

    public int getIntHeader(String name) {
	HeaderValue v = request.getHeaderValue(name);
	if ( v != null ) {
	    Object o = v.getValue();
	    if ((o != null) && (o instanceof Integer))
		return ((Integer) o).intValue();
	}
	return -1;
    }

    /**
     * HttpServletRequest implementation - Get a request header as an date.
     * @return An long (as a number of milliseconds), or <strong>-1</strong>.
     */

    public long getDateHeader(String name) {
	HeaderValue v = request.getHeaderValue(name, null);
	if ( v != null ) {
	    Object o = v.getValue();
	    if ((o != null) && (o instanceof Long)) 
		return ((Long) o).longValue();
	}
	return (long) -1;
    }

    /**
     * HttpServletRequest implementation - Get a all header names.
     * @return An enumeration.
     */

    public Enumeration getHeaderNames() {
	return new HeaderNames(request.enumerateHeaderDescriptions());
    }

    /**
     * Gets, from the first line of the HTTP request, 
     * the part of this request's URI that is to the left of any query string.
     */
    public  String getRequestURI()
    {
	String uri = null;
	//fixme test
	if (request.isProxy()) {
	    uri = request.getURL().toExternalForm();
	} else {
	    uri = request.getURLPath();
	}
	if (request.hasQueryString()) {
	    String query = request.getQueryString();
	    int idx = uri.lastIndexOf(query);
	    uri = uri.substring(0, idx-1);
	}
	return uri;
    }
    
    /**
     * Gets the part of this request's URI that refers to the servlet 
     * being invoked. Analogous to the CGI variable SCRIPT_NAME. 
     */
    public  String getServletPath()
    {
	ResourceReference rr = request.getTargetResource();
	try {
	    return rr.lock().getURLPath();
	} catch (InvalidResourceException ex) {
	    return null;
	} finally {
	    rr.unlock();
	}
    }
    
    /**
     * @return the scheme of the URL used in this request, for example "http",
     * "https", or "ftp". Different schemes have different rules
     * for constructing URLs, as noted in RFC 1738. The URL used to create 
     * a request may be reconstructed using this scheme, the server name 
     * and port, and additional information such as URIs.
     */
    public String getScheme() {
	return request.getURL().getProtocol();
    }

    /**
     * Gets the array of cookies found in this request.
     * @return the array of cookies found in this request or
     * <strong>null</strong> if there is no cookie.
     */
    public Cookie[] getCookies() {
	HttpCookieList cookielist = request.getCookie();
	Cookie[] Scookies = null;
	if (cookielist != null) {
	    HttpCookie[] cookies = cookielist.getCookies();
	    Scookies = new Cookie[cookies.length];
	    for (int i = 0 ; i < cookies.length ; i++ ) {
		Scookies[i] = convertCookie(cookies[i]);
	    }
	}
	return Scookies;
    }
  
    protected Cookie convertCookie(HttpCookie httpCookie) {
	Cookie cookie = new Cookie(httpCookie.getName(),
				   httpCookie.getValue());
	String val = null;
	if ((val = httpCookie.getDomain()) != null)
	    cookie.setDomain(val);
	if ((val = httpCookie.getPath()) != null)
	    cookie.setPath(val);
	cookie.setVersion(httpCookie.getVersion());
	return cookie;
    }

    protected String getRequestedSessionIdFromCookie() {
	HttpCookieList cookielist = request.getCookie();
	if (cookielist != null) {
	    HttpCookie httpCookie = 
		request.getCookie().getCookie(getCookieName());
	    if (httpCookie != null)
		return httpCookie.getValue();
	}
	return null;
    }

    protected String getRequestedSessionIdFromURL() {
	return getURLParameter(getCookieName());
    }

    /**
     * Gets the session id specified with this request. This may differ 
     * from the actual session id. For example, if the request specified an
     * id for an invalid session, then this will get a new session with a 
     * new id. 
     * @return the session id specified by this request, or null if the 
     * request did not specify a session id.
     */
    public String getRequestedSessionId() {
	if (requestedSessionID == null) {
	    requestedSessionID = getRequestedSessionIdFromCookie();
	    if (requestedSessionID == null)
		requestedSessionID = getRequestedSessionIdFromURL();
	}
	return requestedSessionID;
    }

    protected synchronized JigsawHttpSessionContext getSessionContext() {
	return sessionContext;
    }

    /**
     * Gets the current valid session associated with this request, if create
     * is false or, if necessary, creates a new session for the request, if 
     * create is true. 
     * @return the session associated with this request or null if create 
     * was false and no valid session is associated with this request. 
     */
    public HttpSession getSession(boolean create) {
	if (httpSession == null) {
	    httpSession = (JigsawHttpSession)
		getSession(getRequestedSessionId());
	    if (httpSession != null) // the client join the session
		httpSession.setNoMoreNew();
	}
	if (httpSession == null & create) {
	    httpSession = new JigsawHttpSession(getSessionContext(), 
						createCookie());
	    response.addCookie(httpSession.getCookie());
	} else if (httpSession != null) {
	    httpSession.setLastAccessedTime();
	    if (! httpSession.isValid()) {
		httpSession = new JigsawHttpSession(getSessionContext(), 
						    createCookie());
		response.addCookie(httpSession.getCookie());
	    }
	}
	return httpSession;
    }

    /**
     * Gets the current valid session associated with this request.
     * @return the session associated with this request.
     */
    public HttpSession getSession() {
	return getSession(true);
    }

    protected String getCookieName() {
	 ObservableProperties props =
	     request.getClient().getServer().getProperties();
	 return props.getString(ServletProps.SERVLET_COOKIE_NAME,
				ServletProps.DEFAULT_COOKIE_NAME);
    }


    protected Cookie createCookie() {
	ObservableProperties props = 
	    request.getClient().getServer().getProperties();
	String name     = props.getString(ServletProps.SERVLET_COOKIE_NAME,
					  ServletProps.DEFAULT_COOKIE_NAME);
	String path     = props.getString(ServletProps.SERVLET_COOKIE_PATH,
					  "/");
	String domain   = props.getString(ServletProps.SERVLET_COOKIE_DOMAIN,
					  null);
	String comment  = props.getString(ServletProps.SERVLET_COOKIE_COMMENT,
					  null);
	int maxage      = props.getInteger(ServletProps.SERVLET_COOKIE_MAXAGE,
					   86400);
	boolean secure  = props.getBoolean(ServletProps.SERVLET_COOKIE_SECURE,
					   false);

	Cookie cookie = new Cookie(name, null);
	cookie.setPath(path);
	cookie.setMaxAge(maxage);
	if ((comment != null) && (comment.length() > 0))
	    cookie.setComment(comment);
	if ((domain != null) && (domain.length() > 0))
	    cookie.setDomain(domain);
	cookie.setSecure(secure);
	return cookie;
    }

    protected HttpSession getSession(String sessionId) {
	if (sessionId != null)
	    return getSessionContext().getSession(sessionId);
	return null;
    }
  
    /**
     * Checks whether this request is associated with a session that is valid 
     * in the current session context. If it is not valid, the requested
     * session will never be returned from the getSession method. 
     * @return true if this request is assocated with a session that is valid 
     * in the current session context. 
     */
    public boolean isRequestedSessionIdValid() {
	JigsawHttpSession session = (JigsawHttpSession) 
	    getSession(getRequestedSessionId());
	if (session == null)
	    return false;
	return (session.isValid());
    }

    /**
     * Checks whether the session id specified by this request came in as 
     * a cookie. (The requested session may not be one returned by the 
     * getSession method.)
     * @return true if the session id specified by this request came 
     * in as a cookie; false otherwise 
     */
    public boolean isRequestedSessionIdFromCookie() {
	return (getRequestedSessionIdFromCookie() != null);
    }

    /**
     * Checks whether the session id specified by this request came in as 
     * part of the URL. (The requested session may not be the one returned 
     * by the getSession method.)
     * @return true if the session id specified by the request for this 
     * session came in as part of the URL; false otherwise
     * @deprecated since jsdk2.1
     */
    public boolean isRequestedSessionIdFromUrl() {
	return (getRequestedSessionIdFromURL() != null);
    }

    /**
     * Checks whether the session id specified by this request came in as 
     * part of the URL. (The requested session may not be the one returned 
     * by the getSession method.)
     * @return true if the session id specified by the request for this 
     * session came in as part of the URL; false otherwise
     */
    public boolean isRequestedSessionIdFromURL() {
	return (getRequestedSessionIdFromURL() != null);
    }

    protected BufferedReader reader = null;

    /**
     * Returns a buffered reader for reading text in the request body. 
     * This translates character set encodings as appropriate.
     * @exception UnsupportedEncodingException if the character set encoding 
     * is unsupported, so the text can't be correctly decoded. 
     * @exception IllegalStateException if getInputStream has been called on 
     * this same request. 
     * @exception IOException on other I/O related errors. 
     * @see JigsawHttpServletRequest#getInputStream 
     */
    public BufferedReader getReader()
	throws IOException
    {
	if (stream_state == INPUT_STREAM_USED)
	    throw new IllegalStateException("Input Stream used");
	stream_state = STREAM_READER_USED;
	if (reader == null) {
	    String enc[] = request.getContentEncoding();
	    InputStream is = getJigsawInputStream();
	    if (enc != null) {
		InputStreamReader isr = null;
		for (int i=0; i < enc.length; i++) {
		    try {
			isr = new InputStreamReader(is, enc[i]);
			break;
		    } catch (UnsupportedEncodingException ex) {
			// not a valid encoding, skip it
			isr = null;
		    }
		}
		if (isr != null)
		    reader = new BufferedReader(isr);
		else
		    reader = new BufferedReader(new InputStreamReader(is));
	    } else {
		reader = new BufferedReader(new InputStreamReader(is));	
	    }
	}
	return reader;
    }

    /**
     * @@FIXME
     */
    protected Request getRequest() {
	return request;
    }

    JigsawHttpServletRequest(Servlet servlet, 
			     Request request, 
			     JigsawHttpServletResponse response,
			     JigsawHttpSessionContext sessionContext) {
	this.servlet        = servlet;
	this.request        = request;
	this.response       = response;
	this.sessionContext = sessionContext;
    }

}
