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}