/*
 * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
 *
 * The contents of this file are subject to the CCM Public
 * License (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the
 * License at http://www.redhat.com/licenses/ccmpl.html.
 *
 * Software distributed under the License is distributed on an
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
 * or implied. See the License for the specific language
 * governing rights and limitations under the License.
 *
 */
package com.arsdigita.dispatcher;

import com.arsdigita.globalization.Globalization;
import com.arsdigita.util.UncheckedWrapperException;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.Part;
import javax.mail.internet.MimeMultipart;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.log4j.Category;

/**
 * MultipartHttpServletRequest provides
 * multipart/form-data handling capabilities to facilitate file
 * uploads for servlets. This request object parses the HTTP request
 * with MIME type "multipart" and places the encoded object in a
 * stream.
 *
 * @author Karl Goldstein
 * @author Michael Pih
 * @author Uday Mathur
 * @version $Revision: #18 $ $Date: 2004/04/07 $
 * @since 4.5
 */
public class MultipartHttpServletRequest implements HttpServletRequest {
    public static final String versionId =
        "$Id: //core-platform/dev/src/com/arsdigita/dispatcher/MultipartHttpServletRequest.java#18 $" +
        "$Author: dennis $" +
        "$DateTime: 2004/04/07 16:07:11 $";

    private static final Category s_log = Category.getInstance
        (MultipartHttpServletRequest.class);

    private HttpServletRequest m_request;
    private Map m_parameters = null;

    /**
     * Create a multipart servlet request object and parse the request.
     *
     * @param request The request
     */
    public MultipartHttpServletRequest(HttpServletRequest request)
            throws MessagingException, IOException {
        m_request = request;
        m_parameters = Collections.synchronizedMap(new HashMap());
        parseMultipartRequest(m_request);
    }

    /**
     * Create a multipart servlet request object and parse the request.
     *
     * @param request The request
     */
    public MultipartHttpServletRequest(MultipartHttpServletRequest original,
                                       HttpServletRequest current) {
        m_request = current;
        m_parameters = original.m_parameters;
    }

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

    public Enumeration getAttributeNames() {
        return m_request.getAttributeNames();
    }

    public String getParameter(String name) {
        String[] values = (String[]) m_parameters.get(name);

        if (values == null || values.length == 0) {
            return null;
        } else {
            return values[0];
        }
    }

    public Map getParameterMap() {
        return m_parameters;
    }

    public Enumeration getParameterNames() {
        return Collections.enumeration(m_parameters.keySet());
    }

    public String getFileName(String name) {
        return getParameter(name);
    }

    public File getFile(String name) {
        String path = getParameter(name + ".tmpfile");

        if (path == null) {
            return null;
        } else {            
            return new File(path);
        }
    }

    public String[] getParameterValues(String name) {
        return (String[]) m_parameters.get(name);
    }

    // Additional methods for HttpServletRequest

    public String getAuthType() {
        return m_request.getAuthType();
    }

    public Cookie[] getCookies() {
        return m_request.getCookies();
    }

    public long getDateHeader(String name) {
        return m_request.getDateHeader(name);
    }

    public String getHeader(String name) {
        return m_request.getHeader(name);
    }

    public Enumeration getHeaders(String name) {
        return m_request.getHeaders(name);
    }

    public Enumeration getHeaderNames() {
        return m_request.getHeaderNames();
    }

    public int getIntHeader(String name) {
        return m_request.getIntHeader(name);
    }

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

    public String getPathInfo() {
        return m_request.getPathInfo();
    }

    public String getPathTranslated() {
        return m_request.getPathTranslated();
    }

    public String getContextPath() {
        return m_request.getContextPath();
    }

    public String getQueryString() {
        return m_request.getQueryString();
    }

    public String getRemoteUser() {
        return m_request.getRemoteUser();
    }

    public boolean isUserInRole(String role) {
        return m_request.isUserInRole(role);
    }

    public java.security.Principal getUserPrincipal() {
        return m_request.getUserPrincipal();
    }

    public String getRequestedSessionId() {
        return m_request.getRequestedSessionId();
    }

    public String getRequestURI() {
        return m_request.getRequestURI();
    }

    public StringBuffer getRequestURL() {
        throw new UnsupportedOperationException
            ("This is a Servlet 2.3 feature that we do not currently support");
    }

    public String getServletPath() {
        return m_request.getServletPath();
    }

    public HttpSession getSession(boolean create) {
        return m_request.getSession(create);
    }

    public HttpSession getSession() {
        return m_request.getSession();
    }

    public boolean isRequestedSessionIdValid() {
        return m_request.isRequestedSessionIdValid();
    }

    public boolean isRequestedSessionIdFromCookie() {
        return m_request.isRequestedSessionIdFromCookie();
    }

    public boolean isRequestedSessionIdFromURL() {
        return m_request.isRequestedSessionIdFromURL();
    }

    public boolean isRequestedSessionIdFromUrl() {
        return m_request.isRequestedSessionIdFromUrl();
    }

    //methods for ServletRequest Interface

    public String getCharacterEncoding() {
        return m_request.getCharacterEncoding();
    }

    public void setCharacterEncoding(String encoding)
            throws java.io.UnsupportedEncodingException {
        throw new UnsupportedOperationException
            ("This is a Servlet 2.3 feature that we do not currently support");
    }

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

    public String getContentType() {
        return m_request.getContentType();
    }

    public ServletInputStream getInputStream()
            throws IOException {
        //maybe just throw an exception here -- UM
        return m_request.getInputStream();
    }

    public String getProtocol() {
        return m_request.getProtocol();
    }

    public String getScheme() {
        return m_request.getScheme();
    }

