/*
  Copyright (C) 2006 Helge Hess

  This file is part of JOPE.

  JOPE 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, or (at your option) any
  later version.

  JOPE 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.

  You should have received a copy of the GNU Lesser General Public
  License along with JOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

package org.opengroupware.jope.servlets;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.appserver.core.WOCookie;
import org.opengroupware.jope.appserver.core.WORequest;

/*
 * We need to track the servlet response because the Servlet API service()
 * method already has the pointer to the response. Which in turn is needed
 * for content streaming.
 */

public class WOServletRequest extends WORequest {
  private static final Log servLog = LogFactory.getLog("WOServletAdaptor");

  HttpServletRequest  sRequest;
  HttpServletResponse sResponse;
  protected static int maxRequestSize = 64 * 1024 * 1024; /* 64MB  */
  protected static int maxRAMFileSize = 256 * 1024;       /* 256KB */
  protected static File tmpFileLocation = 
    new File(System.getProperty("java.io.tmpdir"));
  
  public WOServletRequest(HttpServletRequest _rq, HttpServletResponse _r) {
    super();
    this.init(_rq, _r);
  }
  
  protected void init(HttpServletRequest _rq, HttpServletResponse _r) {
    this.init(_rq.getMethod(), _rq.getRequestURI(), _rq.getProtocol(),
        null /* headers  */,
        null /* contents */,
        null /* userInfo */);

    this.sRequest  = _rq;
    this.sResponse = _r;

    this.loadHeadersFromServletRequest(_rq);
    this.loadCookies();
    
    // Note: ServletFileUpload.isMultipartContent() is deprecated
    String contentType = this.headerForKey("content-type");
    if (contentType != null) contentType = contentType.toLowerCase();
    
    if (contentType != null && contentType.startsWith("multipart/form-data")) {
      FileItemFactory factory = 
        new DiskFileItemFactory(maxRAMFileSize, tmpFileLocation);
      
      ServletFileUpload upload = new ServletFileUpload(factory);
      upload.setSizeMax(maxRequestSize);
      
      List items = null;
      try {
        items = upload.parseRequest(_rq);
      }
      catch (FileUploadException e) {
        items = null;
        log.error("failed to parse upload request", e);
      }
      
      this.loadFormValuesFromFileItems(items);
    }
    else if (contentType != null && 
             contentType.startsWith("application/x-www-form-urlencoded"))
    {
      /* Note: we need to load form values first, because when we load the
       *       content, we need to process them on our own.
       */
      this.loadFormValuesFromRequest(_rq);
      
      /* Note: since we made the Servlet container process the form content,
       *       we can't load any content ...
       */
    }
    else {
      /* Note: we need to load form values first, because when we load the
       *       content, we need to process them on our own.
       */
      this.loadFormValuesFromRequest(_rq);
      
      // TODO: make this smarter, eg transfer large PUTs to disk
      this.loadContentFromRequest(_rq);
    }
  }
  
  public void dispose() {
    if (this.formValues != null) {
      for (Object value: this.formValues.values()) {
        if (value instanceof FileItem)
          ((FileItem)value).delete();
      }
    }
    
    super.dispose();
  }
  
  /* loading WORequest data from the Servlet */
  
  protected void loadHeadersFromServletRequest(HttpServletRequest _rq) {
    Enumeration e = _rq.getHeaderNames();
    while (e.hasMoreElements()) {
      String name = (String)e.nextElement();
      
      Enumeration ve = _rq.getHeaders(name);
      name = name.toLowerCase();
      while (ve.hasMoreElements()) {
        String v = (String)ve.nextElement();
        this.appendHeader(v, name);
      }
    }
  }
  
  protected void loadCookieFromHeaderString(String _v) {
    WOCookie cookie = WOCookie.parseCookieString(_v);
    if (cookie == null) {
      servLog.error("could not parse cookie: '" + _v + "'");
      return;
    }
    
    this.addCookie(cookie);
  }
  
  protected void loadCookies() {
    /*
     * Note: this is loading using the 'Cookie' syntax. That is, ';' separates
     *       individual cookies, not cookie options.
     */
    for (String v: this.headersForKey("cookie")) {
      String[] cookieStrings = v.split(";");
      for (int i = 0; i < cookieStrings.length; i++)
        this.loadCookieFromHeaderString(cookieStrings[i].trim());
    }
  }
  
  protected IOException loadContentFromStream(int len, InputStream _in) {
    // TODO: directly load into contents buffer w/o copying
    // TODO: deal with requests which have no content-length?
    // TODO: add support for streamed input?
    
    if (len < 1)
      return null; /* no content, no error */

    if (log.isInfoEnabled()) {
      log.info("load content, length " + len + ", type " +
               this.headerForKey("content-type"));
    }
    
    int pos = 0;
    try {
      byte[] buffer = new byte[4096];
      int gotlen;
      
      this.contents = new byte[len];
      
      while ((gotlen = _in.read(buffer)) != -1) {
        System.arraycopy(buffer, 0, this.contents, pos, gotlen);
        pos += gotlen;
      }
    }
    catch (IOException ioe) {      
      // TODO: what to do with failed requests?
      return ioe;
    }
    
    if (pos != len)
      log.warn("did read less bytes than expected (" + pos + " vs " + len +")");
    
    return null; /* everything allright */
  }
  
  protected void loadFormValuesFromRequest(HttpServletRequest _rq) {
    servLog.debug("loading form values from Servlet request ...");
    
    this.formValues = new HashMap<String, Object[]>(16);
    
    Enumeration e = _rq.getParameterNames();
    while (e.hasMoreElements()) {
      String   name = (String)e.nextElement();
      String[] vals = _rq.getParameterValues(name);
      
      // TODO: do we want to support type suffixes here? Like:
      //         balance:int=10
      //       => put("balance", new Integer(10))
      
      // TODO: do we need to morph the String[] into an Object[]?
      this.formValues.put(name, vals);
    }
  }
  
  protected void loadFormValuesFromFileItems(List _items) {
    this.formValues = new HashMap<String, Object[]>(16);
    
    if (_items == null)
      return;
    
    for (Object item: _items) {
      FileItem fileItem = (FileItem)item;
      String   name  = fileItem.getFieldName();
      Object   value = null;
      
      if (fileItem.isFormField())
        value = fileItem.getString();
      else
        value = fileItem; // the WOFileUpload will deal directly with this
      
      /* check whether we need to add a value */
      
      Object[] vals = this.formValues.get(name);
        
      if (vals == null)
        this.formValues.put(name, new Object[] { value });
      else {
        Object[] newVals = new Object[vals.length + 1];
        System.arraycopy(vals, 0, newVals, 0, vals.length);
        newVals[vals.length] = value;
        this.formValues.put(name, newVals);
        newVals = null;
        vals    = null;
      }
    }
  }
  
  protected void loadContentFromRequest(HttpServletRequest _rq) {
    servLog.debug("loading content from Servlet request ...");
    
    InputStream is = null;
    
    try {
      is = _rq.getInputStream();
    }
    catch (IOException ioe) {
      // TODO: could be a real exception? we might need to compare content-len?
    }
    
    if (is != null)
      this.loadContentFromStream(_rq.getIntHeader("content-length"), is);
  }
  
  /* accessors */
  
  public HttpServletRequest servletRequest() {
    return this.sRequest;
  }
  public HttpServletResponse servletResponse() {
    return this.sResponse;
  }
  
  /* streaming support */
  
  public OutputStream outputStream() {
    if (this.sResponse == null)
      return null;
    
    try {
      return this.sResponse.getOutputStream();
    }
    catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      return null;
    }
  }
}
