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