/*
 * File   : $Source: /usr/local/cvs/opencms/src-components/org/opencms/applet/upload/FileUploadApplet.java,v $
 * Date   : $Date: 2008-02-27 12:05:52 $
 * Version: $Revision: 1.28 $
 *
 * This library is part of OpenCms -
 * the Open Source Content Management System
 *
 * Copyright (c) 2002 - 2008 Alkacon Software GmbH (http://www.alkacon.com)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * For further information about Alkacon Software GmbH, please see the
 * company website: http://www.alkacon.com
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.opencms.applet.upload;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.StringTokenizer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.swing.JApplet;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.filechooser.FileFilter;

import org.apache.commons.httpclient.Cookie;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;
import org.apache.commons.httpclient.params.HttpConnectionParams;
import org.apache.commons.httpclient.params.HttpMethodParams;

/**
 * File Upload Applet, displays a file selector box to upload multiple resources into OpenCms.<p>
 * 
 * @author Michael Emmerich 
 * 
 * @version $Revision: 1.28 $ 
 * 
 * @since 6.0.0 
 */
public class FileUploadApplet extends JApplet implements Runnable {

    /** The JSESSIONID cookie header name. */
    public static final String C_JSESSIONID = "JSESSIONID";

    /** The value for the resource upload applet action. */
    // Warning: This constant has to be kept in sync with the same named constant in 
    // org.opencms.explorer.CmsNewResourceUpload
    public static final String DIALOG_CHECK_OVERWRITE = "checkoverwrite";

    /** Serial version UID required for safe serialization. */
    private static final long serialVersionUID = -3710093915699772778L;

    /** Output string for action messages. */
    private String m_action = "";

    private String m_actionOutputCount = "Counting resources ....";

    private String m_actionOutputCreate = "Creating Zip-File...";

    private String m_actionOutputError = "Error";

    /** Defintion of output strings.*/
    private String m_actionOutputSelect = "Seleting files for upload....";

    private String m_actionOutputUpload = "Upload Zip-File";

    private String m_actionOverwriteCheck = "Checking file existance on server...";

    /** Indicates if the applet certificate has been accepted. */
    private boolean m_certificateAccepted;

    private String m_certificateErrorMessage = "The required Applet certificate has not been accepted!";

    private String m_certificateErrorTitle = "Error initializing the OpenCms Upload Applet";

    /** The initial folder path for the file chooser. */
    private String m_clientFolder = null;

    /** Color defintions. */
    private HashMap m_colors = new HashMap();

    private String m_errorLine1 = "An error has occurred on the server:";

    /** The URL to return to after an error. */
    private String m_errorUrl = "";

    /** File extensions, used to find the correct icons for the selectbox. */
    private String m_fileExtensions = "";

    /** The type of gallery to upload to. */
    private String m_fileFilterSelection = "";

    /** The file selector. */
    private UploadAppletFileChooser m_fileSelector;

    private Image m_floater;
    /** Image position for the floater during upload. */
    private int m_floaterPos = 50;
    /** Definition variables for graphics output. */
    private Font m_font;

    /** Maximum file upload size. */
    private long m_maxsize = -1;

    /** Output string for loggin messages. */
    private String m_message = "";
    private String m_messageNoPreview = "no preview available";
    private String m_messageOutputAdding = "Adding ";
    private String m_messageOutputErrorSize = "Zip file too big:";
    private String m_messageOutputErrorZip = "Error creating Zip-File, see Java Console.";
    private String m_messageOutputUpload = "Please wait, uploading data...";
    private FontMetrics m_metrics;
    private Graphics m_offgraphics;
    private Image m_offscreen;

    /** The URL of the OpenCms instance. */
    private String m_opencms = "";

    /** Output mode selector. */
    private int m_outputMode;

    private ModalDialog m_overwriteDialog;
    private String m_overwriteDialogCancel = "Cancel";
    private String m_overwriteDialogIntro = "The files listed below already exist on the server. \nAll checked files will be overwritten.";
    private String m_overwriteDialogLocale = "en";
    private String m_overwriteDialogOk = "Ok";
    private String m_overwriteDialogTitle = "Select the files to overwrite on the server";

    /** List of potential overwrites. */
    private List m_overwrites;

    /** The Target Frame to return to after uploading the files. */
    private String m_redirectTargetFrame = "";

    /** The URL to return to after uploading the files. */
    private String m_redirectUrl = "";

    /** Number of resources to upload. */
    private int m_resources;

    /** Applet thread. */
    private Thread m_runner;

    /** Definition of the images during upload. */
    private Image m_source;

    /** Counter for creating the progress bar. */
    private int m_step;

    private Image m_target;

    /** The URL to send the uploaded files to. */
    private String m_targetUrl = "";

    /** The name of the folder to upload to. */
    private String m_uploadFolder = "";