    public String getServerName() {
        return m_request.getServerName();
    }

    public int getServerPort() {
        return m_request.getServerPort();
    }

    public BufferedReader getReader() throws IOException {
        //maybe just throw an exception here -- Uday
        return m_request.getReader();
    }

    public String getRemoteAddr() {
        return m_request.getRemoteAddr();
    }

    public String getRemoteHost() {
        return m_request.getRemoteHost();
    }

    public void setAttribute(String name,
                             Object o) {
        m_request.setAttribute(name, o);
    }

    public void removeAttribute(String name) {
        m_request.removeAttribute(name);
    }

    public Locale getLocale() {
        return m_request.getLocale();
    }

    public Enumeration getLocales() {
        return m_request.getLocales();
    }

    public boolean isSecure() {
        return m_request.isSecure();
    }

    public RequestDispatcher getRequestDispatcher(String path) {
        return m_request.getRequestDispatcher(path);
    }

    public String getRealPath(String path) {
        return m_request.getRealPath(path);
    }

    /*
     * Parse the body of multipart MIME-encoded request.
     */
    private void parseMultipartRequest(HttpServletRequest request)
            throws MessagingException, IOException {
        ServletRequestDataSource srds = new ServletRequestDataSource(request);
        MimeMultipart multipart = new MimeMultipart(srds);

        int count = multipart.getCount();

        BodyPart part;

        for (int i = 0; i < count; i++) {
            part = multipart.getBodyPart(i);
            handlePart(part);
        }
    }

    private void handlePart(Part part) throws MessagingException, IOException {
        String disposition = part.getHeader("Content-Disposition")[0];

        String paramName = getParamName(disposition);

        // check to see if it's an uploaded file
        String fileName = part.getFileName();

        // content is a string
        if (fileName == null) {
            addParameterValue(paramName, part.getContent());
        } else {
            //setParameterValues(paramName, fileName);
            int fnStart = disposition.indexOf("filename=\"");
            int fnEnd = disposition.indexOf('"',fnStart+10);
            fileName = disposition.substring(fnStart+10, fnEnd);
            addParameterValue(paramName, fileName);
            saveUploadedFile(paramName, part.getInputStream());
        }
    }

    private String getParamName(String disposition) throws MessagingException {
        int start = disposition.indexOf("name=\"");
        if (start != -1) {
            start += 6;
        }
        int end = disposition.indexOf('"', start);

        // lynx < 2.8.4 doesn't quote name= attribute if it only 
        // contains alphanumeric chars.
        if (start == -1 || end == -1) {
            start = disposition.indexOf("name=");
            if (start != -1) {
                start += 5;
                end = disposition.indexOf(" ", start);
                if (end == -1) {
                    end = disposition.length();
                }
            }
        }
        if (start == -1 || end == -1) {
            throw new MessagingException
                ("Failed to extract parameter name while parsing multipart " +
                 "form. Disposition was: '" + disposition + "'");
        }
        String paramName = disposition.substring(start,end);

        return paramName;
    }

    private void addParameterValue(String name, Object value)
        throws IOException
    {
        String[] newValues;
        String[] values = (String[]) m_parameters.get(name);

        if (values == null) {
            newValues = new String[1];
        } else {
            newValues = new String[values.length + 1];
            System.arraycopy(values, 0, newValues, 0, values.length);
        }

        newValues[newValues.length - 1] = convertToString( value );

        m_parameters.put(name, newValues);
    }

    private String convertToString( Object value ) throws IOException {
        if( value instanceof String ) return (String) value;

        if( value instanceof ByteArrayInputStream ) {
            StringBuffer output = new StringBuffer();

            InputStreamReader reader;
            try {
                reader = new InputStreamReader(
                    (ByteArrayInputStream) value,
                    Globalization.DEFAULT_ENCODING );
            } catch( UnsupportedEncodingException ex ) {
                throw new UncheckedWrapperException( ex );
            }

            final int bufSize = 1024;
            char[] buffer = new char[bufSize];

            int read = bufSize;
            while( bufSize == read ) {
                read = reader.read( buffer, 0, bufSize );
                if( read > 0 ) output.append( buffer, 0, read );
            }

            return output.toString();
        }

        // Fallback to default
        return value.toString();
    }

    /*
     * Save the uploaded file as a temporary file.
     *
     * @param paramName The name of the file upload widget
     * @param in An input stream
     */
    private void saveUploadedFile(String paramName, InputStream in)
            throws IOException {
        File tmpFile = File.createTempFile("acs", null, null);
        tmpFile.deleteOnExit();

        addParameterValue(paramName + ".tmpfile", tmpFile.getPath());

        FileOutputStream out = new FileOutputStream(tmpFile);
        try {
            byte[] buffer = new byte[4096];
            int length = 0;
            while ((length = in.read(buffer, 0, 4096)) != -1) {
                out.write(buffer, 0, length);
            }
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                s_log.error("Could not close output stream", e);
            }
        }

    }

    /*
     * Helper class for parsing the multipart request.
     */
    private final class ServletRequestDataSource
        implements javax.activation.DataSource {

        private InputStream m_is;
        private String m_contentType;
        private String m_name = "ServletRequestDataSource";

        public ServletRequestDataSource(HttpServletRequest request)
                throws IOException {
            m_is = request.getInputStream();
            m_contentType = request.getContentType();
        }

        public String getName() {
            return m_name;
        }

        public InputStream getInputStream() {
            return m_is;
        }

        public String getContentType() {
            return m_contentType;
        }

        public OutputStream getOutputStream() {
            throw new UnsupportedOperationException
                ("getOutputStream not allowed on a multipart request " +
                 "data source");
        }
    }

}
