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