    /**
     * Adds a single file to the zip output.<p>
     * 
     * @param zipStream the zip output stream
     * @param file the file to add to the stream
     * @throws Exception if something goes wrong
     */
    private void addFileToZip(ZipOutputStream zipStream, File file) throws Exception {

        // add to zipfile
        String name = file.getAbsolutePath().replace('\\', '/');
        name = name.substring(m_fileSelector.getCurrentDirectory().getAbsolutePath().length());

        m_message = m_messageOutputAdding + " " + name + "..";
        m_step++;
        repaint();
        ZipEntry entry = new ZipEntry(name);
        zipStream.putNextEntry(entry);
        writeFileBytes(file, zipStream);
        zipStream.closeEntry();
    }

    private void addFolderToZip(ZipOutputStream zipStream, File file) throws Exception {

        File[] children = file.listFiles();
        File child;
        for (int i = 0; i < children.length; i++) {
            child = children[i];
            if (child.isDirectory()) {
                addFolderToZip(zipStream, child);
            } else {
                addFileToZip(zipStream, child);
            }
        }
    }

    /**
     * Returns the merge of both file arrays with no check for duplications. <p>
     * 
     * @param files first array of files 
     * 
     * @param overwriteFiles 2nd array of files 
     * 
     * @return the union of both file arrays
     */
    private File[] addOverwrites(File[] files, File[] overwriteFiles) {

        List result = new ArrayList(files.length + overwriteFiles.length);
        // faster for loop;)
        for (int i = files.length - 1; i >= 0; i--) {
            result.add(files[i]);
        }
        for (int i = overwriteFiles.length - 1; i >= 0; i--) {
            result.add(overwriteFiles[i]);
        }

        return (File[])result.toArray(new File[result.size()]);
    }

    /**
     * Checks if the given client files exist on the server and internally stores duplications.<p>
     * 
     * Comparison is made by cutting the current directory of the file chooser from the path of the given files. 
     * The server files (VFS files) to compare to are found by the current session of the user which finds the correct site and 
     * the knowledge about the current directory. File translation rules are taken into account on the server. <p>
     * 
     * @param files the local files to check if they exist in the VFS 
     * 
     * @return one of {@link ModalDialog#ERROR_OPTION} , {@link ModalDialog#CANCEL_OPTION}, {@link ModalDialog#APPROVE_OPTION}. 
     */
    int checkServerOverwrites(File[] files) {

        m_action = m_actionOverwriteCheck;
        repaint();
        int rtv = ModalDialog.ERROR_OPTION;
        // collect files
        List fileNames = new ArrayList();
        for (int i = 0; i < files.length; i++) {
            getRelativeFilePaths(files[i], fileNames);
        }

        StringBuffer uploadFiles = new StringBuffer();
        Iterator it = fileNames.iterator();
        // Http post header is limited, therefore only a ceratain amount of files may be checked 
        // for server overwrites. Solution is: multiple requests. 
        int count = 0;
        List duplications;
        // request to server
        HttpClient client = new HttpClient();
        this.m_overwrites = new ArrayList();
        try {
            while (it.hasNext()) {
                count++;
                uploadFiles.append(((String)it.next())).append('\n');

                if (((count % 40) == 0) || (!it.hasNext())) {
                    // files to upload:
                    PostMethod post = new PostMethod(m_targetUrl);
                    Header postHeader = new Header("uploadFiles", URLEncoder.encode(uploadFiles.toString(), "utf-8"));
                    post.addRequestHeader(postHeader);
                    // upload folder in vfs: 
                    Header header2 = new Header("uploadFolder", URLEncoder.encode(getParameter("filelist"), "utf-8"));
                    post.addRequestHeader(header2);

                    // the action constant
                    post.setParameter("action", DIALOG_CHECK_OVERWRITE);

                    // add jsessionid query string
                    String sessionId = getParameter("sessionId");
                    String query = ";" + C_JSESSIONID.toLowerCase() + "=" + sessionId;
                    post.setQueryString(query);
                    post.addRequestHeader(C_JSESSIONID, sessionId);

                    HttpConnectionParams connectionParams = client.getHttpConnectionManager().getParams();
                    connectionParams.setConnectionTimeout(5000);

                    // add the session cookie
                    client.getState();
                    client.getHostConfiguration().getHost();

                    HttpState initialState = new HttpState();
                    URI uri = new URI(m_targetUrl, false);
                    Cookie sessionCookie = new Cookie(uri.getHost(), C_JSESSIONID, sessionId, "/", null, false);
                    initialState.addCookie(sessionCookie);
                    client.setState(initialState);
                    int status = client.executeMethod(post);

                    if (status == HttpStatus.SC_OK) {
                        String response = post.getResponseBodyAsString();
                        duplications = parseDuplicateFiles(URLDecoder.decode(response, "utf-8"));
                        this.m_overwrites.addAll(duplications);

                    } else {
                        // continue without overwrite check 
                        String error = m_errorLine1 + "\n" + post.getStatusLine();
                        System.err.println(error);
                    }

                    count = 0;
                    uploadFiles = new StringBuffer();
                }

            }
            if (m_overwrites.size() > 0) {
                rtv = showDuplicationsDialog(m_overwrites);
            } else {
                rtv = ModalDialog.APPROVE_OPTION;
            }

        } catch (HttpException e) {
            // TODO Auto-generated catch block
            e.printStackTrace(System.err);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace(System.err);
        }

        return rtv;
    }

