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