/*
  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.ArrayList;
import java.util.Date;
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;
import org.opengroupware.jope.appserver.core.WOResponse;
import org.opengroupware.jope.foundation.NSException;
import org.opengroupware.jope.foundation.NSJavaRuntime;

/*
 * 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?
      log.warn("could not read request ...");
      return ioe;
    }
    
    if (pos != len)
      log.warn("did read less bytes than expected (" + pos + " vs " + len +")");
    
    return null; /* everything allright */
  }
  
  protected static final Object[] emptyObjectArray = new Object[0];
  
  @SuppressWarnings("deprecation")
  protected Object formatFormValue(String _s, String _format) {
    // http://www.faqs.org/docs/ZopeBook/ScriptingZope.html
    // TODO: we should use java.text.Format in an extensible way
    
    if ("int".equals(_format))
      return _s != null ? Integer.valueOf(_s) : null;

    if ("boolean".equals(_format))
      return _s != null ? NSJavaRuntime.boolValueForObject(_s) : Boolean.FALSE;

    if ("long".equals(_format))
      return _s != null ? Long.valueOf(_s) : null;

    if ("float".equals(_format))
      return _s != null ? Float.valueOf(_s) : null;

    if ("string".equals(_format))
      return _s;

    if ("text".equals(_format)) // normalize line endings
      return _s != null ? _s.replace("\r", "") : null;
      
    if ("date".equals(_format)) {
      // TODO: improve date support
      return _s != null ? new Date(_s) : null;
    }
    
    if ("lines".equals(_format))
      return _s != null ? _s.split("\n") : null;

    if ("tokens".equals(_format))
      // TODO: fixme, split on spaces
      return _s != null ? _s.split("\n") : null;

    if ("required".equals(_format)) {
      // TODO: include name
      // TODO: raise a better exception 
      String c = _s != null ? _s.trim() : null;
      if (c == null || c.length() == 0)
        throw new NSException("missing required field");
      return c;
    }

    if ("ignore_empty".equals(_format)) {
      String c = _s != null ? _s.trim() : null;
      return (c == null || c.length() == 0) ? null : c;
    }
    
    log.error("unsupported request value format: " + _format);
    
    return _s;
  }
  
  @SuppressWarnings("unchecked")
  protected void loadFormValuesFromRequest(HttpServletRequest _rq) {
    servLog.debug("loading form values from Servlet request ...");
    
    this.formValues = new HashMap<String, Object[]>(16);
    
    List<String> convertToTuples = null;
    
    Enumeration e = _rq.getParameterNames();
    while (e.hasMoreElements()) {
      String   name = (String)e.nextElement();
      String[] vals = _rq.getParameterValues(name);
      
      if (vals.length == 0) {
        this.formValues.put(name, emptyObjectArray);
        continue;
      }

      int colIdx = name.indexOf(':');
      if (colIdx < 0) {
        // TODO: do we need to morph the String[] into an Object[]?
        this.formValues.put(name, vals);
        continue;
      }

      /* Zope "Converters". Can be attached to form names and will "format"
       * the value. Usually you would want to do this in the element itself
       * (using a formatter), but for quick hacks this is quite nice.
       *
       * eg:
       *   balance:int=10
       *   => put("balance", new Integer(10))
       */
      
      String format = name.substring(colIdx + 1);

      if (format.startsWith("action") || format.startsWith("default_action")) {
        /* processed at a higher level */
        this.formValues.put(name, vals);
        continue;
      }
      
      name = name.substring(0, colIdx);
      
      if (format.startsWith("list") ||
          format.startsWith("tuple") || format.startsWith("array"))
      {
        /* can be nested, eg: dates:list:date */
        colIdx = format.indexOf(':');
        format = format.substring(colIdx + 1);

        Object[] valArray = this.formValues.get(name);
        if (valArray == null || valArray.length == 0) {
          valArray = new Object[] { new ArrayList(4) };
          this.formValues.put(name, valArray);
        }
        List values = (List)valArray[0];

        for (int i = 0; i < vals.length; i++) {
          Object v = colIdx < 0
          ? vals[i] : this.formatFormValue(vals[i], format);

          values.add(v);
        }

        if (format.startsWith("tuple") || format.startsWith("array")) {
          if (convertToTuples == null)
            convertToTuples = new ArrayList<String>(4);
          if (!convertToTuples.contains(name))
            convertToTuples.add(name);
        }
      }
      else if (format.startsWith("record")) {
        /* eg: <input type="text" name="person.age:record:int"> */
        colIdx = format.indexOf(':');
        format = format.substring(colIdx + 1);
        
        String[] path = name.split("\\.");
        if (path.length == 0) {
          log.warn("empty keypath in form value record: " + name +": "+ format);
          continue;
        }
        if (path.length == 1) {
          log.warn("single keypath in form value record: " + name +": "+format);
          // TODO: just apply the single key?
          continue;
        }
        
        // TODO: implement record formats
        log.error("record formats are not yet implemented: " + format);
        
        // TODO: values could also be lists
      }
      else {
        Object[] ovals  = new Object[vals.length];
        for (int i = 0; i < vals.length; i++)
          ovals[i] = this.formatFormValue(vals[i], format);
        this.formValues.put(name, ovals);
      }
    }
    
    /* convert list to tuples */
    
    if (convertToTuples != null) {
      for (String name: convertToTuples) {
        /* Note: we directly modify the array reference */
        Object[] vals = this.formValues.get(name);
        List l = (List)vals[0];
        vals[0] = l.toArray(emptyObjectArray);
      }
    }
  }
  
  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 boolean prepareForStreaming(WOResponse _r) {
    if (_r == null || this.sResponse == null)
      return false;
    
    WOServletAdaptor.prepareResponseHeader(_r, this.sResponse);
    return true;
  }
  
  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;
    }
  }
}
