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.util.Date; 009import java.util.HashMap; 010import java.util.Map; 011import java.util.logging.Level; 012import java.util.logging.Logger; 013 014/** 015 * Used to make HTTP multipart requests to the Box API. 016 * 017 * <p>This class partially implements the HTTP multipart standard in order to upload files to Box. The body of this 018 * request type cannot be set directly. Instead, it can be modified by adding multipart fields and setting file 019 * contents. The body of multipart requests will not be logged since they are likely to contain binary data.</p> 020 * 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 String filename; 032 private long fileSize; 033 private Map<String, String> fields; 034 private boolean firstBoundary; 035 036 /** 037 * Constructs an authenticated BoxMultipartRequest using a provided BoxAPIConnection. 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<String, String>(); 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 * @param key the field's key. 053 * @param value the field's value. 054 */ 055 public void putField(String key, String value) { 056 this.fields.put(key, value); 057 } 058 059 /** 060 * Adds or updates a multipart field in this request. 061 * @param key the field's key. 062 * @param value the field's value. 063 */ 064 public void putField(String key, Date value) { 065 this.fields.put(key, BoxDateFormat.format(value)); 066 } 067 068 /** 069 * Sets the file contents of this request. 070 * @param inputStream a stream containing the file contents. 071 * @param filename the name of the file. 072 */ 073 public void setFile(InputStream inputStream, String filename) { 074 this.inputStream = inputStream; 075 this.filename = filename; 076 } 077 078 /** 079 * Sets the file contents of this request. 080 * @param inputStream a stream containing the file contents. 081 * @param filename the name of the file. 082 * @param fileSize the size of the file. 083 */ 084 public void setFile(InputStream inputStream, String filename, long fileSize) { 085 this.setFile(inputStream, filename); 086 this.fileSize = fileSize; 087 } 088 089 /** 090 * This method is unsupported in BoxMultipartRequest. Instead, the body should be modified via the {@code putField} 091 * and {@code setFile} methods. 092 * @param stream N/A 093 * @throws UnsupportedOperationException this method is unsupported. 094 */ 095 @Override 096 public void setBody(InputStream stream) { 097 throw new UnsupportedOperationException(); 098 } 099 100 /** 101 * This method is unsupported in BoxMultipartRequest. Instead, the body should be modified via the {@code putField} 102 * and {@code setFile} methods. 103 * @param body N/A 104 * @throws UnsupportedOperationException this method is unsupported. 105 */ 106 @Override 107 public void setBody(String body) { 108 throw new UnsupportedOperationException(); 109 } 110 111 @Override 112 protected void writeBody(HttpURLConnection connection, ProgressListener listener) { 113 try { 114 connection.setChunkedStreamingMode(0); 115 connection.setDoOutput(true); 116 this.outputStream = connection.getOutputStream(); 117 118 this.writePartHeader(new String[][] {{"name", "filename"}, {"filename", this.filename}}, 119 "application/octet-stream"); 120 121 OutputStream fileContentsOutputStream = this.outputStream; 122 if (listener != null) { 123 fileContentsOutputStream = new ProgressOutputStream(this.outputStream, listener, this.fileSize); 124 } 125 byte[] buffer = new byte[BUFFER_SIZE]; 126 int n = this.inputStream.read(buffer); 127 while (n != -1) { 128 fileContentsOutputStream.write(buffer, 0, n); 129 n = this.inputStream.read(buffer); 130 } 131 132 if (LOGGER.isLoggable(Level.FINE)) { 133 this.loggedRequest.append("<File Contents Omitted>"); 134 } 135 136 for (Map.Entry<String, String> entry : this.fields.entrySet()) { 137 this.writePartHeader(new String[][] {{"name", entry.getKey()}}); 138 this.writeOutput(entry.getValue()); 139 } 140 141 this.writeBoundary(); 142 } catch (IOException e) { 143 throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e); 144 } 145 } 146 147 @Override 148 protected void resetBody() throws IOException { 149 this.firstBoundary = true; 150 this.inputStream.reset(); 151 this.loggedRequest.setLength(0); 152 } 153 154 @Override 155 protected String bodyToString() { 156 return this.loggedRequest.toString(); 157 } 158 159 private void writeBoundary() throws IOException { 160 if (!this.firstBoundary) { 161 this.writeOutput("\r\n"); 162 } 163 164 this.firstBoundary = false; 165 this.writeOutput("--"); 166 this.writeOutput(BOUNDARY); 167 } 168 169 private void writePartHeader(String[][] formData) throws IOException { 170 this.writePartHeader(formData, null); 171 } 172 173 private void writePartHeader(String[][] formData, String contentType) throws IOException { 174 this.writeBoundary(); 175 this.writeOutput("\r\n"); 176 this.writeOutput("Content-Disposition: form-data"); 177 for (int i = 0; i < formData.length; i++) { 178 this.writeOutput("; "); 179 this.writeOutput(formData[i][0]); 180 this.writeOutput("=\""); 181 this.writeOutput(formData[i][1]); 182 this.writeOutput("\""); 183 } 184 185 if (contentType != null) { 186 this.writeOutput("\r\nContent-Type: "); 187 this.writeOutput(contentType); 188 } 189 190 this.writeOutput("\r\n\r\n"); 191 } 192 193 private void writeOutput(String s) throws IOException { 194 this.outputStream.write(s.getBytes(StandardCharsets.UTF_8)); 195 if (LOGGER.isLoggable(Level.FINE)) { 196 this.loggedRequest.append(s); 197 } 198 } 199 200 private void writeOutput(int b) throws IOException { 201 this.outputStream.write(b); 202 } 203}