    /**
     * Counts all resources to add to the zip file.<p>
     * 
     * @param files the files to be packed into the zipfile
     * @return number of resources
     */
    private int countResources(File[] files) {

        int count = 0;
        // look through all selected resources
        for (int i = 0; i < files.length; i++) {
            if (files[i].isFile()) {
                // its a file, count it
                count++;
            } else {
                // its a folder, count all resources in it and add the number
                count += countSubresources(files[i]);
            }
        }
        return count;
    }

    /**
     * Counts all resources in a folder.<p>
     * 
     * @param folder the folder to count
     * @return number of resources
     */
    private int countSubresources(File folder) {

        int count = 0;
        if (folder.isFile()) {
            // check if is really a folder
            count = 1;
        } else {
            // recurest to count
            count = countResources(folder.listFiles());
        }
        return count;
    }

    /**
     * Creates a ZipFile from all files to upload.<p>
     * 
     * @param files the files to be packed into the zipfile
     * @return reference to the zipfile
     */
    private File createZipFile(File[] files) {

        File targetFile = null;
        if (files.length > 0) {
            m_action = m_actionOutputCreate;
            try {
                // create a new zipStream
                String zipFileName = ".opencms_upload.zip";
                String userHome = System.getProperty("user.home");
                // create file in user home directory where write permissions should exist
                if (userHome != null) {
                    if (!userHome.endsWith(File.separator)) {
                        userHome = userHome + File.separator;
                    }
                    zipFileName = userHome + zipFileName;
                }
                ZipOutputStream zipStream = new ZipOutputStream(new FileOutputStream(zipFileName));
                // loop through all files
                for (int i = 0; i < files.length; i++) {

                    // if its a file, add it to the zipfile
                    if (files[i].isFile()) {
                        addFileToZip(zipStream, files[i]);
                    } else {

                        addFolderToZip(zipStream, files[i]);
                    }
                    repaint();
                    // free mem
                    files[i] = null;
                }
                zipStream.close();
                // get the zipfile
                targetFile = new File(zipFileName);
            } catch (Exception e) {
                System.err.println("Error creating zipfile " + getStackTraceAsString(e));
            }

        }
        return targetFile;
    }

    /**
     * Returns the stack trace (including the message) of an exception as a String.<p>
     * 
     * If the exception is a CmsException, 
     * also writes the root cause to the String.<p>
     * 
     * @param e the exception to get the stack trace from
     * @return the stack trace of an exception as a String
     */
    private static String getStackTraceAsString(Throwable e) {

        StringWriter stringWriter = new StringWriter();
        PrintWriter printWriter = new PrintWriter(stringWriter);
        e.printStackTrace(printWriter);
        return stringWriter.toString();
    }

    /**
     * @see java.applet.Applet#destroy()
     */
    public void destroy() {

        // NOOP
    }

    /**
     * Displays an error message in case the applet could not be initialized.<p>
     */
    public void displayError() {

        m_outputMode = 5;
        m_action = m_certificateErrorTitle;
        m_message = m_certificateErrorMessage;

        JOptionPane.showMessageDialog(this, m_message, m_action, JOptionPane.ERROR_MESSAGE);

        try {
            // redirect back to the server
            getAppletContext().showDocument(new URL(m_redirectUrl), m_redirectTargetFrame);
        } catch (MalformedURLException e) {
            // this should never happen
            e.printStackTrace();
        }

        stop();
    }

    /**
     * Extracts the colors from the parameter String.<p>
     * 
     * @param colors list of color names and values
     * @return HashMap with color names and values
     */
    private HashMap extractColors(String colors) {

        HashMap colorStorage = new HashMap();

        if (colors != null) {
            StringTokenizer tok = new StringTokenizer(colors, ",");
            // loop through the tokens
            // all tokens have the format "extension=type"    
            while (tok.hasMoreElements()) {
                String token = tok.nextToken();
                // now extract the file extension and the type
                String colorName = token.substring(0, token.indexOf("="));
                String colorValue = token.substring(token.indexOf("=") + 1);
                colorStorage.put(colorName, colorValue);
            }
        }
        return colorStorage;
    }

