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