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