    /**
     * Gets a color for drawing the output.<p>
     * 
     * @param colorName the name of the color
     * @return color
     */
    private Color getColor(String colorName) {

        Color col = Color.black;
        try {
            col = Color.decode((String)m_colors.get(colorName));
        } catch (Exception e) {
            System.err.println("Error reading " + colorName + ":" + e);
        }
        return col;
    }

    /**
     * Puts all given files (and all files in subtree of given potential "folder" file) into the given 
     * list (as {@link File} instances) with respect to the current file filter in the file chooser. 
     * <p>
     * 
     * 
     * @param file the file or folder to collect all subfiles of with respect to the current file chooser. 
     * 
     * @param fileNames all given files (and all files in subtree of given potential "folder" file) into the given 
     *      list (as {@link File} instances) with respect to the current file filter in the file chooser 
     */
    private void getFilesInTree(final File file, final List fileNames) {

        if (file.isDirectory()) {
            File[] children = file.listFiles();
            for (int i = 0; i < children.length; i++) {
                getFilesInTree(children[i], fileNames);

            }
        } else {
            FileFilter filter = m_fileSelector.getFileFilter();
            if (filter.accept(file)) {
                fileNames.add(file);
            }
        }
    }

    /**
     * Puts the path of the given file in relation to the current root directory of the intenal 
     * file chooser with support for folder-recursion. <p>
     * 
     * 
     * @param file the file to put into relation to the root directory of the internal file chooser or a folder 
     *      of files to do this. 
     * 
     * @param fileNames will contain the result. 
     */
    private void getRelativeFilePaths(final File file, final List fileNames) {

        if (file.isDirectory()) {
            File[] children = file.listFiles();
            for (int i = 0; i < children.length; i++) {
                getRelativeFilePaths(children[i], fileNames);

            }
        } else {
            FileFilter filter = m_fileSelector.getFileFilter();
            if (filter.accept(file)) {
                String rootDir = m_fileSelector.getCurrentDirectory().getAbsolutePath();
                int rootDirLength = rootDir.length();
                String filePath = file.getAbsolutePath();
                filePath = filePath.substring(rootDirLength);
                filePath = filePath.replace('\\', '/');
                fileNames.add(filePath);
            }
        }
    }

    /**
     * @see java.applet.Applet#init()
     */
    public void init() {

        // has to be first before any gui components are created
        if (getParameter("overwriteDialogLocale") != null) {
            m_overwriteDialogLocale = getParameter("overwriteDialogLocale");
            Locale wpLocale = new Locale(m_overwriteDialogLocale);
            setLocale(wpLocale);
        }

        m_opencms = getParameter("opencms");
        m_targetUrl = getParameter("target");
        m_redirectUrl = getParameter("redirect");
        m_redirectTargetFrame = getParameter("targetframe");
        if ((m_redirectTargetFrame == null) || m_redirectTargetFrame.equals("")) {
            m_redirectTargetFrame = "explorer_files";
        }
        m_errorUrl = getParameter("error");
        m_uploadFolder = getParameter("filelist");
        m_fileFilterSelection = getParameter("filefilterselection");
        String tmpSize = getParameter("maxsize");
        if ((tmpSize != null) && (tmpSize.length() > 0)) {
            m_maxsize = Long.parseLong(tmpSize);
        }
        m_fileExtensions = getParameter("fileExtensions");
        m_colors = extractColors(getParameter("colors"));

        // setup the applet output
        m_font = new java.awt.Font(null, Font.BOLD, 12);
        m_metrics = getFontMetrics(m_font);
        m_source = getImage(getCodeBase(), "org/opencms/applet/upload/applet_source.png");
        m_target = getImage(getCodeBase(), "org/opencms/applet/upload/applet_target.png");
        m_floater = getImage(getCodeBase(), "org/opencms/applet/upload/floater.gif");

        // get the output massages in the correct language
        if (getParameter("actionOutputSelect") != null) {
            m_actionOutputSelect = getParameter("actionOutputSelect");
        }
        if (getParameter("actionOutputCount") != null) {
            m_actionOutputCount = getParameter("actionOutputCount");
        }
        if (getParameter("actionOutputCreate") != null) {
            m_actionOutputCreate = getParameter("actionOutputCreate");
        }
        if (getParameter("actionOverwriteCheck") != null) {
            m_actionOverwriteCheck = getParameter("actionOverwriteCheck");
        }
        if (getParameter("actionOutputUpload") != null) {
            m_actionOutputUpload = getParameter("actionOutputUpload");
        }
        if (getParameter("actionOutputError") != null) {
            m_actionOutputError = getParameter("actionOutputError");
        }
        if (getParameter("messageOutputUpload") != null) {
            m_messageOutputUpload = getParameter("messageOutputUpload");
        }
        if (getParameter("messageOutputAdding") != null) {
            m_messageOutputAdding = getParameter("messageOutputAdding");
        }
        if (getParameter("messageOutputErrorZip") != null) {
            m_messageOutputErrorZip = getParameter("messageOutputErrorZip");
        }
        if (getParameter("messageOutputErrorSize") != null) {
            m_messageOutputErrorSize = getParameter("messageOutputErrorSize");
        }
        if (getParameter("messageNoPreview") != null) {
            m_messageNoPreview = getParameter("messageNoPreview");
        }
        if (getParameter("errorLine1") != null) {
            m_errorLine1 = getParameter("errorLine1");
        }
        if (getParameter("certificateErrorTitle") != null) {
            m_certificateErrorTitle = getParameter("certificateErrorTitle");
        }
        if (getParameter("certificateErrorMessage") != null) {
            m_certificateErrorMessage = getParameter("certificateErrorMessage");
        }
        if (getParameter("overwriteDialogTitle") != null) {
            m_overwriteDialogTitle = getParameter("overwriteDialogTitle");
        }
        if (getParameter("overwriteDialogIntro") != null) {
            m_overwriteDialogIntro = getParameter("overwriteDialogIntro");
        }
        if (getParameter("overwriteDialogCancel") != null) {
            m_overwriteDialogCancel = getParameter("overwriteDialogCancel");
        }
        if (getParameter("overwriteDialogOk") != null) {
            m_overwriteDialogOk = getParameter("overwriteDialogOk");
        }
        if (getParameter("clientFolder") != null) {
            m_clientFolder = getParameter("clientFolder");
        }

        m_certificateAccepted = true;
        try {
            // set log factory to default log factory, otherwise commons logging detection will fail with an exception 
            System.setProperty(
                org.apache.commons.logging.LogFactory.FACTORY_PROPERTY,
                org.apache.commons.logging.LogFactory.FACTORY_DEFAULT);
        } catch (SecurityException e) {
            // this indicates the applet certificate has not been accepted
            m_certificateAccepted = false;
            e.printStackTrace();
        }
    }

