/**
 * <p>Title: MyJavaTools: Client HTTP Request class</p>
 * <p>Description: this class helps to send POST HTTP requests with various form data,
 * including files. Cookies can be added to be included in the request.</p>
 *
 * <p>Copyright: This is public domain;
 * The right of people to use, distribute, copy or improve the contents of the
 * following may not be restricted.</p>
 *
 * @author Vlad Patryshev, Alexei Trebounskikh
 * @version $Id: ClientHttpRequest.java 699 2010-11-13 13:33:50Z rawcoder $
 */
package org.jhotdraw.net;

import edu.umd.cs.findbugs.annotations.Nullable;
import java.net.URLConnection;
import java.net.URL;
import java.io.IOException;
import java.security.InvalidParameterException;
import java.util.HashMap;
import java.util.Map;
import java.io.File;
import java.io.InputStream;
import java.util.Random;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.util.Iterator;

public class ClientHttpRequest {

    URLConnection connection;
    @Nullable
    OutputStream os = null;
    Map<String, String> cookies = new HashMap<String, String>();
    String rawCookies = "";

    protected void connect() throws IOException {
        if (os == null) {
            os = connection.getOutputStream();
        }
    }

    protected void write(char c) throws IOException {
        connect();
        os.write(c);
    }

    protected void write(String s) throws IOException {
        connect();
        // BEGIN PATCH W. Randelshofer 2008-05-23 use UTF-8
        os.write(s.getBytes("UTF-8"));
        // END PATCH W. Randelshofer 2008-05-23 use UTF-8
    }

    protected void newline() throws IOException {
        connect();
        write("\r\n");
    }

    protected void writeln(String s) throws IOException {
        connect();
        write(s);
        newline();
    }
    private static Random random = new Random();

    protected static String randomString() {
        return Long.toString(random.nextLong(), 36);
    }
    String boundary = "---------------------------" + randomString() + randomString() + randomString();

    private void boundary() throws IOException {
        write("--");
        write(boundary);
    }

    /**
     * Creates a new multipart POST HTTP request on a freshly opened URLConnection
     *
     * @param connection an already open URL connection
     * @throws IOException
     */
    public ClientHttpRequest(URLConnection connection) throws IOException {
        this.connection = connection;
        connection.setDoOutput(true);
        connection.setDoInput(true);
        connection.setRequestProperty("Content-Type",
                "multipart/form-data; boundary=" + boundary);
    }

    /**
     * Creates a new multipart POST HTTP request for a specified URL
     *
     * @param url the URL to send request to
     * @throws IOException
     */
    public ClientHttpRequest(URL url) throws IOException {
        this(url.openConnection());
    }

    /**
     * Creates a new multipart POST HTTP request for a specified URL string
     *
     * @param urlString the string representation of the URL to send request to
     * @throws IOException
     */
    public ClientHttpRequest(String urlString) throws IOException {
        this(new URL(urlString));
    }

    private void postCookies() {
        StringBuffer cookieList = new StringBuffer(rawCookies);

        for (Iterator i = cookies.entrySet().iterator(); i.hasNext();) {
            Map.Entry entry = (Map.Entry) (i.next());
            cookieList.append(entry.getKey().toString() + "=" + entry.getValue());

            if (i.hasNext()) {
                cookieList.append("; ");
            }
        }
        if (cookieList.length() > 0) {
            connection.setRequestProperty("Cookie", cookieList.toString());
        }
    }

    /**
     * adds a cookie to the requst
     * @param rawCookies A string with raw cookie data.
     * @throws IOException
     */
    public void setCookies(String rawCookies) throws IOException {
        this.rawCookies = (rawCookies == null) ? "" : rawCookies;
        cookies.clear();
    }

    /**
     * adds a cookie to the requst
     * @param name cookie name
     * @param value cookie value
     * @throws IOException
     */
    public void setCookie(String name, String value) throws IOException {
        cookies.put(name, value);
    }

    /**
     * adds cookies to the request
     * @param cookies the cookie "name-to-value" map
     * @throws IOException
     */
    public void setCookies(Map<String, String> cookies) throws IOException {
        if (cookies == null) {
            return;
        }
        this.cookies.putAll(cookies);
    }

