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}