View Javadoc
1   //
2   // $Id: FileUploadThreadHTTP.java 1469 2010-12-14 12:27:42Z etienne_sf $
3   //
4   // jupload - A file upload applet.
5   // Copyright 2007 The JUpload Team
6   //
7   // Created: 2007-03-07
8   // Creator: etienne_sf
9   // Last modified: $Date: 2010-12-14 13:27:42 +0100 (mar., 14 déc. 2010) $
10  //
11  // This program is free software; you can redistribute it and/or modify it under
12  // the terms of the GNU General Public License as published by the Free Software
13  // Foundation; either version 2 of the License, or (at your option) any later
14  // version. This program is distributed in the hope that it will be useful, but
15  // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16  // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17  // details. You should have received a copy of the GNU General Public License
18  // along with this program; if not, write to the Free Software Foundation, Inc.,
19  // 675 Mass Ave, Cambridge, MA 02139, USA.
20  package wjhk.jupload2.upload;
21  
22  import java.io.IOException;
23  import java.io.OutputStream;
24  import java.io.UnsupportedEncodingException;
25  import java.net.URL;
26  import java.net.URLDecoder;
27  import java.net.URLEncoder;
28  import java.util.HashMap;
29  import java.util.Iterator;
30  import java.util.Map;
31  import java.util.Set;
32  import java.util.concurrent.BlockingQueue;
33  
34  import wjhk.jupload2.exception.JUploadException;
35  import wjhk.jupload2.exception.JUploadIOException;
36  import wjhk.jupload2.policies.UploadPolicy;
37  import wjhk.jupload2.upload.helper.ByteArrayEncoder;
38  import wjhk.jupload2.upload.helper.ByteArrayEncoderHTTP;
39  import wjhk.jupload2.upload.helper.HTTPConnectionHelper;
40  
41  /**
42   * This class implements the file upload via HTTP POST request.
43   * 
44   * @author etienne_sf
45   * @version $Revision: 1469 $
46   */
47  public class FileUploadThreadHTTP extends DefaultFileUploadThread {
48  
49      /**
50       * The current connection helper. No initialization now: we need to wait for
51       * the startRequest method, to have all needed information.
52       */
53      private HTTPConnectionHelper connectionHelper = null;
54  
55      /**
56       * local head within the multipart post, for each file. This is
57       * precalculated for all files, in case the upload is not chunked. The heads
58       * length are counted in the total upload size, to check that it is less
59       * than the maxChunkSize. tails are calculated once, as they depend not of
60       * the file position in the upload.
61       */
62      private HashMap<UploadFileData, ByteArrayEncoder> heads = null;
63  
64      /**
65       * same as heads, for the ... tail in the multipart post, for each file. But
66       * tails depend on the file position (the boundary is added to the last
67       * tail). So it's to be calculated for each upload.
68       */
69      private HashMap<UploadFileData, ByteArrayEncoder> tails = null;
70  
71      /**
72       * Creates a new instance.
73       * 
74       * @param uploadPolicy The policy to be applied.
75       * @param packetQueue The queue from wich packets to upload are available.
76       * @param fileUploadManagerThread
77       */
78      public FileUploadThreadHTTP(UploadPolicy uploadPolicy,
79              BlockingQueue<UploadFilePacket> packetQueue,
80              FileUploadManagerThread fileUploadManagerThread) {
81          super("FileUploadThreadHTTP thread", packetQueue, uploadPolicy,
82                  fileUploadManagerThread);
83          this.uploadPolicy.displayDebug("  Using " + this.getClass().getName(),
84                  30);
85  
86          uploadPolicy.displayDebug("Upload done by using the "
87                  + getClass().getName() + " class", 30);
88          // Name the thread (useful for debugging)
89          setName("FileUploadThreadHTTP");
90          // FIXME There are two such initializations in this class. Necessary ??
91          this.connectionHelper = new HTTPConnectionHelper(uploadPolicy);
92      }
93  
94      /** @see DefaultFileUploadThread#beforeRequest(UploadFilePacket) */
95      @Override
96      void beforeRequest(UploadFilePacket packet) throws JUploadException {
97          if (this.connectionHelper != null) {
98              // It must be retring an upload. We clear any previous work.
99              this.connectionHelper.dispose();
100         }
101         this.connectionHelper = new HTTPConnectionHelper(uploadPolicy);
102         setAllHead(packet, this.connectionHelper.getBoundary());
103         setAllTail(packet, this.connectionHelper.getBoundary());
104     }
105 
106     /** @see DefaultFileUploadThread#getAdditionnalBytesForUpload(UploadFileData) */
107     @Override
108     long getAdditionnalBytesForUpload(UploadFileData uploadFileData)
109             throws JUploadIOException {
110         return this.heads.get(uploadFileData).getEncodedLength()
111                 + this.tails.get(uploadFileData).getEncodedLength();
112     }
113 
114     /** @see DefaultFileUploadThread#afterFile(UploadFileData) */
115     @Override
116     void afterFile(UploadFileData uploadFileData) throws JUploadIOException {
117         this.connectionHelper.append(this.tails.get(uploadFileData));
118         this.uploadPolicy.displayDebug("--- filetail start (len="
119                 + this.tails.get(uploadFileData).getEncodedLength() + "):", 70);
120         this.uploadPolicy.displayDebug(quoteCRLF(this.tails.get(uploadFileData)
121                 .getString()), 70);
122         this.uploadPolicy.displayDebug("--- filetail end", 70);
123     }
124 
125     /** @see DefaultFileUploadThread#beforeFile(UploadFilePacket, UploadFileData) */
126     @Override
127     void beforeFile(UploadFilePacket uploadFilePacket,
128             UploadFileData uploadFileData) throws JUploadException {
129         // heads[i] contains the header specific for the file, in the multipart
130         // content.
131         // It is initialized at the beginning of the run() method. It can be
132         // override at the beginning of this loop, if in chunk mode.
133         try {
134             this.connectionHelper.append(this.heads.get(uploadFileData)
135                     .getEncodedByteArray());
136 
137             // Debug output: always called, so that the debug file is correctly
138             // filled.
139             this.uploadPolicy.displayDebug("--- fileheader start (len="
140                     + this.heads.get(uploadFileData).getEncodedLength() + "):",
141                     70);
142             this.uploadPolicy.displayDebug(quoteCRLF(this.heads.get(
143                     uploadFileData).getString()), 70);
144             this.uploadPolicy.displayDebug("--- fileheader end", 70);
145         } catch (Exception e) {
146             throw new JUploadException(e);
147         }
148     }
149 
150     /** @see DefaultFileUploadThread#cleanAll() */
151     @Override
152     void cleanAll() throws JUploadException {
153         // Nothing to do in HTTP mode.
154     }
155 
156     /** @see DefaultFileUploadThread#cleanRequest() */
157     @Override
158     void cleanRequest() throws JUploadException {
159         try {
160             this.connectionHelper.dispose();
161         } catch (JUploadIOException e) {
162             this.uploadPolicy.displayErr(this.uploadPolicy
163                     .getLocalizedString("errDuringUpload"), e);
164             throw e;
165         }
166     }
167 
168     @Override
169     int finishRequest() throws JUploadException {
170         if (this.uploadPolicy.getDebugLevel() > 100) {
171             // Let's have a little time to check the upload messages written on
172             // the progress bar.
173             try {
174                 Thread.sleep(400);
175             } catch (InterruptedException e) {
176             }
177         }
178         int status = this.connectionHelper.readHttpResponse();
179         setResponseMsg(this.connectionHelper.getResponseMsg());
180         setResponseBody(this.connectionHelper.getResponseBody());
181         return status;
182     }
183 
184     /**
185      * When interrupted, we close all network connection.
186      */
187     @Override
188     void interruptionReceived() {
189         // FIXME: this should manage chunked upload (to free temporary files on
190         // the server)
191         try {
192             if (this.connectionHelper != null) {
193                 this.connectionHelper.dispose();
194                 this.connectionHelper = null;
195             }
196 
197             if (this.heads != null) {
198                 for (UploadFileData uploadFileData : this.heads.keySet()) {
199                     ByteArrayEncoder bae = this.heads.get(uploadFileData);
200                     if (bae != null) {
201                         bae.close();
202                     }
203                 }
204                 this.heads = null;
205             }
206             if (this.tails != null) {
207                 for (UploadFileData uploadFileData : this.tails.keySet()) {
208                     ByteArrayEncoder bae = this.tails.get(uploadFileData);
209                     if (bae != null) {
210                         bae.close();
211                     }
212                 }
213                 this.tails = null;
214             }
215         } catch (Exception e) {
216             this.uploadPolicy.displayWarn("Exception in "
217                     + getClass().getName() + ".interruptionReceived() ("
218                     + e.getClass().getName() + "): " + e.getMessage());
219         }
220     }
221 
222     /**
223      * @see DefaultFileUploadThread#getResponseBody()
224      * @Override String getResponseBody() { return
225      *           this.sbHttpResponseBody.toString(); }
226      */
227     /** @see DefaultFileUploadThread#getOutputStream() */
228     @Override
229     OutputStream getOutputStream() throws JUploadException {
230         return this.connectionHelper.getOutputStream();
231     }
232 
233     /** @see DefaultFileUploadThread#startRequest(long, boolean, int, boolean) */
234     @Override
235     void startRequest(long contentLength, boolean bChunkEnabled, int chunkPart,
236             boolean bLastChunk) throws JUploadException {
237 
238         try {
239             String chunkHttpParam = "jupart=" + chunkPart + "&jufinal="
240                     + (bLastChunk ? "1" : "0");
241             this.uploadPolicy.displayDebug("chunkHttpParam: " + chunkHttpParam,
242                     30);
243 
244             URL url = new URL(this.uploadPolicy.getPostURL());
245 
246             // Add the chunking query params to the URL if there are any
247             if (bChunkEnabled) {
248                 if (null != url.getQuery() && !"".equals(url.getQuery())) {
249                     url = new URL(url.toExternalForm() + "&" + chunkHttpParam);
250                 } else {
251                     url = new URL(url.toExternalForm() + "?" + chunkHttpParam);
252                 }
253             }
254 
255             this.connectionHelper.initRequest(url, "POST", bChunkEnabled,
256                     bLastChunk);
257 
258             // Get the GET parameters from the URL and convert them to
259             // post form params
260             ByteArrayEncoder formParams = getFormParamsForPostRequest(url);
261             contentLength += formParams.getEncodedLength();
262 
263             this.connectionHelper.append(
264                     "Content-Type: multipart/form-data; boundary=").append(
265                     this.connectionHelper.getBoundary().substring(2)).append(
266                     "\r\n");
267             this.connectionHelper.append("Content-Length: ").append(
268                     String.valueOf(contentLength)).append("\r\n");
269 
270             // Blank line (end of header)
271             this.connectionHelper.append("\r\n");
272 
273             // formParams are not really part of the main header, but we add
274             // them here anyway. We write directly into the
275             // ByteArrayOutputStream, as we already encoded them, to get the
276             // encoded length. We need to flush the writer first, before
277             // directly writing to the ByteArrayOutputStream.
278             this.connectionHelper.append(formParams);
279 
280             // Let's call the server
281             this.connectionHelper.sendRequest();
282 
283             // Debug output: always called, so that the debug file is correctly
284             // filled.
285             this.uploadPolicy.displayDebug("=== main header (len="
286                     + this.connectionHelper.getByteArrayEncoder()
287                             .getEncodedLength()
288                     + "):\n"
289                     + quoteCRLF(this.connectionHelper.getByteArrayEncoder()
290                             .getString()), 70);
291             this.uploadPolicy.displayDebug("=== main header end", 70);
292         } catch (IOException e) {
293             throw new JUploadIOException(e);
294         } catch (IllegalArgumentException e) {
295             throw new JUploadException(e);
296         }
297     }
298 
299     // ////////////////////////////////////////////////////////////////////////////////////
300     // /////////////////////// PRIVATE METHODS
301     // ////////////////////////////////////////////////////////////////////////////////////
302     /**
303      * Returns the header for this file, within the http multipart body.
304      * 
305      * @param numInCurrentUpload Index of the file in the array that contains
306      *            all files to upload.
307      * @param bound The boundary that separate files in the http multipart post
308      *            body.
309      * @param chunkPart The numero of the current chunk (from 1 to n)
310      * @return The encoded header for this file. The {@link ByteArrayEncoder} is
311      *         closed within this method.
312      * @throws JUploadException
313      */
314     private final ByteArrayEncoder getFileHeader(UploadFileData uploadFileData,
315             int numInCurrentUpload, String bound, int chunkPart)
316             throws JUploadException {
317         String filenameEncoding = this.uploadPolicy.getFilenameEncoding();
318         String mimetype = uploadFileData.getMimeType();
319         String uploadFilename = uploadFileData
320                 .getUploadFilename(numInCurrentUpload);
321         ByteArrayEncoder bae = new ByteArrayEncoderHTTP(this.uploadPolicy,
322                 bound);
323 
324         if (numInCurrentUpload == 0) {
325             // Only once when uploading multiple files in same request.
326             // We'll encode the output stream into UTF-8.
327             String form = this.uploadPolicy.getFormdata();
328             if (null != form) {
329                 bae.appendFormVariables(form);
330             }
331         }
332         // We ask the current FileData to add itself its properties.
333         uploadFileData.appendFileProperties(bae, numInCurrentUpload);
334 
335         // boundary.
336         bae.append(bound).append("\r\n");
337 
338         // Content-Disposition.
339         bae.append("Content-Disposition: form-data; name=\"");
340         bae.append(uploadFileData.getUploadName(numInCurrentUpload)).append(
341                 "\"; filename=\"");
342         if (filenameEncoding == null) {
343             bae.append(uploadFilename);
344         } else {
345             try {
346                 this.uploadPolicy.displayDebug("Encoded filename: "
347                         + URLEncoder.encode(uploadFilename, filenameEncoding),
348                         70);
349                 bae.append(URLEncoder.encode(uploadFilename, filenameEncoding));
350             } catch (UnsupportedEncodingException e) {
351                 this.uploadPolicy
352                         .displayWarn(e.getClass().getName() + ": "
353                                 + e.getMessage()
354                                 + " (in UploadFileData.getFileHeader)");
355                 bae.append(uploadFilename);
356             }
357         }
358         bae.append("\"\r\n");
359 
360         // Line 3: Content-Type.
361         bae.append("Content-Type: ").append(mimetype).append("\r\n");
362 
363         // An empty line to finish the header.
364         bae.append("\r\n");
365 
366         // The ByteArrayEncoder is now filled.
367         bae.close();
368         return bae;
369     }// getFileHeader
370 
371     /**
372      * Construction of the head for each file.
373      * 
374      * @param bound The String boundary between the post data in the HTTP
375      *            request.
376      * @throws JUploadException
377      */
378     private final void setAllHead(UploadFilePacket packet, String bound)
379             throws JUploadException {
380         this.heads = new HashMap<UploadFileData, ByteArrayEncoder>(packet
381                 .size());
382         int numInCurrentUpload = 0;
383         for (UploadFileData uploadFileData : packet) {
384             this.heads.put(uploadFileData, getFileHeader(uploadFileData,
385                     numInCurrentUpload++, bound, -1));
386         }
387     }
388 
389     /**
390      * Construction of the tail for each file.
391      * 
392      * @param bound Current boundary, to apply for these tails.
393      */
394     private final void setAllTail(UploadFilePacket packet, String bound)
395             throws JUploadException {
396         this.tails = new HashMap<UploadFileData, ByteArrayEncoder>(packet
397                 .size());
398         for (int i = 0; i < packet.size(); i++) {
399             // We'll encode the output stream into UTF-8.
400             ByteArrayEncoder bae = new ByteArrayEncoderHTTP(this.uploadPolicy,
401                     bound);
402 
403             bae.append("\r\n");
404 
405             if (this.uploadPolicy.getSendMD5Sum()) {
406                 bae.appendTextProperty("md5sum", packet.get(i).getMD5(), i);
407             }
408 
409             // The last tail gets an additional "--" in order to tell the
410             // server we have finished.
411             if (i == packet.size() - 1) {
412                 bae.append(bound).append("--\r\n");
413             }
414 
415             // Let's store this tail.
416             bae.close();
417 
418             this.tails.put(packet.get(i), bae);
419         }
420 
421     }
422 
423     /**
424      * Converts the parameters in GET form to post form
425      * 
426      * @param url the <code>URL</code> containing the query parameters
427      * @return the parameters in a string in the correct form for a POST request
428      * @throws JUploadIOException
429      */
430     private final ByteArrayEncoder getFormParamsForPostRequest(final URL url)
431             throws JUploadIOException {
432 
433         // Use a string buffer
434         // We'll encode the output stream into UTF-8.
435         ByteArrayEncoder bae = new ByteArrayEncoderHTTP(this.uploadPolicy,
436                 this.connectionHelper.getBoundary());
437 
438         // Get the query string
439         String query = url.getQuery();
440 
441         if (null != query) {
442             // Split this into parameters
443             HashMap<String, String> requestParameters = new HashMap<String, String>();
444             String[] paramPairs = query.split("&");
445             String[] oneParamArray;
446 
447             // TODO This could be much more simple !
448 
449             // Put the parameters correctly to the Hashmap
450             for (String param : paramPairs) {
451                 if (param.contains("=")) {
452                     oneParamArray = param.split("=");
453                     if (oneParamArray.length > 1) {
454                         // There is a value for this parameter
455                         try {
456                             // Correction of URL double encoding bug
457                             requestParameters.put(oneParamArray[0], URLDecoder
458                                     .decode(oneParamArray[1], "UTF-8"));
459                         } catch (UnsupportedEncodingException e) {
460                             throw new JUploadIOException(e.getClass().getName()
461                                     + ": " + e.getMessage()
462                                     + " (when trying to decode "
463                                     + oneParamArray[1] + ")");
464                         }
465                     } else {
466                         // There is no value for this parameter
467                         requestParameters.put(oneParamArray[0], "");
468                     }
469                 }
470             }
471 
472             // Now add one multipart segment for each
473             Set<Map.Entry<String, String>> entrySet = requestParameters
474                     .entrySet();
475             Map.Entry<String, String> entry;
476             Iterator<Map.Entry<String, String>> i = entrySet.iterator();
477             while (i.hasNext()) {
478                 entry = i.next();
479                 bae.appendTextProperty(entry.getKey(), entry.getValue(), -1);
480             }
481         }
482         // Return the body content
483         bae.close();
484 
485         return bae;
486     }// getFormParamsForPostRequest
487 }