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