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