// // $Id: DefaultFileUploadThread.java 287 2007-06-17 09:07:04 +0000 (dim., 17 // juin 2007) felfert $ // // jupload - A file upload applet. // Copyright 2007 The JUpload Team // // Created: ? // Creator: William JinHua Kwong // Last modified: $Date: 2010-06-29 16:49:10 -0300 (Ter, 29 Jun 2010) $ // // This program is free software; you can redistribute it and/or modify it under // the terms of the GNU General Public License as published by the Free Software // Foundation; either version 2 of the License, or (at your option) any later // version. This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more // details. You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software Foundation, Inc., // 675 Mass Ave, Cambridge, MA 02139, USA. package wjhk.jupload2.upload; import java.io.OutputStream; import java.net.SocketException; import java.util.concurrent.BlockingQueue; import java.util.regex.Pattern; import wjhk.jupload2.exception.JUploadException; import wjhk.jupload2.exception.JUploadExceptionUploadFailed; import wjhk.jupload2.exception.JUploadIOException; import wjhk.jupload2.exception.JUploadInterrupted; import wjhk.jupload2.filedata.FileData; import wjhk.jupload2.gui.DialogUploadRetry; import wjhk.jupload2.policies.UploadPolicy; /** * This class is based on the {@link FileUploadThread} class. It's an abstract * class that contains the default implementation for the * {@link FileUploadThread} interface.
* It contains the following abstract methods, which must be implemented in the * children classes. These methods are called in this order:
  • For each * upload request (for instance, upload of 3 files with nbFilesPerRequest to 2, * makes 2 request: 2 files, then the last one):
  • try
  • * {@link #startRequest}: start of the UploadRequest.
  • Then, for each file to * upload (according to the nbFilesPerRequest and maxChunkSize applet * parameters)
  • beforeFile(int) is called before writting the bytes for * this file (or this chunk)
  • afterFile(int) is called after writting the * bytes for this file (or this chunk)
  • finishRequest()
  • * finallycleanRequest()
  • Call of cleanAll(), to clean up any used * resources, common to the whole upload.
  • */ public abstract class DefaultFileUploadThread extends Thread implements FileUploadThread { // //////////////////////////////////////////////////////////////////////// // /////////////////////// VARIABLES ////////////////////////////////////// // //////////////////////////////////////////////////////////////////////// /** * The queue that'll transmit each packet to upload to the server. */ BlockingQueue packetQueue = null; /** * The upload manager. The thread that prepares files, and is responsible to * manage the upload process. * * @see FileUploadManagerThread */ FileUploadManagerThread fileUploadManagerThread = null; /** * The upload policy contains all parameters needed to define the way files * should be uploaded, including the URL. */ protected UploadPolicy uploadPolicy = null; /** * The value of the applet parameter maxChunkSize, or its default value. */ private long maxChunkSize; // //////////////////////////////////////////////////////////////////////////////////// // /////////////////////// PRIVATE ATTRIBUTES // //////////////////////////////////////////////////////////////////////////////////// /** * Current retry number, for the resume upload feature. The first try occurs * with nbRetry=0. */ int nbRetry = 0; /** * The full response message from the server, if any. For instance, in HTTP * mode, this contains both the headers and the body. */ protected String responseMsg = ""; /** * The response message from the application. For instance, in HTTP mode, * this contains the body response.
    * Note: for easier management on the various server configurations, all end * or line characters (CR, LF or CRLF) are changed to uniform CRLF. */ protected String responseBody = ""; /** * Creates a new instance. * * @param threadName * The name of the thread, that will be displayed in the debugger * and in the logs. * @param packetQueue * The queue from wich packets to upload are available. * @param uploadPolicy * The upload policy to be applied. * @param fileUploadManagerThread * The thread that is managing the upload. */ public DefaultFileUploadThread(String threadName, BlockingQueue packetQueue, UploadPolicy uploadPolicy, FileUploadManagerThread fileUploadManagerThread) { // Thread parameters. super(threadName); // Specific stuff. this.packetQueue = packetQueue; this.uploadPolicy = uploadPolicy; this.fileUploadManagerThread = fileUploadManagerThread; // Let's read up to date upload parameters. this.maxChunkSize = this.uploadPolicy.getMaxChunkSize(); this.uploadPolicy.displayDebug("DefaultFileUploadThread created", 30); } /** * This method is called before the upload. It calls the * {@link FileData#beforeUpload()} method for all files to upload, and * prepares the progressBar bar (if any), with total number of bytes to * upload. final private void beforeUpload() throws JUploadException { for * (int i = 0; i < this.filesToUpload.length && * !this.fileUploadManager.isUploadStopped(); i++) { * this.filesToUpload[i].beforeUpload(); } } /** This methods upload * overhead for the file number indexFile in the filesDataParam given to the * constructor. For instance, in HTTP, the upload contains a head and a tail * for each files. * * @param uploadFileData * The file whose additional length is asked. * @return The additional number of bytes for this file. */ abstract long getAdditionnalBytesForUpload(UploadFileData uploadFileData) throws JUploadIOException; /** * This method is called before starting of each request. It can be used to * prepare any work, before starting the request. For instance, in HTTP, the * tail must be properly calculated, as the last one must be different from * the others.
    * The packets to send are available through the {@link #packetQueue} queue. */ abstract void beforeRequest(UploadFilePacket packet) throws JUploadException; /** * This method is called for each upload request to the server. The number * of request to the server depends on:
  • The total number of files * to upload.
  • The value of the nbFilesPerRequest applet parameter.
  • * The value of the maxChunkSize applet parameter.
  • The main objective * of this method is to open the connection to the server, where the files * to upload will be written. It should also send any header necessary for * this upload request. The {@link #getOutputStream()} methods is then * called to know where the uploaded files should be written.
    * Note: it's up to the class containing this method to internally manage * the connection. * * @param contentLength * The total number of bytes for the files (or the chunk) to * upload in this query. * @param bChunkEnabled * True if this upload is part of a file (can occurs only if the * maxChunkSize applet parameter is set). False otherwise. * @param chunkPart * The chunk number. Should be ignored if bChunkEnabled is false. * @param bLastChunk * True if in chunk mode, and this upload is the last one. Should * be ignored if bChunkEnabled is false. */ abstract void startRequest(long contentLength, boolean bChunkEnabled, int chunkPart, boolean bLastChunk) throws JUploadException; /** * This method is called at the end of each request. * * @return The response status code from the server (200 == OK) * @see #startRequest(long, boolean, int, boolean) */ abstract int finishRequest() throws JUploadException; /** * Reaction of the upload thread, when an interruption has been received. * This method should close all resource to the server, to allow the server * to free any resource (temporary file, network connection...). */ abstract void interruptionReceived(); /** * This method is called before sending the bytes corresponding to the file * whose index is given in argument. If the file is splitted in chunks (see * the maxChunkSize applet parameter), this method is called before each * chunk for this file. * * @param uploadFilePacket * The bunch of files in the current request * @param uploadFileData * The next file that will be sent */ abstract void beforeFile(UploadFilePacket uploadFilePacket, UploadFileData uploadFileData) throws JUploadException; /** * Idem as {@link #beforeFile(UploadFilePacket, UploadFileData)}, but is * called after each file (and each chunks for each file). * * @param uploadFileData * The file that was just sent. */ abstract void afterFile(UploadFileData uploadFileData) throws JUploadException; /** * Clean any used resource of the last executed request. In HTTP mode, the * output stream, input stream and the socket should be cleaned here. */ abstract void cleanRequest() throws JUploadException; /** * Clean any used resource, like a 'permanent' connection. This method is * called after the end of the last request (see on the top of this page for * details). */ abstract void cleanAll() throws JUploadException; /** * Get the output stream where the files should be written for upload. * * @return The target output stream for upload. */ abstract OutputStream getOutputStream() throws JUploadException; /** * Return the the body for the server response. That is: the server response * without the http header. This is the functional response from the server * application, that has been as the HTTP reply body, for instance: all * 'echo' PHP commands.
    * * @return The last application response (HTTP body, in HTTP upload) */ public String getResponseBody() { return this.responseBody; } /** * Get the server Output. * * @return The status message from the first line of the response (e.g. "200 * OK"). */ public String getResponseMsg() { return this.responseMsg; } /** * @return the packetQueue */ public BlockingQueue getPacketQueue() { return packetQueue; } /** * Unused Store the String that contains the server response body. * * @param body * The response body that has been read. */ void setResponseBody(String body) { this.responseBody = normalizeCRLF(body); } /** * Add a String that has been read from the server response. * * @param msg * The status message from the first line of the response (e.g. * "200 OK"). */ void setResponseMsg(String msg) { this.responseMsg = normalizeCRLF(msg); } // //////////////////////////////////////////////////////////////////////////////////// // /////////////////////// PRIVATE FUNCTIONS // //////////////////////////////////////////////////////////////////////////////////// /** * This method waits for a packet on the packetQueue. Then, it calls the * doUpload() method, to send these files to the server. */ @Override final public void run() { this.uploadPolicy.displayDebug("Start of the FileUploadThread", 5); // We'll stop the upload if an error occurs. So the try/catch is // outside the while. while (!this.fileUploadManagerThread.isUploadFinished()) { UploadFilePacket packet = null; try { // We take the next packet. This method will block until a // packet is ready. packet = packetQueue.take(); // If the packet is 'poisonned', then it's the standard end of // work. if (packet.isPoisonned()) { break; } // ///////////////////////////////////////////////////////////////////////////////// // Let's go to work : THIS IS THE UPLOAD, surrounded by the // RESUME LOOP // ///////////////////////////////////////////////////////////////////////////////// nbRetry = 0; while (true) { try { // Let's try to upload the current packet. doUpload(packet); // If we are here, the last upload is a success. Let's // exit the loop. break; } catch (JUploadException jue) { // manageRetry throw the exception, if no retry should // be done. manageRetry(jue); // If we are here, the applet should retry the upload. // We let it loop again. nbRetry += 1; } }// while(resume) // ///////////////////////////////////////////////////////////////////////////////// // //////////////// ENF OF RESUME LOOP // ///////////////////////////////////////////////////////////////////////////////// this.uploadPolicy.displayDebug("After do upload", 50); } catch (InterruptedException e) { this.uploadPolicy.displayWarn(this.getClass().getName() + ".run(): received in " + e.getClass().getName() + ", exiting..."); break; } catch (JUploadException e) { if (this.fileUploadManagerThread.isUploadFinished()) { // We ignore the error this.uploadPolicy.displayWarn("Ignoring " + e.getClass().getName() + " because upload is finished"); } else { this.fileUploadManagerThread.setUploadException(e); } } catch (JUploadInterrupted e) { // The upload has been interrupted, probably by the user // (stop // button). The fileManagerUploadThread aleady knows this. this.uploadPolicy.displayInfo("Upload stopped by the user"); this.uploadPolicy.displayDebug(e.getMessage(), 30); } finally { // Let's free any locked resource for the current packet. // This is done here, to allow the resume feature (and, even in // case an error occurs, we free resources only after the last // retry) for (UploadFileData uploadFileData : packet) { if (uploadFileData.isPreparedForUpload()) { uploadFileData.afterUpload(); } } } }// while (!isUploadFinished) this.uploadPolicy.displayDebug("End of the FileUploadThread", 5); }// run /** * @param jue * @throws JUploadException */ private void manageRetry(JUploadException jue) throws JUploadException { String exceptionCauseClass = (jue.getCause() == null) ? "no exception cause" : jue.getCause().getClass().getName(); String errMsg = jue.getClass().getName() + " (" + jue.getMessage() + "), caused by: " + exceptionCauseClass; if (this.fileUploadManagerThread.isUploadFinished()) { // The upload is stopped. This error may be caused this.uploadPolicy .displayWarn("The following error occurs, but the upload is stopped, ignoring it ]" + errMsg + "]"); throw jue; } else if (jue.getCause() instanceof SocketException) { this.uploadPolicy.displayWarn("A 'resumable' error occurred: " + errMsg); // If it was the last retry, we stop here. if (nbRetry >= this.uploadPolicy.getRetryMaxNumberOf()) { this.uploadPolicy.displayWarn("Too much retries (" + nbRetry + "), exiting..."); throw jue; } DialogUploadRetry dialogUploadRetry = new DialogUploadRetry( this.uploadPolicy.getContext().getFrame(), jue, nbRetry, this.uploadPolicy); // The constructor returns, when the dialog is closed. Let's check // the user answer: if (dialogUploadRetry.isRetryValidated()) { this.uploadPolicy.displayDebug( "The user (or the timer) choosed to retry the upload", 30); } else { this.uploadPolicy.displayDebug( "The user refuses to retry the upload, exiting...", 30); // No retry, let's note the exception and go out throw jue; }// End of resumable exceptions management. } else { // This exception can't be resumed. We transmit it. this.uploadPolicy .displayWarn("Non resumable error occured, exiting..."); throw jue; } } /** * Actual execution file(s) upload. It's called by the run methods, once for * all files, or file by file, depending on the UploadPolicy. The list of * files to upload is stored in the packet parameter.
    * This method is called by the run() method. The prerequisite about the * filesToUpload array are:
  • If the sum of contentLength for the * files in the array is more than the maxChunkSize, then * nbFilesToUploadParam is one.
  • The number of elements in filesToUpload * is less (or equal) than the nbMaxFilesPerUpload.
  • * * @throws JUploadException * @throws JUploadInterrupted * Thrown when an interruption of the thread is detected. */ final private void doUpload(UploadFilePacket packet) throws JUploadException, JUploadInterrupted { boolean bChunkEnabled = false; long totalContentLength = 0; long totalFileLength = 0; // We are about to start a new upload. this.fileUploadManagerThread.setUploadStatus(packet, packet.get(0), FileUploadManagerThread.UPLOAD_STATUS_UPLOADING); // Prepare upload, for all files to be uploaded. beforeRequest(packet); for (UploadFileData uploadFileData : packet) { // The upload may be finished, while we're working on the files... if (this.fileUploadManagerThread.isUploadFinished()) { // Let's stop our work here. return; } // Total length, for HTTP upload. totalContentLength += uploadFileData.getUploadLength(); totalContentLength += getAdditionnalBytesForUpload(uploadFileData); // Total file length: used to manage the progress bar (we don't // follow the bytes uploaded within headers and forms). totalFileLength += uploadFileData.getUploadLength(); this.uploadPolicy.displayDebug("file " + uploadFileData.getFileName() + ": content=" + uploadFileData.getUploadLength() + " bytes, getAdditionnalBytesForUpload=" + getAdditionnalBytesForUpload(uploadFileData) + " bytes", 50); }// for // Ok, now we check that the totalContentLength is less than the chunk // size. if (totalFileLength >= this.maxChunkSize) { // hum, hum, we have to download file by file, with chunk enabled. // This a prerequisite of this method. if (packet.size() > 1) { this.fileUploadManagerThread .setUploadException(new JUploadException( "totalContentLength >= chunkSize: this.filesToUpload.length should be 1 (doUpload)")); } bChunkEnabled = true; } // Now, we can actually do the job. This is delegate into smaller // method, for easier understanding. if (bChunkEnabled) { // No more than one file, when in chunk mode. if (packet.size() > 1) { throw new JUploadException( "totalContentLength >= chunkSize: this.filesToUpload.length should not be more than 1 (doUpload)"); } doChunkedUpload(packet); } else { doNonChunkedUpload(packet, totalContentLength, totalFileLength); } // If the request properly finished, we remove the files from the list // of files to upload. if (this.fileUploadManagerThread.getUploadException() == null && !this.fileUploadManagerThread.isUploadStopped()) { this.fileUploadManagerThread.currentRequestIsFinished(packet); } } /** * Execution of an upload, in chunk mode. This method expects that the given * packet contains only one file. * * @param packet * The packet that contains the file to upload in chunk mode * @throws JUploadException * When any error occurs, or when there is more than one file in * packet. * @throws JUploadInterrupted * Thrown when an interruption of the thread is detected. */ final private void doChunkedUpload(UploadFilePacket packet) throws JUploadException, JUploadInterrupted { boolean bLastChunk = false; int chunkPart = 0; long contentLength = 0; long thisChunkSize = 0; if (packet.size() > 1) { throw new JUploadException( "doChunkedUpload called with a packet of more than 1 file (" + packet.size() + " files)"); } UploadFileData uploadFileData = packet.get(0); // This while enables the chunk management: // In chunk mode, it loops until the last chunk is uploaded. This works // only because, in chunk mode, files are uploaded one y one (the for // loop within the while loops through ... 1 unique file). // In normal mode, it does nothing, as the bLastChunk is set to true in // the first test, within the while. while (!bLastChunk && !this.fileUploadManagerThread.isUploadFinished()) { // Let's manage chunk: // Files are uploaded one by one. This is checked just above. chunkPart += 1; bLastChunk = (contentLength > uploadFileData.getRemainingLength()); // Is this the last chunk ? if (bLastChunk) { thisChunkSize = uploadFileData.getRemainingLength(); } else { thisChunkSize = this.maxChunkSize; } contentLength = thisChunkSize + getAdditionnalBytesForUpload(uploadFileData); // We are about to start a new upload. this.fileUploadManagerThread.setUploadStatus(packet, uploadFileData, FileUploadManagerThread.UPLOAD_STATUS_UPLOADING); // Ok, we've prepare the job for chunk upload. Let's do it! startRequest(contentLength, true, chunkPart, bLastChunk); // Let's add any file-specific header. beforeFile(packet, uploadFileData); // Actual upload of the file: uploadFileData.uploadFile(getOutputStream(), thisChunkSize); // If we are not in chunk mode, or if it was the last chunk, // upload should be finished. if (bLastChunk && uploadFileData.getRemainingLength() > 0) { throw new JUploadExceptionUploadFailed( "Files has not be entirely uploaded. The remaining size is " + uploadFileData.getRemainingLength() + " bytes. File size was: " + uploadFileData.getUploadLength() + " bytes."); } // Let's add any file-specific header. afterFile(uploadFileData); // Let's finish the request, and wait for the server Output, if // any (not applicable in FTP) int status = finishRequest(); if (bLastChunk) { // We are finished with this one. Let's display it. this.fileUploadManagerThread .setUploadStatus( packet, uploadFileData, FileUploadManagerThread.UPLOAD_STATUS_FILE_UPLOADED_WAITING_FOR_RESPONSE); } else { // We are finished with the current chunk, but not with the // file. Let's display it. this.fileUploadManagerThread .setUploadStatus( packet, uploadFileData, FileUploadManagerThread.UPLOAD_STATUS_CHUNK_UPLOADED_WAITING_FOR_RESPONSE); } // We now ask to the uploadPolicy, if it was a success. // If not, the isUploadSuccessful should raise an exception. this.uploadPolicy.checkUploadSuccess(status, getResponseMsg(), getResponseBody()); cleanRequest(); }// while // Let's tell our manager that we've done the job! this.fileUploadManagerThread.anotherFileHasBeenSent(packet, uploadFileData); }// doChunkedUpload /** * Execution of an upload, in standard mode. This method uploads all files * in the given packet. * * @param packet * The files to upload in the current request to the server * @param totalContentLength * The total size of the upload, including any protocol-specific * header or footer. * @param totalFileLength * The sum of each file length. * @throws JUploadException * When any error occurs * @throws JUploadInterrupted * Thrown when an interruption of the thread is detected. */ final private void doNonChunkedUpload(UploadFilePacket packet, final long totalContentLength, final long totalFileLength) throws JUploadException, JUploadInterrupted { // First step is to prepare all files. startRequest(totalContentLength, false, 0, true); // Then, upload each file. for (UploadFileData uploadFileData : packet) { if (this.fileUploadManagerThread.isUploadFinished()) { // Upload is finished (by the user or because of an error, or // instance) break; } // We are about to start a new upload. this.fileUploadManagerThread.setUploadStatus(packet, uploadFileData, FileUploadManagerThread.UPLOAD_STATUS_UPLOADING); // Let's add any file-specific header. beforeFile(packet, uploadFileData); // Actual upload of the file: if (!this.fileUploadManagerThread.isUploadFinished()) { uploadFileData.uploadFile(getOutputStream(), uploadFileData .getUploadLength()); } // Let's add any file-specific header. if (!this.fileUploadManagerThread.isUploadFinished()) { afterFile(uploadFileData); // Let's tell our manager that we've done the job! // Ok, maybe the server will refuse it, but we won't say that // now! this.fileUploadManagerThread.anotherFileHasBeenSent(packet, uploadFileData); } } // We are finished with this one. Let's display it. if (!this.fileUploadManagerThread.isUploadFinished()) { this.fileUploadManagerThread .setUploadStatus( packet, packet.get(packet.size() - 1), FileUploadManagerThread.UPLOAD_STATUS_FILE_UPLOADED_WAITING_FOR_RESPONSE); // Let's finish the request, and wait for the server Output, if // any (not applicable in FTP) int status = finishRequest(); // We now ask to the uploadPolicy, if it was a success. // If not, the isUploadSuccessful should raise an exception. this.uploadPolicy.checkUploadSuccess(status, getResponseMsg(), getResponseBody()); } cleanRequest(); }// doNonChunkedUpload /** @see FileUploadThread#close() */ public void close() { try { cleanAll(); } catch (JUploadException e) { this.uploadPolicy.displayErr(e); } } /** * Replace single \r and \n by uniform end of line characters (CRLF). This * makes it easier, to search for string within the body. * * @param s * The original string * @return The string with single \r and \n modified changed to CRLF (\r\n). */ public final String normalizeCRLF(String s) { Pattern p = Pattern.compile("\\r\\n|\\r|\\n", Pattern.MULTILINE); String[] lines = p.split(s); // Worst case: the s string contains only \n or \r characters: we then // need to triple the string length. Let's say double is enough. StringBuffer sb = new StringBuffer(s.length() * 2); for (int i = 0; i < lines.length; i += 1) { sb.append(lines[i]).append("\r\n"); } return sb.toString(); } /** * Replace \r and \n by correctly displayed end of line characters. Used to * display debug output. It also replace any single \r or \n by \r\n, to * make it easier, to search for string within the body. * * @param s * The original string * @return The string with \r and \n modified, to be correctly displayed. */ public final String quoteCRLF(String s) { return s.replaceAll("\r\n", "\\\\r\\\\n\n"); } /** * {@inheritDoc} */ public void setFileUploadThreadManager( FileUploadManagerThread fileUploadManagerThread) throws JUploadException { if (this.fileUploadManagerThread != null) { throw new JUploadException( "Can not override fileUploadManagerThread (in DefaultFileUpload.setFileUploadThreadManager()"); } this.fileUploadManagerThread = fileUploadManagerThread; } }