    /**
     * adds cookies to the request
     * @param cookies array of cookie names and values (cookies[2*i] is a name, cookies[2*i + 1] is a value)
     * @throws IOException
     */
    public void setCookies(String[] cookies) throws IOException {
        if (cookies == null) {
            return;
        }
        for (int i = 0; i < cookies.length - 1; i += 2) {
            setCookie(cookies[i], cookies[i + 1]);
        }
    }

    private void writeName(String name) throws IOException {
        newline();
        write("Content-Disposition: form-data; name=\"");
        write(name);
        write('"');
    }

    /**
     * adds a string parameter to the request
     * @param name parameter name
     * @param value parameter value
     * @throws IOException
     */
    public void setParameter(String name, String value) throws IOException {
        if (name == null) {
            throw new InvalidParameterException("setParameter(" + name + "," + value + ") name must not be null");
        }
        if (value == null) {
            throw new InvalidParameterException("setParameter(" + name + "," + value + ") value must not be null");
        }
        boundary();
        writeName(name);
        newline();
        newline();
        writeln(value);
    }

    private static void pipe(InputStream in, OutputStream out) throws IOException {
        byte[] buf = new byte[500000];
        int nread;
        int total = 0;

        synchronized (in) {
            while ((nread = in.read(buf, 0, buf.length)) >= 0) {
                out.write(buf, 0, nread);
                total += nread;
            }
        }
        out.flush();
        buf = null;
    }

    /**
     * adds a file parameter to the request
     * @param name parameter name
     * @param filename the name of the file
     * @param is input stream to read the contents of the file from
     * @throws IOException
     */
    public void setParameter(String name, String filename, InputStream is) throws IOException {
        boundary();
        writeName(name);
        write("; filename=\"");
        write(filename);
        write('"');
        newline();
        write("Content-Type: ");
        String type = URLConnection.guessContentTypeFromName(filename);
        if (type == null) {
            type = "application/octet-stream";
        }
        writeln(type);
        newline();
        pipe(is, os);
        newline();
    }

    /**
     * adds a file parameter to the request
     * @param name parameter name
     * @param file the file to upload
     * @throws IOException
     */
    public void setParameter(String name, File file) throws IOException {
        FileInputStream in = new FileInputStream(file);
        try {
            setParameter(name, file.getPath(), in);
        } finally {
            in.close();
        }
    }

    /**
     * adds a parameter to the request; if the parameter is a File, the file is uploaded, otherwise the string value of the parameter is passed in the request
     * @param name parameter name
     * @param object parameter value, a File or anything else that can be stringified
     * @throws IOException
     */
    public void setParameter(String name, Object object) throws IOException {
        if (object instanceof File) {
            setParameter(name, (File) object);
        } else {
            setParameter(name, object.toString());
        }
    }

    /**
     * adds parameters to the request
     * @param parameters "name-to-value" map of parameters; if a value is a file, the file is uploaded, otherwise it is stringified and sent in the request
     * @throws IOException
     */
    public void setParameters(Map parameters) throws IOException {
        if (parameters != null) {
            for (Iterator i = parameters.entrySet().iterator(); i.hasNext();) {
                Map.Entry entry = (Map.Entry) i.next();
                setParameter(entry.getKey().toString(), entry.getValue());
            }
        }
    }

    /**
     * adds parameters to the request
     * @param parameters array of parameter names and values (parameters[2*i] is a name, parameters[2*i + 1] is a value); if a value is a file, the file is uploaded, otherwise it is stringified and sent in the request
     * @throws IOException
     */
    public void setParameters(Object[] parameters) throws IOException {
        if (parameters != null) {
            for (int i = 0; i < parameters.length - 1; i += 2) {
                setParameter(parameters[i].toString(), parameters[i + 1]);
            }
        }
    }

    /**
     * posts the requests to the server, with all the cookies and parameters that were added
     * @return input stream with the server response
     * @throws IOException
     */
    private InputStream doPost() throws IOException {
        boundary();
        writeln("--");
        os.close();

        return connection.getInputStream();
    }

