001package com.box.sdk;
002
003import java.io.IOException;
004import java.io.InputStream;
005import java.io.OutputStream;
006import java.net.HttpURLConnection;
007import java.net.URL;
008import java.net.URLEncoder;
009import java.util.Date;
010import java.util.HashMap;
011import java.util.Map;
012import java.util.logging.Level;
013import java.util.logging.Logger;
014
015/**
016 * Used to make HTTP multipart requests to the Box API.
017 *
018 * <p>This class partially implements the HTTP multipart standard in order to upload files to Box. The body of this
019 * request type cannot be set directly. Instead, it can be modified by adding multipart fields and setting file
020 * contents. The body of multipart requests will not be logged since they are likely to contain binary data.</p>
021 *
022 */
023public class BoxMultipartRequest extends BoxAPIRequest {
024    private static final Logger LOGGER = Logger.getLogger(BoxMultipartRequest.class.getName());
025    private static final String BOUNDARY = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
026    private static final int BUFFER_SIZE = 8192;
027
028    private final StringBuilder loggedRequest = new StringBuilder();
029
030    private OutputStream outputStream;
031    private InputStream inputStream;
032    private String filename;
033    private long fileSize;
034    private Map<String, String> fields;
035    private boolean firstBoundary;
036
037    /**
038     * Constructs an authenticated BoxMultipartRequest using a provided BoxAPIConnection.
039     * @param  api    an API connection for authenticating the request.
040     * @param  url    the URL of the request.
041     */
042    public BoxMultipartRequest(BoxAPIConnection api, URL url) {
043        super(api, url, "POST");
044
045        this.fields = new HashMap<String, String>();
046        this.firstBoundary = true;
047
048        this.addHeader("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
049    }
050
051    /**
052     * Adds or updates a multipart field in this request.
053     * @param key   the field's key.
054     * @param value the field's value.
055     */
056    public void putField(String key, String value) {
057        this.fields.put(key, value);
058    }
059
060    /**
061     * Adds or updates a multipart field in this request.
062     * @param key   the field's key.
063     * @param value the field's value.
064     */
065    public void putField(String key, Date value) {
066        this.fields.put(key, BoxDateFormat.format(value));
067    }
068
069    /**
070     * Sets the file contents of this request.
071     * @param inputStream a stream containing the file contents.
072     * @param filename    the name of the file.
073     */
074    public void setFile(InputStream inputStream, String filename) {
075        this.inputStream = inputStream;
076        this.filename = filename;
077    }
078
079    /**
080     * Sets the file contents of this request.
081     * @param inputStream a stream containing the file contents.
082     * @param filename    the name of the file.
083     * @param fileSize    the size of the file.
084     */
085    public void setFile(InputStream inputStream, String filename, long fileSize) {
086        this.setFile(inputStream, filename);
087        this.fileSize = fileSize;
088    }
089
090    /**
091     * Sets the SHA1 hash of the file contents of this request.
092     * If set, it will ensure that the file is not corrupted in transit.
093     * @param sha1 a string containing the SHA1 hash of the file contents.
094     */
095    public void setContentSHA1(String sha1) {
096        this.addHeader("Content-MD5", sha1);
097    }
098
099    /**
100     * This method is unsupported in BoxMultipartRequest. Instead, the body should be modified via the {@code putField}
101     * and {@code setFile} methods.
102     * @param stream N/A
103     * @throws UnsupportedOperationException this method is unsupported.
104     */
105    @Override
106    public void setBody(InputStream stream) {
107        throw new UnsupportedOperationException();
108    }
109
110    /**
111     * This method is unsupported in BoxMultipartRequest. Instead, the body should be modified via the {@code putField}
112     * and {@code setFile} methods.
113     * @param body N/A
114     * @throws UnsupportedOperationException this method is unsupported.
115     */
116    @Override
117    public void setBody(String body) {
118        throw new UnsupportedOperationException();
119    }
120
121    @Override
122    protected void writeBody(HttpURLConnection connection, ProgressListener listener) {
123        try {
124            connection.setChunkedStreamingMode(0);
125            connection.setDoOutput(true);
126            this.outputStream = connection.getOutputStream();
127
128            for (Map.Entry<String, String> entry : this.fields.entrySet()) {
129                this.writePartHeader(new String[][] {{"name", entry.getKey()}});
130                this.writeOutput(entry.getValue());
131            }
132
133            this.writePartHeader(new String[][] {{"name", "file"}, {"filename", this.filename}},
134                "application/octet-stream");
135
136            OutputStream fileContentsOutputStream = this.outputStream;
137            if (listener != null) {
138                fileContentsOutputStream = new ProgressOutputStream(this.outputStream, listener, this.fileSize);
139            }
140            byte[] buffer = new byte[BUFFER_SIZE];
141            int n = this.inputStream.read(buffer);
142            while (n != -1) {
143                fileContentsOutputStream.write(buffer, 0, n);
144                n = this.inputStream.read(buffer);
145            }
146
147            if (LOGGER.isLoggable(Level.FINE)) {
148                this.loggedRequest.append("<File Contents Omitted>");
149            }
150
151            this.writeBoundary();
152            this.writeOutput("--");
153        } catch (IOException e) {
154            throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
155        }
156    }
157
158    @Override
159    protected void resetBody() throws IOException {
160        this.firstBoundary = true;
161        this.inputStream.reset();
162        this.loggedRequest.setLength(0);
163    }
164
165    @Override
166    protected String bodyToString() {
167        return this.loggedRequest.toString();
168    }
169
170    private void writeBoundary() throws IOException {
171        if (!this.firstBoundary) {
172            this.writeOutput("\r\n");
173        }
174
175        this.firstBoundary = false;
176        this.writeOutput("--");
177        this.writeOutput(BOUNDARY);
178    }
179
180    private void writePartHeader(String[][] formData) throws IOException {
181        this.writePartHeader(formData, null);
182    }
183
184    private void writePartHeader(String[][] formData, String contentType) throws IOException {
185        this.writeBoundary();
186        this.writeOutput("\r\n");
187        this.writeOutput("Content-Disposition: form-data");
188        for (int i = 0; i < formData.length; i++) {
189            this.writeOutput("; ");
190            this.writeOutput(formData[i][0]);
191            this.writeOutput("=\"");
192            this.writeOutput(URLEncoder.encode(formData[i][1], "UTF-8"));
193            this.writeOutput("\"");
194        }
195
196        if (contentType != null) {
197            this.writeOutput("\r\nContent-Type: ");
198            this.writeOutput(contentType);
199        }
200
201        this.writeOutput("\r\n\r\n");
202    }
203
204    private void writeOutput(String s) throws IOException {
205        this.outputStream.write(s.getBytes(StandardCharsets.UTF_8));
206        if (LOGGER.isLoggable(Level.FINE)) {
207            this.loggedRequest.append(s);
208        }
209    }
210
211    private void writeOutput(int b) throws IOException {
212        this.outputStream.write(b);
213    }
214}