    /**
     * Move the floating upload image to right, wrap around on right side.<p>
     */
    public void moveFloater() {

        m_floaterPos += 10;
        if ((m_floaterPos) > 430) {
            m_floaterPos = 50;
        }
        repaint();
    }

    /**
     * @see java.awt.Component#paint(Graphics)
     */
    public void paint(Graphics g) {

        // create the box
        m_offscreen = createImage(getSize().width, getSize().height);
        m_offgraphics = m_offscreen.getGraphics();
        m_offgraphics.setColor(getColor("bgColor"));
        m_offgraphics.fillRect(0, 0, getSize().width, getSize().height);
        m_offgraphics.setColor(getColor("outerBorderRightBottom"));
        m_offgraphics.drawLine(0, getSize().height - 1, getSize().width - 1, getSize().height - 1);
        m_offgraphics.drawLine(getSize().width - 1, 0, getSize().width - 1, getSize().height - 1);
        m_offgraphics.setColor(getColor("outerBorderLeftTop"));
        m_offgraphics.drawLine(0, 0, getSize().width - 1, 0);
        m_offgraphics.drawLine(0, 0, 0, getSize().height - 1);
        m_offgraphics.setColor(getColor("innerBorderRightBottom"));
        m_offgraphics.drawLine(1, getSize().height - 2, getSize().width - 2, getSize().height - 2);
        m_offgraphics.drawLine(getSize().width - 2, 1, getSize().width - 2, getSize().height - 2);
        m_offgraphics.setColor(getColor("innerBorderLeftTop"));
        m_offgraphics.drawLine(1, 1, getSize().width - 2, 1);
        m_offgraphics.drawLine(1, 1, 1, getSize().height - 2);
        m_offgraphics.setColor(getColor("bgHeadline"));
        m_offgraphics.fillRect(4, 4, getSize().width - 5, 18);

        m_offgraphics.setColor(getColor("innerBorderRightBottom"));
        m_offgraphics.drawLine(10, getSize().height - 11, getSize().width - 11, getSize().height - 11);
        m_offgraphics.drawLine(getSize().width - 11, 25, getSize().width - 11, getSize().height - 11);
        m_offgraphics.setColor(getColor("innerBorderLeftTop"));
        m_offgraphics.drawLine(10, 25, getSize().width - 11, 25);
        m_offgraphics.drawLine(10, 25, 10, getSize().height - 11);

        // draw title
        int cx = 10;
        int cy = 17;
        m_offgraphics.setFont(m_font);
        m_offgraphics.setColor(getColor("colorHeadline"));
        m_offgraphics.drawString(m_action, cx, cy);

        m_offgraphics.setColor(getColor("colorText"));
        // draw process message
        if (m_outputMode >= 3) {
            cx = Math.max((getSize().width - m_metrics.stringWidth(m_message)) / 2, 0);
        } else {
            cx = 25;
        }
        cy = 41;
        m_offgraphics.drawString(m_message, cx, cy);

        // draw process bar during zip creation
        if (m_outputMode == 2) {
            float bar = new Float(m_step).floatValue() / new Float(m_resources).floatValue();
            String barText = "(" + m_step + " / " + m_resources + ")";
            m_offgraphics.drawRect(25, 50, 450, 20);
            m_offgraphics.setColor(Color.white);
            m_offgraphics.fillRect(26, 51, 449, 19);
            m_offgraphics.setColor(getColor("progessBar"));
            m_offgraphics.fillRect(26, 51, new Float(bar * 449).intValue(), 19);
            int progressWith = m_metrics.stringWidth(barText);
            cx = Math.max((getSize().width - progressWith) / 2, 0);
            cy = 64;
            m_offgraphics.setColor(Color.black);
            m_offgraphics.drawString(barText, cx, cy);
        }

        // show floater during upload
        if (m_outputMode == 3) {
            m_offgraphics.drawImage(m_floater, m_floaterPos, 57, this);
            m_offgraphics.drawImage(m_source, 30, 47, this);
            m_offgraphics.drawImage(m_target, 440, 47, this);
        }

        // copy the offcreen graphics to the applet
        g.drawImage(m_offscreen, 0, 0, null);
    }

