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.nio.charset.StandardCharsets;
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(BoxFolder.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     * This method is unsupported in BoxMultipartRequest. Instead, the body should be modified via the {@code putField}
092     * and {@code setFile} methods.
093     * @param stream N/A
094     * @throws UnsupportedOperationException this method is unsupported.
095     */
096    @Override
097    public void setBody(InputStream stream) {
098        throw new UnsupportedOperationException();
099    }
100
101    /**
102     * This method is unsupported in BoxMultipartRequest. Instead, the body should be modified via the {@code putField}
103     * and {@code setFile} methods.
104     * @param body N/A
105     * @throws UnsupportedOperationException this method is unsupported.
106     */
107    @Override
108    public void setBody(String body) {
109        throw new UnsupportedOperationException();
110    }
111
112    @Override
113    protected void writeBody(HttpURLConnection connection, ProgressListener listener) {
114        try {
115            connection.setChunkedStreamingMode(0);
116            connection.setDoOutput(true);
117            this.outputStream = connection.getOutputStream();
118
119            this.writePartHeader(new String[][] {{"name", "filename"}, {"filename", this.filename}},
120                "application/octet-stream");
121
122            OutputStream fileContentsOutputStream = this.outputStream;
123            if (listener != null) {
124                fileContentsOutputStream = new ProgressOutputStream(this.outputStream, listener, this.fileSize);
125            }
126            byte[] buffer = new byte[BUFFER_SIZE];
127            int n = this.inputStream.read(buffer);
128            while (n != -1) {
129                fileContentsOutputStream.write(buffer, 0, n);
130                n = this.inputStream.read(buffer);
131            }
132
133            if (LOGGER.isLoggable(Level.FINE)) {
134                this.loggedRequest.append("<File Contents Omitted>");
135            }
136
137            for (Map.Entry<String, String> entry : this.fields.entrySet()) {
138                this.writePartHeader(new String[][] {{"name", entry.getKey()}});
139                this.writeOutput(entry.getValue());
140            }
141
142            this.writeBoundary();
143        } catch (IOException e) {
144            throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
145        }
146    }
147
148    @Override
149    protected void resetBody() throws IOException {
150        this.firstBoundary = true;
151        this.inputStream.reset();
152        this.loggedRequest.setLength(0);
153    }
154
155    @Override
156    protected String bodyToString() {
157        return this.loggedRequest.toString();
158    }
159
160    private void writeBoundary() throws IOException {
161        if (!this.firstBoundary) {
162            this.writeOutput("\r\n");
163        }
164
165        this.firstBoundary = false;
166        this.writeOutput("--");
167        this.writeOutput(BOUNDARY);
168    }
169
170    private void writePartHeader(String[][] formData) throws IOException {
171        this.writePartHeader(formData, null);
172    }
173
174    private void writePartHeader(String[][] formData, String contentType) throws IOException {
175        this.writeBoundary();
176        this.writeOutput("\r\n");
177        this.writeOutput("Content-Disposition: form-data");
178        for (int i = 0; i < formData.length; i++) {
179            this.writeOutput("; ");
180            this.writeOutput(formData[i][0]);
181            this.writeOutput("=\"");
182            this.writeOutput(formData[i][1]);
183            this.writeOutput("\"");
184        }
185
186        if (contentType != null) {
187            this.writeOutput("\r\nContent-Type: ");
188            this.writeOutput(contentType);
189        }
190
191        this.writeOutput("\r\n\r\n");
192    }
193
194    private void writeOutput(String s) throws IOException {
195        this.outputStream.write(s.getBytes(StandardCharsets.UTF_8));
196        if (LOGGER.isLoggable(Level.FINE)) {
197            this.loggedRequest.append(s);
198        }
199    }
200
201    private void writeOutput(int b) throws IOException {
202        this.outputStream.write(b);
203    }
204}