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}