001package jmri.util;
002
003import java.io.BufferedReader;
004import java.io.File;
005import java.io.FileInputStream;
006import java.io.IOException;
007import java.io.InputStreamReader;
008import java.io.OutputStream;
009import java.io.OutputStreamWriter;
010import java.io.PrintWriter;
011import java.net.HttpURLConnection;
012import java.net.URL;
013import java.net.URLConnection;
014import java.util.ArrayList;
015import java.util.List;
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
018
019/**
020 * Sends multi-part HTTP POST requests to a web server
021 * <p>
022 * Based on
023 * http://www.codejava.net/java-se/networking/upload-files-by-sending-multipart-request-programmatically
024 * <hr>
025 * This file is part of JMRI.
026 * <p>
027 * JMRI is free software; you can redistribute it and/or modify it under the
028 * terms of version 2 of the GNU General Public License as published by the Free
029 * Software Foundation. See the "COPYING" file for a copy of this license.
030 * <p>
031 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
032 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
033 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
034 *
035 * @author Matthew Harris Copyright (C) 2014
036 */
037public class MultipartMessage {
038
039    private final String boundary;
040    private static final String LINE_FEED = "\r\n";
041    private final HttpURLConnection httpConn;
042    private final String charSet;
043    private final OutputStream outStream;
044    private final PrintWriter writer;
045
046    /**
047     * Constructor initialises a new HTTP POST request with content type set to
048     * 'multipart/form-data'.
049     * <p>
050     * This allows for additional binary data to be uploaded.
051     *
052     * @param requestURL URL to which this request should be sent
053     * @param charSet    character set encoding of this message
054     * @throws IOException if {@link OutputStream} cannot be created
055     */
056    public MultipartMessage(String requestURL, String charSet) throws IOException {
057        this.charSet = charSet;
058
059        // create unique multi-part message boundary
060        boundary = "===" + System.currentTimeMillis() + "===";
061        URL url = new URL(requestURL);
062        httpConn = (HttpURLConnection) url.openConnection();
063        httpConn.setUseCaches(false);
064        httpConn.setDoOutput(true);
065        httpConn.setDoInput(true);
066        httpConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
067        httpConn.setRequestProperty("User-Agent", "JMRI " + jmri.Version.getCanonicalVersion());
068        outStream = httpConn.getOutputStream();
069        writer = new PrintWriter(new OutputStreamWriter(outStream, this.charSet), true);
070    }
071
072    /**
073     * Adds form field data to the request
074     *
075     * @param name  field name
076     * @param value field value
077     */
078    public void addFormField(String name, String value) {
079        log.debug("add form field: {}; value: {}", name, value);
080        writer.append("--" + boundary).append(LINE_FEED);
081        writer.append(
082                "Content-Disposition: form-data; name=\"" + name
083                + "\"").append(LINE_FEED);
084        writer.append("Content-Type: text/plain; charset=" + charSet)
085                .append(LINE_FEED);
086        writer.append(LINE_FEED);
087        writer.append(value).append(LINE_FEED);
088        writer.flush();
089    }
090
091    /**
092     * Adds an upload file section to the request. MIME type of the file is
093     * determined based on the file extension.
094     *
095     * @param fieldName  name attribute in form &lt;input name="{fieldName}"
096     *                   type="file" /&gt;
097     * @param uploadFile file to be uploaded
098     * @throws IOException if problem adding file to request
099     */
100    public void addFilePart(String fieldName, File uploadFile) throws IOException {
101        addFilePart(fieldName, uploadFile, URLConnection.guessContentTypeFromName(uploadFile.getName()));
102    }
103
104    /**
105     * Adds an upload file section to the request. MIME type of the file is
106     * explicitly set.
107     *
108     * @param fieldName  name attribute in form &lt;input name="{fieldName}"
109     *                   type="file" /&gt;
110     * @param uploadFile file to be uploaded
111     * @param fileType   MIME type of file
112     * @throws IOException if problem adding file to request
113     */
114    public void addFilePart(String fieldName, File uploadFile, String fileType) throws IOException {
115        log.debug("add file field: {}; file: {}; type: {}", fieldName, uploadFile, fileType);
116        String fileName = uploadFile.getName();
117        writer.append("--" + boundary).append(LINE_FEED);
118        writer.append(
119                "Content-Disposition: form-data; name=\"" + fieldName
120                + "\"; filename=\"" + fileName + "\"")
121                .append(LINE_FEED);
122        writer.append(
123                "Content-Type: " + fileType).append(LINE_FEED);
124        writer.append("Content-Transfer-Encoding: binary").append(LINE_FEED);
125        writer.append(LINE_FEED);
126        writer.flush();
127
128        try (FileInputStream inStream = new FileInputStream(uploadFile)) {
129            byte[] buffer = new byte[4096];
130            int bytesRead;
131            while ((bytesRead = inStream.read(buffer)) != -1) {
132                outStream.write(buffer, 0, bytesRead);
133            }
134            outStream.flush();
135        }
136
137        writer.append(LINE_FEED);
138        writer.flush();
139    }
140
141    /**
142     * Adds a header field to the request
143     *
144     * @param name  name of header field
145     * @param value value of header field
146     */
147    public void addHeaderField(String name, String value) {
148        log.debug("add header field: {}; value: {}", name, value);
149        writer.append(name + ": " + value).append(LINE_FEED);
150        writer.flush();
151    }
152
153    /**
154     * Finalise and send MultipartMessage to end-point.
155     *
156     * @return Responses from end-point as a List of Strings
157     * @throws IOException if problem sending MultipartMessage to end-point
158     */
159    public List<String> finish() throws IOException {
160        List<String> response = new ArrayList<>();
161
162        writer.append(LINE_FEED).flush();
163        writer.append("--" + boundary + "--").append(LINE_FEED);
164        writer.close();
165
166        // check server status code first
167        int status = httpConn.getResponseCode();
168        if (status == HttpURLConnection.HTTP_OK) {
169            try (BufferedReader reader = new BufferedReader(new InputStreamReader(httpConn.getInputStream()))) {
170                String line;
171                while ((line = reader.readLine()) != null) {
172                    response.add(line);
173                }
174            }
175            httpConn.disconnect();
176        } else {
177            throw new IOException("Server returned non-OK status: " + status);
178        }
179
180        return response;
181    }
182
183    private static final Logger log = LoggerFactory.getLogger(MultipartMessage.class);
184
185}