    /**
     * posts the requests to the server, with all the cookies and parameters that were added
     * @return input stream with the server response
     * @throws IOException
     */
    public InputStream post() throws IOException {
        postCookies();
        return doPost();
    }

    /**
     * posts the requests to the server, with all the cookies and parameters that were added before (if any), and with parameters that are passed in the argument
     * @param parameters request parameters
     * @return input stream with the server response
     * @throws IOException
     * @see #setParameters
     */
    public InputStream post(Map parameters) throws IOException {
        postCookies();
        setParameters(parameters);
        return doPost();
    }

    /**
     * posts the requests to the server, with all the cookies and parameters that were added before (if any), and with parameters that are passed in the argument
     * @param parameters request parameters
     * @return input stream with the server response
     * @throws IOException
     * @see #setParameters
     */
    public InputStream post(Object[] parameters) throws IOException {
        postCookies();
        setParameters(parameters);
        return doPost();
    }

    /**
     * posts the requests to the server, with all the cookies and parameters that were added before (if any), and with cookies and parameters that are passed in the arguments
     * @param cookies request cookies
     * @param parameters request parameters
     * @return input stream with the server response
     * @throws IOException
     * @see #setParameters
     * @see #setCookies
     */
    public InputStream post(Map<String, String> cookies, Map parameters) throws IOException {
        setCookies(cookies);
        postCookies();
        setParameters(parameters);
        return doPost();
    }

    /**
     * posts the requests to the server, with all the cookies and parameters that were added before (if any), and with cookies and parameters that are passed in the arguments
     * @param raw_cookies request cookies
     * @param parameters request parameters
     * @return input stream with the server response
     * @throws IOException
     * @see #setParameters
     * @see #setCookies
     */
    public InputStream post(String raw_cookies, Map parameters) throws IOException {
        setCookies(raw_cookies);
        postCookies();
        setParameters(parameters);
        return doPost();
    }

    /**
     * posts the requests to the server, with all the cookies and parameters that were added before (if any), and with cookies and parameters that are passed in the arguments
     * @param cookies request cookies
     * @param parameters request parameters
     * @return input stream with the server response
     * @throws IOException
     * @see #setParameters
     * @see #setCookies
     */
    public InputStream post(String[] cookies, Object[] parameters) throws IOException {
        setCookies(cookies);
        postCookies();
        setParameters(parameters);
        return doPost();
    }

    /**
     * post the POST request to the server, with the specified parameter
     * @param name parameter name
     * @param value parameter value
     * @return input stream with the server response
     * @throws IOException
     * @see #setParameter
     */
    public InputStream post(String name, Object value) throws IOException {
        postCookies();
        setParameter(name, value);
        return doPost();
    }

    /**
     * post the POST request to the server, with the specified parameters
     * @param name1 first parameter name
     * @param value1 first parameter value
     * @param name2 second parameter name
     * @param value2 second parameter value
     * @return input stream with the server response
     * @throws IOException
     * @see #setParameter
     */
    public InputStream post(String name1, Object value1, String name2, Object value2) throws IOException {
        postCookies();
        setParameter(name1, value1);
        setParameter(name2, value2);
        return doPost();
    }

    /**
     * post the POST request to the server, with the specified parameters
     * @param name1 first parameter name
     * @param value1 first parameter value
     * @param name2 second parameter name
     * @param value2 second parameter value
     * @param name3 third parameter name
     * @param value3 third parameter value
     * @return input stream with the server response
     * @throws IOException
     * @see #setParameter
     */
    public InputStream post(String name1, Object value1, String name2, Object value2, String name3, Object value3) throws IOException {
        postCookies();
        setParameter(name1, value1);
        setParameter(name2, value2);
        setParameter(name3, value3);
        return doPost();
    }