    private List parseDuplicateFiles(String responseBodyAsString) {

        List result = new ArrayList();
        LineNumberReader reader = new LineNumberReader(new InputStreamReader(new ByteArrayInputStream(
            responseBodyAsString.getBytes())));
        try {
            String trim;
            for (String read = reader.readLine(); read != null; read = reader.readLine()) {
                trim = read.trim();
                if (!(trim.equals("") || trim.equals("\n"))) {
                    // empty strings could happen if the serverside jsp is edited and has new unwanted linebreaks
                    result.add(read);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * @see java.lang.Runnable#run()
     */
    public void run() {

        try {
            boolean ok = true;
            while (ok) {
                ok = true;

                m_message = "";
                m_resources = 0;
                m_step = 0;
                // create a new file chooser

                if (m_fileSelector == null) {
                    m_fileSelector = new UploadAppletFileChooser(this);
                }

                // file selector can read files and folders
                m_fileSelector.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);

                m_fileSelector.setDialogTitle(m_actionOutputSelect);

                FileFilter imageFilter = new ImageFilter();
                FileFilter officeFilter = new OfficeFilter();
                FileFilter webFilter = new WebFilter();

                // add two custom file filters (office and images) and the default filters
                m_fileSelector.addChoosableFileFilter(imageFilter);
                m_fileSelector.addChoosableFileFilter(officeFilter);
                m_fileSelector.addChoosableFileFilter(webFilter);

                m_fileSelector.setAcceptAllFileFilterUsed(true);
                // enable multi-selection of files
                m_fileSelector.setMultiSelectionEnabled(true);
                // add custom icons for file types.
                m_fileSelector.setFileView(new ImageFileView(m_opencms, m_fileExtensions));
                // add the image preview pane.
                m_fileSelector.setAccessory(new ImagePreview(m_fileSelector, m_messageNoPreview));
                if ((m_clientFolder != null) && !m_clientFolder.trim().equals("")) {
                    File clientFolder = new File(m_clientFolder);
                    if (clientFolder.exists() && clientFolder.isDirectory()) {
                        m_fileSelector.setCurrentDirectory(clientFolder);
                    }
                }

                m_action = m_actionOutputSelect;

                // pre - selection of the filter: 
                if ((m_fileFilterSelection != null) && !m_fileFilterSelection.trim().equals("")) {
                    if (WebFilter.FILTER_ID.equals(m_fileFilterSelection)) {
                        m_fileSelector.setFileFilter(webFilter);
                    } else if (OfficeFilter.FILTER_ID.equals(m_fileFilterSelection)) {
                        m_fileSelector.setFileFilter(officeFilter);
                    } else if (ImageFilter.FILTER_ID.equals(m_fileFilterSelection)) {
                        m_fileSelector.setFileFilter(imageFilter);
                    }
                }
                repaint();
                m_overwrites = new ArrayList();
                int returnVal = m_fileSelector.showDialog(this, "OK");

                // process the results.
                if (returnVal == JFileChooser.APPROVE_OPTION) {
                    // count all resources
                    m_outputMode = 1;
                    m_action = m_actionOutputCount;
                    repaint();

                    File[] files = m_fileSelector.getSelectedFiles();
                    List fileNames = new ArrayList();
                    for (int i = 0; i < files.length; i++) {
                        getRelativeFilePaths(files[i], fileNames);
                    }

                    if (m_overwrites.size() > 0) {
                        // subtract all duplicate files first
                        files = subtractDuplicates(fileNames, m_overwrites);
                        files = addOverwrites(
                            files,
                            ((FileSelectionPanel)m_overwriteDialog.getControlPanel().getComponent(1)).getSelectedFiles());
                    } else {

                        fileNames = new ArrayList();
                        for (int i = 0; i < files.length; i++) {
                            getFilesInTree(files[i], fileNames);
                        }
                        files = (File[])fileNames.toArray(new File[fileNames.size()]);
                    }

                    m_resources = countResources(files);

                    // create the zipfile  
                    m_outputMode = 2;
                    if (files.length > 0) {
                        File targetFile = createZipFile(files);
                        // check the size of the zip files
                        if ((targetFile == null) || ((m_maxsize > 0) && (targetFile.length() > m_maxsize))) {
                            // show some details in the applet itself
                            m_outputMode = 4;
                            if (targetFile == null) {
                                m_message = m_messageOutputErrorZip;
                            } else {
                                m_message = m_messageOutputErrorSize + " " + targetFile.length() + " > " + m_maxsize;
                            }
                            m_action = m_actionOutputError;
                            repaint();
                            // show an error-alertbog
                            JOptionPane.showMessageDialog(this, m_message, m_action, JOptionPane.ERROR_MESSAGE);
                        } else {
                            m_outputMode = 3;
                            m_message = m_messageOutputUpload + " (" + targetFile.length() / 1024 + " kb)";
                            repaint();
                            // upload the zipfile
                            FileUploadThread uploadThreat = new FileUploadThread();

                            uploadThreat.init(this);
                            uploadThreat.start();

                            uploadZipFile(targetFile);
                            ok = false;
                        }
                    } else {
                        // zero files were selected for upload (might be that all potential overwrites were deselected)
                        ok = false;
                        getAppletContext().showDocument(new URL(m_redirectUrl), m_redirectTargetFrame);
                    }

                } else {
                    //the cancel button was used, so go back to the workplace
                    ok = false;
                    getAppletContext().showDocument(new URL(m_redirectUrl), m_redirectTargetFrame);
                }
            }
        } catch (RuntimeException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /** 
     * Displays the dialog that shows the list of files that will be overwritten on the server.
     * <p>
     * The user may uncheck the checkboxes in front of the relative paths to avoid overwriting. 
     * <p>
     * 
     * @param duplications 
     *      a list of Strings that are relative paths to the files that will be overwritten on the server
     *      
     * @return one of 
     */
    private int showDuplicationsDialog(List duplications) {

        int rtv = ModalDialog.ERROR_OPTION;
        try {

            JTextArea dialogIntroPanel = new JTextArea();
            dialogIntroPanel.setLineWrap(true);
            dialogIntroPanel.setWrapStyleWord(true);
            dialogIntroPanel.setText(m_overwriteDialogIntro);
            dialogIntroPanel.setEditable(false);
            dialogIntroPanel.setBackground(m_fileSelector.getBackground());
            dialogIntroPanel.setFont(m_font);

            FileSelectionPanel selectionPanel = new FileSelectionPanel(
                duplications,
                m_fileSelector.getCurrentDirectory().getAbsolutePath());

            JPanel stacker = new JPanel(new GridBagLayout());
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.anchor = GridBagConstraints.NORTHWEST;
            gbc.gridheight = 1;
            gbc.gridwidth = 1;
            gbc.weightx = 1f;
            gbc.weighty = 0f;
            gbc.gridx = 0;
            gbc.gridy = 0;
            gbc.fill = GridBagConstraints.BOTH;
            gbc.insets = new Insets(2, 2, 2, 2);

            stacker.add(dialogIntroPanel, gbc);

            gbc.weighty = 1f;
            gbc.gridy = 1;
            gbc.insets = new Insets(0, 2, 0, 2);
            stacker.add(selectionPanel, gbc);

            m_overwriteDialog = new ModalDialog(
                m_fileSelector,
                m_overwriteDialogTitle,
                m_overwriteDialogOk,
                m_overwriteDialogCancel,
                stacker);
            m_overwriteDialog.setSize(new Dimension(560, 280));

            //dialog.setResizable(false);
            m_overwriteDialog.showDialog();
            rtv = m_overwriteDialog.getReturnValue();

        } catch (Throwable f) {
            f.printStackTrace(System.err);
        }
        return rtv;
    }

    /**
     * @see java.applet.Applet#start()
     */
    public void start() {

        if (m_certificateAccepted) {
            // certificate was accepted, start upload thread
            m_runner = new Thread(this);
            m_runner.start();
        } else {
            // certificate was not accepted, show error message
            displayError();
        }
    }

    /**
     * @see java.applet.Applet#stop()
     */
    public void stop() {

        m_runner = null;
    }

    /**
     * Return all files that are found in the first argument except a matching path suffix is found in the list 
     * of Strings given by the 2nd argument. <p>
     * 
     * @param fileNames the list of paths to diminish by matching path suffixes of the 2nd argument. 
     * 
     * @param duplications a list of Strings that contains "relative" paths (without absolute location)
     * 
     * @return all files that are found in the first argument except a matching path suffix is found in the list 
     *      of Strings given by the 2nd argument
     */
    private File[] subtractDuplicates(List fileNames, List duplications) {

        // subtract: 
        String path;
        Iterator itDuplications = duplications.iterator();
        while (itDuplications.hasNext()) {
            path = (String)itDuplications.next();
            fileNames.remove(path);
        }

        // no recreate the subtractor list to files: 
        List result = new ArrayList();
        File rootPath = m_fileSelector.getCurrentDirectory();
        Iterator it = fileNames.iterator();
        while (it.hasNext()) {
            path = (String)it.next();
            result.add(new File(rootPath, path));
        }
        return (File[])result.toArray(new File[result.size()]);
    }

    /**
     * @see java.awt.Component#update(java.awt.Graphics)
     */
    public void update(Graphics g) {

        paint(g);
    }

    /**
     * Uploads the zipfile to the OpenCms.<p>
     * 
     * @param uploadFile the zipfile to upload
     */
    private void uploadZipFile(File uploadFile) {

        m_action = m_actionOutputUpload;
        repaint();

        PostMethod post = new PostMethod(m_targetUrl);

        try {
            Part[] parts = new Part[5];
            parts[0] = new FilePart(uploadFile.getName(), uploadFile);
            parts[1] = new StringPart("action", "submitform");
            parts[2] = new StringPart("unzipfile", "true");
            parts[3] = new StringPart("uploadfolder", m_uploadFolder);
            parts[4] = new StringPart("clientfolder", m_fileSelector.getCurrentDirectory().getAbsolutePath());

            HttpMethodParams methodParams = post.getParams();
            methodParams.setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
            MultipartRequestEntity request = new MultipartRequestEntity(parts, methodParams);
            post.setRequestEntity(request);

            // add jsessionid query string
            String sessionId = getParameter("sessionId");
            String query = ";" + C_JSESSIONID.toLowerCase() + "=" + sessionId;
            post.setQueryString(query);
            post.addRequestHeader(C_JSESSIONID, sessionId);

            HttpClient client = new HttpClient();
            HttpConnectionParams connectionParams = client.getHttpConnectionManager().getParams();
            connectionParams.setConnectionTimeout(5000);

            // add the session cookie
            client.getState();
            client.getHostConfiguration().getHost();

            HttpState initialState = new HttpState();
            URI uri = new URI(m_targetUrl, false);
            Cookie sessionCookie = new Cookie(uri.getHost(), C_JSESSIONID, sessionId, "/", null, false);
            initialState.addCookie(sessionCookie);
            client.setState(initialState);

            // no execute the file upload
            int status = client.executeMethod(post);

            if (status == HttpStatus.SC_OK) {
                //return to the specified url and frame target
                getAppletContext().showDocument(new URL(m_redirectUrl), m_redirectTargetFrame);
            } else {
                // create the error text
                String error = m_errorLine1 + "\n" + post.getStatusLine();
                //JOptionPane.showMessageDialog(this, error, "Error!", JOptionPane.ERROR_MESSAGE);
                getAppletContext().showDocument(
                    new URL(m_errorUrl + "?action=showerror&uploaderror=" + error),
                    "explorer_files");
            }
        } catch (RuntimeException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            post.releaseConnection();
            // finally delete the zipFile on the harddisc
            uploadFile.delete();
        }
    }

    /**
     * Writes the bytes of the file to the zip output stream.<p>
     *
     * @param file the name of the file to read
     * 
     * @param out the zip outputstream
     * 
     * @throws Exception if something goes wrong
     */
    private void writeFileBytes(File file, OutputStream out) throws Exception {

        byte[] buffer = new byte[2048];
        FileInputStream fileStream = null;
        int charsRead;
        int size;
        try {
            fileStream = new FileInputStream(file);
            charsRead = 0;
            size = new Long(file.length()).intValue();
            int readCount = 0;
            while ((charsRead < size) && (readCount != -1)) {
                readCount = fileStream.read(buffer);
                charsRead += readCount;
                out.write(buffer, 0, readCount);
            }
        } catch (IOException e) {
            throw e;
        } finally {
            try {
                if (fileStream != null) {
                    fileStream.close();
                }
            } catch (IOException e) {
                // ignore
            }
        }
    }
}