    /**
     * post the POST request to the server, with the specified parameters
     * @param name1 first parameter name
     * @param value1 first parameter value
     * @param name2 second parameter name
     * @param value2 second parameter value
     * @param name3 third parameter name
     * @param value3 third parameter value
     * @param name4 fourth parameter name
     * @param value4 fourth parameter value
     * @return input stream with the server response
     * @throws IOException
     * @see #setParameter
     */
    public InputStream post(String name1, Object value1, String name2, Object value2, String name3, Object value3, String name4, Object value4) throws IOException {
        postCookies();
        setParameter(name1, value1);
        setParameter(name2, value2);
        setParameter(name3, value3);
        setParameter(name4, value4);
        return doPost();
    }

    /**
     * posts a new request to specified URL, with parameters that are passed in the argument
     * @param parameters request parameters
     * @return input stream with the server response
     * @throws IOException
     * @see #setParameters
     */
    public static InputStream post(URL url, Map parameters) throws IOException {
        return new ClientHttpRequest(url).post(parameters);
    }

    /**
     * posts a new request to specified URL, with parameters that are passed in the argument
     * @param parameters request parameters
     * @return input stream with the server response
     * @throws IOException
     * @see #setParameters
     */
    public static InputStream post(URL url, Object[] parameters) throws IOException {
        return new ClientHttpRequest(url).post(parameters);
    }

    /**
     * posts a new request to specified URL, with cookies and parameters that are passed in the argument
     * @param cookies request cookies
     * @param parameters request parameters
     * @return input stream with the server response
     * @throws IOException
     * @see #setCookies
     * @see #setParameters
     */
    public static InputStream post(URL url, Map<String, String> cookies, Map parameters) throws IOException {
        return new ClientHttpRequest(url).post(cookies, parameters);
    }

    /**
     * posts a new request to specified URL, with cookies and parameters that are passed in the argument
     * @param url post URL
     * @param cookies request cookies
     * @param parameters request parameters
     * @return input stream with the server response
     * @throws IOException
     * @see #setCookies
     * @see #setParameters
     */
    public static InputStream post(URL url, String[] cookies, Object[] parameters) throws IOException {
        return new ClientHttpRequest(url).post(cookies, parameters);
    }

    /**
     * post the POST request specified URL, with the specified parameter
     * @param url post URL
     * @param name1 parameter name
     * @param value1 parameter value
     * @return input stream with the server response
     * @throws IOException
     * @see #setParameter
     */
    public static InputStream post(URL url, String name1, Object value1) throws IOException {
        return new ClientHttpRequest(url).post(name1, value1);
    }

    /**
     * post the POST request to specified URL, with the specified parameters
     * @param name1 first parameter name
     * @param value1 first parameter value
     * @param name2 second parameter name
     * @param value2 second parameter value
     * @return input stream with the server response
     * @throws IOException
     * @see #setParameter
     */
    public static InputStream post(URL url, String name1, Object value1, String name2, Object value2) throws IOException {
        return new ClientHttpRequest(url).post(name1, value1, name2, value2);
    }

    /**
     * post the POST request to specified URL, with the specified parameters
     * @param name1 first parameter name
     * @param value1 first parameter value
     * @param name2 second parameter name
     * @param value2 second parameter value
     * @param name3 third parameter name
     * @param value3 third parameter value
     * @return input stream with the server response
     * @throws IOException
     * @see #setParameter
     */
    public static InputStream post(URL url, String name1, Object value1, String name2, Object value2, String name3, Object value3) throws IOException {
        return new ClientHttpRequest(url).post(name1, value1, name2, value2, name3, value3);
    }

    /**
     * post the POST request to specified URL, with the specified parameters
     * @param name1 first parameter name
     * @param value1 first parameter value
     * @param name2 second parameter name
     * @param value2 second parameter value
     * @param name3 third parameter name
     * @param value3 third parameter value
     * @param name4 fourth parameter name
     * @param value4 fourth parameter value
     * @return input stream with the server response
     * @throws IOException
     * @see #setParameter
     */
    public static InputStream post(URL url, String name1, Object value1, String name2, Object value2, String name3, Object value3, String name4, Object value4) throws IOException {
        return new ClientHttpRequest(url).post(name1, value1, name2, value2, name3, value3, name4, value4);
    }
}
