/**
 *  FTPClient
 *  Copyright 2002, 2004, 2006, 2010 by Michael Peter Christen
 *  first published on http://yacy.net
 *  main implementation finished: 28.05.2002
 *  last major change: 06.05.2004
 *  added html generation for directories: 5.9.2006
 *  migrated to the cora package and re-licensed under lgpl: 23.08.2010
 *
 *  This file is part of YaCy Content Integration
 *
 *  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.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program in the file lgpl21.txt
 *  If not, see <http://www.gnu.org/licenses/>.
 */

package net.yacy.cora.protocol.ftp;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.yacy.cora.document.encoding.UTF8;
import net.yacy.cora.protocol.Domains;
import net.yacy.cora.util.ConcurrentLog;

public class FTPClient {

    public static final String ANONYMOUS = "anonymous";
    private static final ConcurrentLog log = new ConcurrentLog("FTPClient");

    private static final String vDATE = "20161222";

    private boolean glob = true; // glob = false -> filenames are taken
    // literally for mget, ..

    // transfer type
    private static final char transferType = 'i'; // transfer binary

    // block size [1K by default]
    private static final int blockSize = 1024;

    // client socket for commands
    private Socket ControlSocket = null;

    // socket timeout
    private static final int ControlSocketTimeout = 10000;

    // data socket timeout
    private int DataSocketTimeout = 0; // in seconds (default infinite)

    // socket for data transactions
    private ServerSocket DataSocketActive = null;
    private Socket DataSocketPassive = null;
    private boolean DataSocketPassiveMode = true;

    // output and input streams for client control connection
    private BufferedReader clientInput = null;
    private DataOutputStream clientOutput = null;

    // client prompt
    private String prompt = "ftp [local]>";

    String[] cmd;

    // session parameters
    File currentLocalPath;
    String account, password, host, remotemessage, remotegreeting, remotesystem;
    int port;

    // entry info cache
    private final Map<String, entryInfo> infoCache = new HashMap<String, entryInfo>();

    // date-format in LIST (english month names)
    private static final SimpleDateFormat lsDateFormat = new SimpleDateFormat("MMM d y H:m", Locale.forLanguageTag("en"));

    // TODO: implement RFC 2640 Internationalization

    public FTPClient() {

        this.currentLocalPath = new File(System.getProperty("user.dir"));
        try {
            this.currentLocalPath = new File(this.currentLocalPath.getCanonicalPath());
        } catch (final IOException e) {
        }

        this.account = null;
        this.password = null;
        this.host = null;
        this.port = -1;
        this.remotemessage = null;
        this.remotegreeting = null;
        this.remotesystem = null;
    }

    public boolean exec(String command, final boolean promptIt) {
        if ((command == null) || (command.isEmpty())) {
            return true;
        }
        int pos;
        String com;
        boolean ret = true;
        while (command.length() > 0) {
            pos = command.indexOf(';',0);
            if (pos < 0) {
                pos = command.indexOf("\n",0);
            }
            if (pos < 0) {
                com = command;
                command = "";
            } else {
                com = command.substring(0, pos);
                command = command.substring(pos + 1);
            }
            if (promptIt) {
                log.info(this.prompt + com);
            }
            this.cmd = line2args(com);
            try {
                ret = (((Boolean) getClass().getMethod(this.cmd[0].toUpperCase(), (Class<?>[]) Array.newInstance(Class.class, 0)).invoke(this, (Object[]) Array.newInstance(Object.class, 0)))
                        .booleanValue());
            } catch (final InvocationTargetException e) {
                if (e.getMessage() != null) {
                    if (notConnected()) {
                        // the error was probably caused because there is no
                        // connection
                        log.warn("not connected. no effect.", e);
                    } else {
                        log.warn("ftp internal exception: target exception " + e);
                    }
                    return ret;
                }
            } catch (final IllegalAccessException e) {
                log.warn("ftp internal exception: wrong access " + e);
                return ret;
            } catch (final NoSuchMethodException e) {
                // consider first that the user attempted to execute a java
                // command from
                // the current path; either local or remote
                if (notConnected()) {
                    // try a local exec
                    try {
                        javaexec(this.cmd);
                    } catch (final Exception ee) {
                        log.warn("Command '" + this.cmd[0] + "' not supported. Try 'HELP'.");
                    }
                } else {
                    // try a remote exec
                    exec("java " + com, false);
                }
                return ret;
            }
        }
        return ret;
    }

    private String[] line2args(final String line) {
        // parse the command line
        if ((line == null) || (line.isEmpty())) {
            return null;
        }
        // pre-parse
        String line1 = "";
        boolean quoted = false;
        for (int i = 0; i < line.length(); i++) {
            if (quoted) {
                if (line.charAt(i) == '"') {
                    quoted = false;
                } else {
                    line1 = line1 + line.charAt(i);
                }
            } else {
                if (line.charAt(i) == '"') {
                    quoted = true;
                } else if (line.charAt(i) == ' ') {
                    line1 = line1 + '|';
                } else {
                    line1 = line1 + line.charAt(i);
                }
            }
        }
        return line1.split("\\|");
    }

    static class cl extends ClassLoader {

        public cl() {
            super();
        }

        @Override
        public synchronized Class<?> loadClass(final String classname, final boolean resolve) throws ClassNotFoundException {
            Class<?> c = findLoadedClass(classname);
            if (c == null) {
                try {
                    // second try: ask the system
                    c = findSystemClass(classname);
                } catch (final ClassNotFoundException e) {
                    // third try: load myself
                    final File f = new File(System.getProperty("user.dir"), classname + ".class");
                    final int length = (int) f.length();
                    final byte[] classbytes = new byte[length];
                    DataInputStream in = null;
                    try {
                        in = new DataInputStream(new FileInputStream(f));
                        in.readFully(classbytes);
                        c = defineClass(classname, classbytes, 0, classbytes.length);
                    } catch (final FileNotFoundException ee) {
                        throw new ClassNotFoundException();
                    } catch (final IOException ee) {
                        throw new ClassNotFoundException();
                    } finally {
                        try {
							in.close();
						} catch (IOException ioe) {
							log.warn("Could not close input stream on file " + f);
						}
                    }
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

    }

    private void javaexec(final String[] inArgs) {
        final String obj = inArgs[0];
        final String[] args = new String[inArgs.length - 1];

        // remove the object name from the array of arguments
        System.arraycopy(inArgs, 1, args, 0, inArgs.length - 1);

        // Build the argument list for invoke() method.
        final Object[] argList = new Object[1];
        argList[0] = args;

        final Properties pr = System.getProperties();
        final String origPath = (String) pr.get("java.class.path");
        try {

            // set the user.dir to the actual local path
            pr.put("user.dir", this.currentLocalPath.toString());

            // add the current path to the classpath
            // pr.put("java.class.path", "" + pr.get("user.dir") +
            // pr.get("path.separator") + origPath);

            // log.warning("System Properties: " + pr.toString());

            System.setProperties(pr);

            // locate object
            final Class<?> c = (new cl()).loadClass(obj);
            // Class c = this.getClass().getClassLoader().loadClass(obj);

            // locate public static main(String[]) method
            final Class<?>[] parameterType = (Class<?>[]) Array.newInstance(Class.class, 1);
            parameterType[0] = Class.forName("[Ljava.lang.String;");
            Method m = c.getMethod("main", parameterType);

            // invoke object.main()
            final Object result = m.invoke(null, argList);
            //parameterType = null;
            m = null;

            // handle result
            if (result != null) {
                log.info("returns " + result);
            }

            // set the local path to the user.dir (which may have changed)
            this.currentLocalPath = new File((String) pr.get("user.dir"));

        } catch (final ClassNotFoundException e) {
            // log.warning("cannot find class file " + obj +
            // ".class");
            // class file does not exist, go silently over it to not show
            // everybody that the
            // system attempted to load a class file
            log.warn("Command '" + obj + "' not supported. Try 'HELP'.");
        } catch (final NoSuchMethodException e) {
            log.warn("no \"public static main(String args[])\" in " + obj);
        } catch (final InvocationTargetException e) {
            final Throwable orig = e.getTargetException();
            if (orig.getMessage() != null) {
                log.warn("Exception from " + obj + ": " + orig.getMessage(), orig);
            }
        } catch (final IllegalAccessException e) {
            log.warn("Illegal access for " + obj + ": class is probably not declared as public", e);
        } catch (final NullPointerException e) {
            log.warn("main(String args[]) is not defined as static for " + obj);
            /*
             * } catch (final IOException e) { // class file does not exist, go
             * silently over it to not show everybody that the // system
             * attempted to load a class file log.warning("Command '" + obj + "'
             * not supported. Try 'HELP'.");
             */
        } catch (final Exception e) {
            log.warn("Exception caught: ", e);
        }

        // set the classpath to its original definition
        pr.put("java.class.path", origPath);

    }

    // FTP CLIENT COMMANDS ------------------------------------

    public boolean ASCII() {
        if (this.cmd.length != 1) {
            log.warn("Syntax: ASCII  (no parameter)");
            return true;
        }
        try {
            literal("TYPE A");
        } catch (final IOException e) {
            log.warn("Error: ASCII transfer type not supported by server.");
        }
        return true;
    }

    public boolean BINARY() {
        if (this.cmd.length != 1) {
            log.warn("Syntax: BINARY  (no parameter)");
            return true;
        }
        try {
            literal("TYPE I");
        } catch (final IOException e) {
            log.warn("Error: BINARY transfer type not supported by server.");
        }
        return true;
    }

    public boolean BYE() {
        return QUIT();
    }

    public boolean CD() {
        if (this.cmd.length != 2) {
            log.warn("Syntax: CD <path>");
            return true;
        }
        if (notConnected()) {
            return LCD();
        }
        try {
            // send cwd command
            send("CWD " + this.cmd[1]);

            final String reply = receive();
            if (isNotPositiveCompletion(reply)) {
                throw new IOException(reply);
            }
        } catch (final IOException e) {
            log.warn("Error: change of working directory to path " + this.cmd[1] + " failed.");
        }
        return true;
    }

    public boolean CLOSE() {
        return DISCONNECT();
    }

    private void rmForced(final String path) throws IOException {
        // first try: send DELE command (to delete a file)
        send("DELE " + path);
        // read reply
        final String reply1 = receive();
        if (isNotPositiveCompletion(reply1)) {
            // second try: send a RMD command (to delete a directory)
            send("RMD " + path);
            // read reply
            final String reply2 = receive();
            if (isNotPositiveCompletion(reply2)) {
                // third try: test if this thing is a directory or file and send
                // appropriate error message
                if (isFolder(path)) {
                    throw new IOException(reply2);
                }
                throw new IOException(reply1);
            }
        }
    }

    /**
     * @param path
     * @return date of entry on ftp-server or now if date can not be obtained
     */
    public Date entryDate(final String path) {
        final entryInfo info = fileInfo(path);
        Date date = null;
        if (info != null) {
            date = info.date;
        }
        return date;
    }

    public boolean DEL() {
        if (this.cmd.length != 2) {
            log.warn("Syntax: DEL <file>");
            return true;
        }
        if (notConnected()) {
            return LDEL();
        }
        try {
            rmForced(this.cmd[1]);
        } catch (final IOException e) {
            log.warn("Error: deletion of file " + this.cmd[1] + " failed.");
        }
        return true;
    }

    public boolean RM() {
        return DEL();
    }

    public boolean DIR() {
        if (this.cmd.length > 2) {
            log.warn("Syntax: DIR [<path>|<file>]");
            return true;
        }
        if (notConnected()) {
            return LDIR();
        }
        try {
            List<String> l;
            if (this.cmd.length == 2) {
                l = list(this.cmd[1], false);
            } else {
                l = list(".", false);
            }
            printElements(l);
        } catch (final IOException e) {
            log.warn("Error: remote list not available (1): " + e.getMessage());
        }
        return true;
    }

    public boolean DISCONNECT() {
        try {
            quit();
            log.info("---- Connection closed.");
        } catch (final IOException e) {
            // Connection to server lost
            // do not append any error to errPrintln because we can silently go over this error
            // otherwise the client treats this case as an error and does not accept the result of the session
        }
        try {
            closeConnection();
        } catch (final IOException e) {
            this.ControlSocket = null;
            this.DataSocketActive = null;
            this.DataSocketPassive = null;
            this.clientInput = null;
            this.clientOutput = null;
        }
        this.prompt = "ftp [local]>";
        return true;
    }

    private String quit() throws IOException {

        send("QUIT");

        // read status reply
        final String reply = receive();
        if (isNotPositiveCompletion(reply)) {
            throw new IOException(reply);
        }

        closeConnection();

        return reply;
    }

    public boolean EXIT() {
        return QUIT();
    }

    public boolean GET() {
        if ((this.cmd.length < 2) || (this.cmd.length > 3)) {
            log.warn("Syntax: GET <remote-file> [<local-file>]");
            return true;
        }
        final String remote = this.cmd[1]; // (new File(cmd[1])).getName();
        final boolean withoutLocalFile = this.cmd.length == 2;

        final String localFilename = (withoutLocalFile) ? remote : this.cmd[2];
        final File local = absoluteLocalFile(localFilename);

        if (local.exists()) {
            log.warn("Error: local file " + local.toString() + " already exists.\n" + "               File " + remote
                    + " not retrieved. Local file unchanged.");
        } else {
            if (withoutLocalFile) {
                retrieveFilesRecursively(remote, false);
            } else {
                try {
                    get(local.getAbsolutePath(), remote);
                } catch (final IOException e) {
                    log.warn("Error: retrieving file " + remote + " failed. (" + e.getMessage() + ")");
                }
            }
        }
        return true;
    }

    /**
     * @param localFilename
     * @return
     */
    private File absoluteLocalFile(final String localFilename) {
        File local;
        final File l = new File(localFilename);
        if (l.isAbsolute()) {
            local = l;
        } else {
            local = new File(this.currentLocalPath, localFilename);
        }
        return local;
    }

    private void retrieveFilesRecursively(final String remote, final boolean delete) {
        final File local = absoluteLocalFile(remote);
        try {
            get(local.getAbsolutePath(), remote);
            try {
                if (delete) {
                    rmForced(remote);
                }
            } catch (final IOException eee) {
                log.warn("Warning: remote file or path " + remote + " cannot be removed.");
            }
        } catch (final IOException e) {
            if (e.getMessage().startsWith("550")) {
                // maybe it's a "not a plain file" error message", then it can
                // be a folder
                // test if this exists (then it should be a folder)
                if (isFolder(remote)) {
                    // copy the whole directory
                    exec("cd \"" + remote + "\";lmkdir \"" + remote + "\";lcd \"" + remote + "\"", true);
                    // exec("mget *",true);
                    try {
                        for (final String element : list(".", false)) {
                            retrieveFilesRecursively(element, delete);
                        }
                    } catch (final IOException ee) {
                    }
                    exec("cd ..;lcd ..", true);
                    try {
                        if (delete) {
                            rmForced(remote);
                        }
                    } catch (final IOException eee) {
                        log.warn("Warning: remote file or path " + remote + " cannot be removed.");
                    }
                } else {
                    log.warn("Error: remote file or path " + remote + " does not exist.");
                }
            } else {
                log.warn("Error: retrieving file " + remote + " failed. (" + e.getMessage() + ")");
            }
        }
    }

    /**
     * checks if path is a folder
     *
     * @param path
     * @return true if ftp-server changes to path
     */
    public boolean isFolder(final String path) {
        try {
            // /// try to parse LIST output (1 command)
            final entryInfo info = fileInfo(path);
            if (info != null) {
                return info.type == filetype.directory;
            }

            // /// try to change to folder (4 commands)
            // current folder
            final String currentFolder = pwd();
            // check if we can change to folder
            send("CWD " + path);
            final String reply = receive();
            if (isNotPositiveCompletion(reply)) {
                throw new IOException(reply);
            }
            // check if we actually changed into the folder
            final String changedPath = pwd();
            if (!(changedPath.equals(path) || changedPath.equals(currentFolder
                    + (currentFolder.endsWith("/") ? "" : "/") + path))) {
                throw new IOException("folder is '" + changedPath + "' should be '" + path + "'");
            }
            // return to last folder
            send("CWD " + currentFolder);
            /*reply =*/ receive();
            return true;
        } catch (final IOException e) {
            return false;
        }
    }

    public boolean GLOB() {
        if (this.cmd.length != 1) {
            log.warn("Syntax: GLOB  (no parameter)");
            return true;
        }
        this.glob = !this.glob;
        log.info("---- globbing is now turned " + ((this.glob) ? "ON" : "OFF"));
        return true;
    }

    public boolean HASH() {
        log.warn("no games implemented");
        return true;
    }

    /*
     * private static String[] shift(String args[]) { if ((args == null) ||
     * (args.length == 0)) return args; else { String[] newArgs = new
     * String[args.length-1]; System.arraycopy(args, 1, newArgs, 0,
     * args.length-1); return newArgs; } } public boolean JAR() { //Sun
     * proprietary API may be removed in a future Java release
     * sun.tools.jar.Main.main(shift(cmd)); return true; }
     */

    public boolean JJENCODE() {
        if (this.cmd.length != 2) {
            log.warn("Syntax: JJENCODE <path>");
            return true;
        }
        final String path = this.cmd[1];

        final File dir = new File(path);
        final File newPath = dir.isAbsolute() ? dir : new File(this.currentLocalPath, path);
        if (newPath.exists()) {
            if (newPath.isDirectory()) {
                // exec("cd \"" + remote + "\";lmkdir \"" + remote + "\";lcd \""
                // + remote + "\"",true);
                /*
                 * if not exist %1\nul goto :error cd %1 c:\jdk1.2.2\bin\jar
                 * -cfM0 ..\%1.jar *.* cd .. c:\jdk1.2.2\bin\jar -cfM %1.jj
                 * %1.jar del %1.jar
                 */
                String s = "";
                final String[] l = newPath.list();
                for (final String element : l) {
                    s = s + " \"" + element + "\"";
                }
                exec("cd \"" + path + "\";jar -cfM0 ../\"" + path + ".jar\"" + s, true);
                exec("cd ..;jar -cfM \"" + path + ".jj\" \"" + path + ".jar\"", true);
                exec("rm \"" + path + ".jar\"", true);
            } else {
                log.warn("Error: local path " + newPath.toString() + " denotes not to a directory.");
            }
        } else {
            log.warn("Error: local path " + newPath.toString() + " does not exist.");
        }
        return true;
    }

    public boolean JJDECODE() {
        if (this.cmd.length != 2) {
            log.warn("Syntax: JJENCODE <path>");
            return true;
        }
        final String path = this.cmd[1];
        final File dir = new File(path);
        final File newPath = dir.isAbsolute() ? dir : new File(this.currentLocalPath, path);
        final File newFolder = new File(newPath.toString() + ".dir");
        if (newPath.exists()) {
            if (!newPath.isDirectory()) {
                if (!newFolder.mkdir()) {
                    /*
                     * if not exist %1.jj goto :error mkdir %1.dir copy %1.jj
                     * %1.dir\ > %1.dummy && del %1.dummy cd %1.dir
                     * c:\jdk1.2.2\bin\jar -xf %1.jj del %1.jj
                     * c:\jdk1.2.2\bin\jar -xf %1.jar del %1.jar cd ..
                     */
                    exec("mkdir \"" + path + ".dir\"", true);

                } else {
                    log.warn("Error: target dir " + newFolder.toString() + " cannot be created");
                }
            } else {
                log.warn("Error: local path " + newPath.toString() + " must denote to jar/jar file");
            }
        } else {
            log.warn("Error: local path " + newPath.toString() + " does not exist.");
        }
        return true;
    }

    private static String[] argList2StringArray(final String argList) {
        return argList.split("\\s");
    }

    public boolean JOIN(String[] args) {

        // make sure the specified dest file does not exist
        final String dest_name = args[1];
        final File dest_file = new File(dest_name);
        if (dest_file.exists()) {
            log.warn("join: destination file " + dest_name + " already exists");
            return true;
        }

        // prepare or search file names of the input files to be joined
        String source_name;
        File source_file;
        int pc = -1;
        // create new string array with file names
        // scan first for the files
        pc = 0;
        source_name = dest_name + ".000";
        String argString = "";
        source_file = new File(source_name);
        while ((source_file.exists()) && (source_file.isFile()) && (source_file.canRead())) {
            argString = argString + " " + source_name;
            pc++;
            source_name = dest_name + (pc < 10 ? ".00" + pc : (pc < 100 ? ".0" + pc : "." + pc));
            source_file = new File(source_name);
        }
        args = argList2StringArray(argString.substring(1));

        // do the join
        FileOutputStream dest = null;
        FileInputStream source = null;
        byte[] buffer;
        int bytes_read = 0;

        try {
            // open output file
            dest = new FileOutputStream(dest_file);
            buffer = new byte[1024];

            // append all source files
            for (pc = 0; pc < args.length; pc++) {
                // open the source file
                source_name = args[pc];
                source_file = new File(source_name);
                source = new FileInputStream(source_file);

                // start with the copy of one source file
                while (true) {
                    bytes_read = source.read(buffer);
                    if (bytes_read == -1) {
                        break;
                    }
                    dest.write(buffer, 0, bytes_read);
                }

                // copy finished. close source file
                try {
                    source.close();
                } catch (final IOException e) {
                }
            }
            // close the output file
            try {
                dest.close();
            } catch (final IOException e) {
            }

            // if we come to this point then everything went fine
            // if the user wanted to delete the source it is save to do so now
            for (pc = 0; pc < args.length; pc++) {
                try {
                    if (!(new File(args[pc])).delete()) {
                        log.warn("join: unable to delete file " + args[pc]);
                    }
                } catch (final SecurityException e) {
                    log.warn("join: no permission to delete file " + args[pc]);
                }
            }
        } catch (final FileNotFoundException e) {
        } catch (final IOException e) {
        }

        // clean up
        finally {
            // close any opened streams
            if (dest != null) {
                try {
                    dest.close();
                } catch (final IOException e) {
                }
            }
            if (source != null) {
                try {
                    source.close();
                } catch (final IOException e) {
                }
            }

            // print appropriate message
            log.warn("join created output from " + args.length + " source files");
        }
        return true;
    }

    public boolean COPY(final String[] args) {
        final File dest_file = new File(args[2]);
        if (dest_file.exists()) {
            log.warn("copy: destination file " + args[2] + " already exists");
            return true;
        }
        int bytes_read = 0;
        FileOutputStream dest = null;
        FileInputStream source = null;
        try {
            // open output file
            dest = new FileOutputStream(dest_file);
            final byte[] buffer = new byte[1024];

            // open the source file
            final File source_file = new File(args[1]);
            source = new FileInputStream(source_file);

            // start with the copy of one source file
            while (true) {
                bytes_read = source.read(buffer);
                if (bytes_read == -1) {
                    break;
                }
                dest.write(buffer, 0, bytes_read);
            }

        } catch (final FileNotFoundException e) {
        } catch (final IOException e) {
        } finally {
            // copy finished. close source file
            if (source != null) {
                try {
                    source.close();
                } catch (final IOException e) {
                }
            }

            // close the output file
            if (dest != null) {
                try {
                    dest.close();
                } catch (final IOException e) {
                }
            }
        }
        return true;
    }

    public boolean JAVA() {
        String s = "JAVA";
        for (int i = 1; i < this.cmd.length; i++) {
            s = s + " " + this.cmd[i];
        }
        try {
            send(s);
            /* String reply = */receive();
        } catch (final IOException e) {
        }
        return true;
    }

    public boolean LCD() {
        if (this.cmd.length != 2) {
            log.warn("Syntax: LCD <path>");
            return true;
        }
        final String path = this.cmd[1];
        final File dir = new File(path);
        File newPath = dir.isAbsolute() ? dir : new File(this.currentLocalPath, path);
        try {
            newPath = new File(newPath.getCanonicalPath());
        } catch (final IOException e) {
        }
        if (newPath.exists()) {
            if (newPath.isDirectory()) {
                this.currentLocalPath = newPath;
                log.info("---- New local path: " + this.currentLocalPath.toString());
            } else {
                log.warn("Error: local path " + newPath.toString() + " denotes not a directory.");
            }
        } else {
            log.warn("Error: local path " + newPath.toString() + " does not exist.");
        }
        return true;
    }

    public boolean LDEL() {
        return LRM();
    }

    public boolean LDIR() {
        if (this.cmd.length != 1) {
            log.warn("Syntax: LDIR  (no parameter)");
            return true;
        }
        final String[] name = this.currentLocalPath.list();
        for (String element : name) {
            log.info(ls(new File(this.currentLocalPath, element)));
        }
        return true;
    }

    /**
     * parse LIST of file
     *
     * @param path
     *                on ftp-server
     * @return null if info cannot be determined or error occures
     */
    public entryInfo fileInfo(final String path) {
        if (this.infoCache.containsKey(path)) {
            return this.infoCache.get(path);
        }
        try {
            /*
             * RFC959 page 33f: If the argument is a pathname, the command is
             * analogous to the "list" command except that data shall be
             * transferred over the control connection.
             */
            send("STAT " +  path);

            final String reply = receive();
            if (isNotPositiveCompletion(reply)) {
                throw new IOException(reply);
            }

            // check if reply is correct multi-line reply
            final String[] lines = reply.split("\\r\\n");
            if (lines.length < 3) {
                throw new IOException(reply);
            }
            final int startCode = getStatusCode(lines[0]);
            final int endCode = getStatusCode(lines[lines.length - 1]);
            if (startCode != endCode) {
                throw new IOException(reply);
            }

            // first line which gives a result is taken (should be only one)
            entryInfo info = null;
            final int endFor = lines.length - 1;
            for (int i = 1; i < endFor; i++) {
                info = parseListData(lines[i]);
                if (info != null) {
                    this.infoCache.put(path, info);
                    break;
                }
            }
            return info;
        } catch (final IOException e) {
            return null;
        }
    }

    /**
     * returns status of reply
     *
     * 1 Positive Preliminary reply 2 Positive Completion reply 3 Positive
     * Intermediate reply 4 Transient Negative Completion reply 5 Permanent
     * Negative Completion reply
     *
     * @param reply
     * @return first digit of the reply code
     */
    private int getStatus(final String reply) {
        return Integer.parseInt(reply.substring(0, 1));
    }

    /**
     * gives reply code
     *
     * @param reply
     * @return
     */
    private int getStatusCode(final String reply) {
        return Integer.parseInt(reply.substring(0, 3));
    }

    /**
     * checks if status code is in group 2 ("2xx message")
     *
     * @param reply
     * @return
     */
    private boolean isNotPositiveCompletion(final String reply) {
        return getStatus(reply) != 2;
    }

    private final static Pattern lsStyle = Pattern.compile("^([-\\w]{10}).\\s*\\d+\\s+[-\\w]+\\s+[-\\w]+\\s+(\\d+)\\s+(\\w{3})\\s+(\\d+)\\s+(\\d+:?\\d*)\\s+(.*)$");

    /**
     * parses output of LIST from ftp-server currently UNIX ls-style only, ie:
     * -rw-r--r-- 1 root other 531 Jan 29 03:26 README dr-xr-xr-x 2 root 512 Apr
     * 8 1994 etc
     *
     * @param line
     * @return null if not parseable
     */
    private static entryInfo parseListData(final String line) {
        // groups: 1: rights, 2: size, 3: month, 4: day, 5: time or year, 6: name
        final Matcher tokens = lsStyle.matcher(line);
        if (tokens.matches() && tokens.groupCount() == 6) {
            filetype type = filetype.file;
            if (tokens.group(1).startsWith("d")) type = filetype.directory;
            if (tokens.group(1).startsWith("l")) type = filetype.link;
            long size = -1;
            try {
                size = Long.parseLong(tokens.group(2));
            } catch (final NumberFormatException e) {
                log.warn("not a number in list-entry: ", e);
                return null;
            }
            String time;
            String year;
            if (tokens.group(5).contains(":")) {
                time = tokens.group(5);
                year = String.valueOf(Calendar.getInstance().get(Calendar.YEAR)); // current
                // year
            } else {
                time = "00:00";
                year = tokens.group(5);
            }
            // construct date string
            // this has to be done, because the list-entry may have multiple
            // spaces, tabs or so
            Date date;
            final String dateString = tokens.group(3) + " " + tokens.group(4) + " " + year + " " + time;
            try {
            	synchronized(lsDateFormat) {
            		date = lsDateFormat.parse(dateString);
            	}
            } catch (final ParseException e) {
                log.warn("---- Error: not ls date-format '" + dateString, e);
                date = new Date();
            }
            final String filename = tokens.group(6);
            return new entryInfo(type, size, date, filename);
        }
        return null;
    }


    public static final entryInfo POISON_entryInfo = new entryInfo();

    public static enum filetype {
        file, link, directory;
    }

    /**
     * parameter class
     *
     * @author danielr
     * @since 2008-03-13 r4558
     */
    public static class entryInfo {
        /**
         * file type
         */
        public final filetype type;
        /**
         * size in bytes
         */
        public final long size;
        /**
         * date of file
         */
        public final Date date;
        /**
         * name of entry
         */
        public String name;

        public entryInfo() {
            this.type = filetype.file;
            this.size = -1;
            this.date = null;
            this.name = null;
        }

        /**
         * constructor
         *
         * @param isDir
         * @param size
         *                bytes
         * @param date
         * @param name
         */
        public entryInfo(final filetype type, final long size, final Date date, final String name) {
            this.type = type;
            this.size = size;
            this.date = date;
            this.name = name;
        }

        /*
         * (non-Javadoc)
         *
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            final StringBuilder info = new StringBuilder(100);
            info.append(this.name);
            info.append(" (type=");
            info.append(this.type.name());
            info.append(", size=");
            info.append(this.size);
            info.append(", ");
            info.append(this.date);
            info.append(")");
            return info.toString();
        }
    }

    private String ls(final File inode) {
        if ((inode == null) || (!inode.exists())) {
            return "";
        }
        String s = "";
        if (inode.isDirectory()) {
            s = s + "d";
        } else if (inode.isFile()) {
            s = s + "-";
        } else {
            s = s + "?";
        }
        if (inode.canRead()) {
            s = s + "r";
        } else {
            s = s + "-";
        }
        if (inode.canWrite()) {
            s = s + "w";
        } else {
            s = s + "-";
        }
        s = s + " " + lenformatted(Long.toString(inode.length()), 9);
        final DateFormat df = DateFormat.getDateTimeInstance();
        s = s + " " + df.format(new Date(inode.lastModified()));
        s = s + " " + inode.getName();
        if (inode.isDirectory()) {
            s = s + "/";
        }
        return s;
    }

    private String lenformatted(String s, int l) {
        l = l - s.length();
        while (l > 0) {
            s = " " + s;
            l--;
        }
        return s;
    }

    public boolean LITERAL() {
        if (this.cmd.length == 1) {
            log.warn("Syntax: LITERAL <ftp-command> [<command-argument>]   (see RFC959)");
            return true;
        }
        String s = "";
        for (int i = 1; i < this.cmd.length; i++) {
            s = s + " " + this.cmd[i];
        }
        try {
            literal(s.substring(1));
        } catch (final IOException e) {
            log.warn("Error: Syntax of FTP-command wrong. See RFC959 for details.");
        }
        return true;
    }

    public boolean LLS() {
        return LDIR();
    }

    public boolean LMD() {
        return LMKDIR();
    }

    public boolean LMKDIR() {
        if (this.cmd.length != 2) {
            log.warn("Syntax: LMKDIR <folder-name>");
            return true;
        }
        final File f = new File(this.currentLocalPath, this.cmd[1]);
        if (f.exists()) {
            log.warn("Error: local file/folder " + this.cmd[1] + " already exists");
        } else {
            if (!f.mkdir()) {
                log.warn("Error: creation of local folder " + this.cmd[1] + " failed");
            }
        }
        return true;
    }

    public boolean LMV() {
        if (this.cmd.length != 3) {
            log.warn("Syntax: LMV <from> <to>");
            return true;
        }
        final File from = new File(this.cmd[1]);
        final File to = new File(this.cmd[2]);
        if (!to.exists()) {
            if (from.renameTo(to)) {
                log.info("---- \"" + from.toString() + "\" renamed to \"" + to.toString() + "\"");
            } else {
                log.warn("rename failed");
            }
        } else {
            log.warn("\"" + to.toString() + "\" already exists");
        }
        return true;
    }

    public boolean LPWD() {
        if (this.cmd.length != 1) {
            log.warn("Syntax: LPWD  (no parameter)");
            return true;
        }
        log.info("---- Local path: " + this.currentLocalPath.toString());
        return true;
    }

    public boolean LRD() {
        return LMKDIR();
    }

    public boolean LRMDIR() {
        if (this.cmd.length != 2) {
            log.warn("Syntax: LRMDIR <folder-name>");
            return true;
        }
        final File f = new File(this.currentLocalPath, this.cmd[1]);
        if (!f.exists()) {
            log.warn("Error: local folder " + this.cmd[1] + " does not exist");
        } else {
            if (!f.delete()) {
                log.warn("Error: deletion of local folder " + this.cmd[1] + " failed");
            }
        }
        return true;
    }

    public boolean LRM() {
        if (this.cmd.length != 2) {
            log.warn("Syntax: LRM <file-name>");
            return true;
        }
        final File f = new File(this.currentLocalPath, this.cmd[1]);
        if (!f.exists()) {
            log.warn("Error: local file " + this.cmd[1] + " does not exist");
        } else {
            if (!f.delete()) {
                log.warn("Error: deletion of file " + this.cmd[1] + " failed");
            }
        }
        return true;
    }

    public boolean LS() {
        if (this.cmd.length > 2) {
            log.warn("Syntax: LS [<path>|<file>]");
            return true;
        }
        if (notConnected()) {
            return LLS();
        }
        try {
            List<String> l;
            if (this.cmd.length == 2) {
                l = list(this.cmd[1], true);
            } else {
                l = list(".", true);
            }
            printElements(l);
        } catch (final IOException e) {
            log.warn("Error: remote list not available (2): " + e.getMessage());
        }
        return true;
    }

    /**
     * @param list
     */
    private void printElements(final List<String> list) {
        log.info("---- v---v---v---v---v---v---v---v---v---v---v---v---v---v---v---v---v---v---v");
        for (final String element : list) {
            log.info(element);
        }
        log.info("---- ^---^---^---^---^---^---^---^---^---^---^---^---^---^---^---^---^---^---^");
    }

    public List<String> list(final String path, final boolean extended) throws IOException {

        createDataSocket();

        send("CWD " + path);
        String reply = receive();
        // get status code
        int status = getStatus(reply);
        if (status > 2) {
            throw new IOException(reply);
        }

        // send command to the control port
        if (extended) {
            send("LIST");
        } else {
            send("NLST");
        }

        // read status of the command from the control port
        reply = receive();

        // get status code
        status = getStatus(reply);
        if (status != 1) {
            throw new IOException(reply);
        }

        // starting data transaction
        final Socket dataSocket = getDataSocket();
        final BufferedReader dataStream = new BufferedReader(new InputStreamReader(dataSocket.getInputStream()));

        // read file system data
        String line;
        final ArrayList<String> files = new ArrayList<String>();
        try {
            while ((line = dataStream.readLine()) != null) {
                if (!line.startsWith("total ")) {
                    files.add(line);
                }
            }
        } catch (final IOException e1) {
            e1.printStackTrace();
        } finally {try {
            // shutdown data connection
            dataStream.close(); // Closing the returned InputStream will
            closeDataSocket(); // close the associated socket.
        } catch (final IOException e) {
            e.printStackTrace();
        }}
        // after stream is empty we should get control completion echo
        reply = receive();
        //System.out.println("reply of LIST: " + reply);
        // boolean success = !isNotPositiveCompletion(reply);
        //for (String s: files) System.out.println("FILES of '" + path + "': " + s);

        files.trimToSize();
        return files;
    }

    public boolean MDIR() {
        return MKDIR();
    }

    public boolean MKDIR() {
        if (this.cmd.length != 2) {
            log.warn("Syntax: MKDIR <folder-name>");
            return true;
        }
        if (notConnected()) {
            return LMKDIR();
        }
        try {
            // send mkdir command
            send("MKD " + this.cmd[1]);
            // read reply
            final String reply = receive();
            if (isNotPositiveCompletion(reply)) {
                throw new IOException(reply);
            }
        } catch (final IOException e) {
            log.warn("Error: creation of folder " + this.cmd[1] + " failed");
        }
        return true;
    }

    public boolean MGET() {
        if (this.cmd.length != 2) {
            log.warn("Syntax: MGET <file-pattern>");
            return true;
        }
        try {
            mget(this.cmd[1], false);
        } catch (final IOException e) {
            log.warn("Error: mget failed (" + e.getMessage() + ")");
        }
        return true;
    }

    private void mget(final String pattern, final boolean remove) throws IOException {
        final List<String> l = list(".", false);
        File local;
        for (final String remote : l) {
            if (matches(remote, pattern)) {
                local = new File(this.currentLocalPath, remote);
                if (local.exists()) {
                    log.warn("Warning: local file " + local.toString() + " overwritten.");
                    if(!local.delete())
                        log.warn("Warning: local file " + local.toString() + " could not be deleted.");
                }
                retrieveFilesRecursively(remote, remove);
            }
        }
    }

    public boolean MOVEDOWN() {
        if (this.cmd.length != 2) {
            log.warn("Syntax: MOVEDOWN <file-pattern>");
            return true;
        }
        try {
            mget(this.cmd[1], true);
        } catch (final IOException e) {
            log.warn("Error: movedown failed (" + e.getMessage() + ")");
        }
        return true;
    }

    /**
     * public boolean MOVEUP() { }
     *
     * @return
     */
    public boolean MV() {
        if (this.cmd.length != 3) {
            log.warn("Syntax: MV <from> <to>");
            return true;
        }
        if (notConnected()) {
            return LMV();
        }
        try {
            // send rename commands
            send("RNFR " + this.cmd[1]);
            // read reply
            String reply = receive();
            if (isNotPositiveCompletion(reply)) {
                throw new IOException(reply);
            }
            send("RNTO " + this.cmd[2]);
            // read reply
            reply = receive();
            if (isNotPositiveCompletion(reply)) {
                throw new IOException(reply);
            }
        } catch (final IOException e) {
            log.warn("Error: rename of " + this.cmd[1] + " to " + this.cmd[2] + " failed.");
        }
        return true;
    }

    public boolean NOOP() {
        if (this.cmd.length != 1) {
            log.warn("Syntax: NOOP  (no parameter)");
            return true;
        }
        try {
            literal("NOOP");
        } catch (final IOException e) {
            log.warn("Error: server does not know how to do nothing");
        }
        return true;
    }

    public boolean OPEN() {
        if ((this.cmd.length < 2) || (this.cmd.length > 3)) {
            log.warn("Syntax: OPEN <host> [<port>]");
            return true;
        }
        int port = 21;
        if (this.cmd.length == 3) {
            try {
                port = java.lang.Integer.parseInt(this.cmd[2]);
            } catch (final NumberFormatException e) {
                port = 21;
            }
        }
        if (this.cmd[1].indexOf(':',0) > 0) {
            // port is given
            port = java.lang.Integer.parseInt(this.cmd[1].substring(this.cmd[1].indexOf(':',0) + 1));
            this.cmd[1] = this.cmd[1].substring(0, this.cmd[1].indexOf(':',0));
        }
        try {
            open(this.cmd[1], port);
            log.info("---- Connection to " + this.cmd[1] + " established.");
            this.prompt = "ftp [" + this.cmd[1] + "]>";
        } catch (final IOException e) {
            log.warn("Error: connecting " + this.cmd[1] + " on port " + port + " failed: " + e.getMessage());
        }
        return true;
    }

    public void open(final String host, final int port) throws IOException {
        if (this.ControlSocket != null) {
            exec("close", false); // close any existing connections first
        }

        try {
            this.ControlSocket = new Socket();
            this.ControlSocket.setSoTimeout(getTimeout());
            this.ControlSocket.setKeepAlive(true);
            this.ControlSocket.setTcpNoDelay(true); // no accumulation until buffer is full
            this.ControlSocket.setSoLinger(false, getTimeout()); // !wait for all data being written on close()
            this.ControlSocket.setSendBufferSize(1440); // read http://www.cisco.com/warp/public/105/38.shtml
            this.ControlSocket.setReceiveBufferSize(1440); // read http://www.cisco.com/warp/public/105/38.shtml
            this.ControlSocket.connect(new InetSocketAddress(host, port), 1000);
            this.clientInput = new BufferedReader(new InputStreamReader(this.ControlSocket.getInputStream()));
            this.clientOutput = new DataOutputStream(new BufferedOutputStream(this.ControlSocket.getOutputStream()));

            // read and return server message
            this.host = host;
            this.port = port;
            this.remotemessage = receive();
            if ((this.remotemessage != null) && (this.remotemessage.length() > 3)) {
                this.remotemessage = this.remotemessage.substring(4);
            }
        } catch (final IOException e) {
            // if a connection was opened, it should not be used
            closeConnection();
            throw new IOException(e.getMessage());
        }
    }

    /**
     * @return
     */
    public boolean notConnected() {
        return this.ControlSocket == null;
    }

    /**
     * close all sockets
     *
     * @throws IOException
     */
    private void closeConnection() throws IOException {
        // cleanup
        if (this.clientOutput != null) this.clientOutput.close();
        if (this.clientInput != null) this.clientInput.close();
        if (this.ControlSocket != null) this.ControlSocket.close();
        if (this.DataSocketActive != null) this.DataSocketActive.close();
        if (this.DataSocketPassive != null) this.DataSocketPassive.close();
    }

    public boolean PROMPT() {
        log.warn("prompt is always off");
        return true;
    }

    public boolean PUT() {
        if ((this.cmd.length < 2) || (this.cmd.length > 3)) {
            log.warn("Syntax: PUT <local-file> [<remote-file>]");
            return true;
        }
        final File local = new File(this.currentLocalPath, this.cmd[1]);
        final String remote = (this.cmd.length == 2) ? local.getName() : this.cmd[2];
        if (!local.exists()) {
            log.warn("Error: local file " + local.toString() + " does not exist.");
            log.warn("            Remote file " + remote + " not overwritten.");
        } else {
            try {
                put(local.getAbsolutePath(), remote);
            } catch (final IOException e) {
                log.warn("Error: transmitting file " + local.toString() + " failed.");
            }
        }
        return true;
    }

    public boolean PWD() {
        if (this.cmd.length > 1) {
            log.warn("Syntax: PWD  (no parameter)");
            return true;
        }
        if (notConnected()) {
            return LPWD();
        }
        try {
            log.info("---- Current remote path is: " + pwd());
        } catch (final IOException e) {
            log.warn("Error: remote path not available");
        }
        return true;
    }

    private String pwd() throws IOException {
        // send pwd command
        send("PWD");

        // read current directory
        final String reply = receive();
        if (isNotPositiveCompletion(reply)) {
            throw new IOException(reply);
        }

        // parse directory name out of the reply
        return reply.substring(5, reply.lastIndexOf('"'));
    }

    public boolean REMOTEHELP() {
        if (this.cmd.length != 1) {
            log.warn("Syntax: REMOTEHELP  (no parameter)");
            return true;
        }
        try {
            literal("HELP");
        } catch (final IOException e) {
            log.warn("Error: remote help not supported by server.");
        }
        return true;
    }

    public boolean RMDIR() {
        if (this.cmd.length != 2) {
            log.warn("Syntax: RMDIR <folder-name>");
            return true;
        }
        if (notConnected()) {
            return LRMDIR();
        }
        try {
            rmForced(this.cmd[1]);
        } catch (final IOException e) {
            log.warn("Error: deletion of folder " + this.cmd[1] + " failed.");
        }
        return true;
    }

    public boolean QUIT() {
        if (!notConnected()) {
            exec("close", false);
        }
        return false;
    }

    public boolean RECV() {
        return GET();
    }

    /**
     * size of file on ftp-server (maybe size of directory-entry is possible)
     *
     * @param path
     * @return size in bytes or -1 if size cannot be determinied
     */
    public long fileSize(final String path) {
        long size = -1;
        try {
            // extended FTP
            size = size(path);
        } catch (final IOException e) {
            // else with LIST-data
            final entryInfo info = fileInfo(path);
            if (info != null) {
                size = info.size;
            }
        }
        return size;
    }

    public int size(final String path) throws IOException {
        // get the size of a file. If the given path targets to a directory, a
        // -1 is returned
        // this function is not supported by standard rfc 959. The method is
        // descibed in RFC 3659 Extensions to FTP
        // if the method is not supported by the target server, this throws an
        // IOException with the
        // server response as exception message

        // send command to the control port
        send("SIZE " + path);

        // read status of the command from the control port
        final String reply = receive();

        if (getStatusCode(reply) != 213) {
            throw new IOException(reply);
        }

        try {
            return Integer.parseInt(reply.substring(4));
        } catch (final NumberFormatException e) {
            throw new IOException(reply);
        }
    }

    public boolean USER() {
        if (this.cmd.length != 3) {
            log.warn("Syntax: USER <user-name> <password>");
            return true;
        }
        try {
            login(this.cmd[1], this.cmd[2]);
            log.info("---- Granted access for user " + this.cmd[1] + ".");
        } catch (final IOException e) {
            log.warn("Error: authorization of user " + this.cmd[1] + " failed: " + e.getMessage());
        }
        return true;
    }

    public boolean APPEND() {
        log.warn("not yet supported");
        return true;
    }

    public boolean HELP() {
        log.info("---- ftp HELP ----");
        log.info("");
        log.info("This ftp client shell can act as command shell for the local host as well for the");
        log.info("remote host. Commands that point to the local host are preceded by 'L'.");
        log.info("");
        log.info("Supported Commands:");
        log.info("ASCII");
        log.info("   switch remote server to ASCII transfer mode");
        log.info("BINARY");
        log.info("   switch remote server to BINARY transfer mode");
        log.info("BYE");
        log.info("   quit the command shell (same as EXIT)");
        log.info("CD <path>");
        log.info("   change remote path");
        log.info("CLOSE");
        log.info("   close connection to remote host (same as DISCONNECT)");
        log.info("DEL <file>");
        log.info("   delete file on remote server (same as RM)");
        log.info("RM <file>");
        log.info("   remove file from remote server (same as DEL)");
        log.info("DIR [<path>|<file>] ");
        log.info("   print file information for remote directory or file");
        log.info("DISCONNECT");
        log.info("   disconnect from remote server (same as CLOSE)");
        log.info("EXIT");
        log.info("   quit the command shell (same as BYE)");
        log.info("GET <remote-file> [<local-file>]");
        log.info("   load <remote-file> from remote server and store it locally,");
        log.info("   optionally to <local-file>. if the <remote-file> is a directory,");
        log.info("   then all files in that directory are retrieved,");
        log.info("   including recursively all subdirectories.");
        log.info("GLOB");
        log.info("   toggles globbing: matching with wild cards or not");
        log.info("COPY");
        log.info("   copies local files");
        log.info("LCD <path>");
        log.info("   local directory change");
        log.info("LDEL <file>");
        log.info("   local file delete");
        log.info("LDIR");
        log.info("   shows local directory content");
        log.info("LITERAL <ftp-command> [<command-argument>]");
        log.info("   Sends FTP commands as documented in RFC959");
        log.info("LLS");
        log.info("   as LDIR");
        log.info("LMD");
        log.info("   as LMKDIR");
        log.info("LMV <local-from> <local-to>");
        log.info("   copies local files");
        log.info("LPWD");
        log.info("   prints local path");
        log.info("LRD");
        log.info("   as LMKDIR");
        log.info("LRMD <folder-name>");
        log.info("   deletes local directory <folder-name>");
        log.info("LRM <file-name>");
        log.info("   deletes local file <file-name>");
        log.info("LS [<path>|<file>]");
        log.info("   prints list of remote directory <path> or information of file <file>");
        log.info("MDIR");
        log.info("   as MKDIR");
        log.info("MGET <file-pattern>");
        log.info("   copies files from remote server that fits into the");
        log.info("   pattern <file-pattern> to the local path.");
        log.info("MOVEDOWN <file-pattern>");
        log.info("   copies files from remote server as with MGET");
        log.info("   and deletes them afterwards on the remote server");
        log.info("MV <from> <to>");
        log.info("   moves or renames files on the local host");
        log.info("NOOP");
        log.info("   sends the NOOP command to the remote server (which does nothing)");
        log.info("   This command is usually used to measure the speed of the remote server.");
        log.info("OPEN <host[':'port]> [<port>]");
        log.info("   connects the ftp shell to the remote server <host>. Optionally,");
        log.info("   a port number can be given, the default port number is 21.");
        log.info("   Example: OPEN localhost:2121 or OPEN 192.168.0.1 2121");
        log.info("PROMPT");
        log.info("   compatibility command, that usually toggles beween prompting on or off.");
        log.info("   ftp has prompting switched off by default and cannot switched on.");
        log.info("PUT <local-file> [<remote-file>]");
        log.info("   copies the <local-file> to the remote server to the current remote path or");
        log.info("   optionally to the given <remote-file> path.");
        log.info("PWD");
        log.info("   prints current path on the remote server.");
        log.info("REMOTEHELP");
        log.info("   asks the remote server to print the help text of the remote server");
        log.info("RMDIR <folder-name>");
        log.info("   removes the directory <folder-name> on the remote server");
        log.info("QUIT");
        log.info("   exits the ftp application");
        log.info("RECV");
        log.info("   as GET");
        log.info("USER <user-name> <password>");
        log.info("   logs into the remote server with the user <user-name>");
        log.info("   and the password <password>");
        log.info("");
        log.info("");
        log.info("EXAMPLE:");
        log.info("a standard sessions looks like this");
        log.info(">open 192.168.0.1:2121");
        log.info(">user anonymous bob");
        log.info(">pwd");
        log.info(">ls");
        log.info(">.....");
        log.info("");
        log.info("");
        return true;
    }

    public boolean QUOTE() {
        log.warn("not yet supported");
        return true;
    }

    public boolean BELL() {
        log.warn("not yet supported");
        return true;
    }

    public boolean MDELETE() {
        log.warn("not yet supported");
        return true;
    }

    public boolean SEND() {
        log.warn("not yet supported");
        return true;
    }

    public boolean DEBUG() {
        log.warn("not yet supported");
        return true;
    }

    public boolean MLS() {
        log.warn("not yet supported");
        return true;
    }

    public boolean TRACE() {
        log.warn("not yet supported");
        return true;
    }

    public boolean MPUT() {
        log.warn("not yet supported");
        return true;
    }

    public boolean TYPE() {
        log.warn("not yet supported");
        return true;
    }

    public boolean CREATE() {
        log.warn("not yet supported");
        return true;
    }

    // helper functions

    private boolean matches(final String name, final String pattern) {
        // checks whether the string name matches with the pattern
        // the pattern may contain characters '*' as wildcard for several
        // characters (also none) and '?' to match exactly one characters
        // log.info("MATCH " + name + " " + pattern);
        if (!this.glob) {
            return name.equals(pattern);
        }
        if (pattern.equals("*")) {
            return true;
        }
        if (pattern.length() > 0 && pattern.charAt(0) == '*' && pattern.endsWith("*")) {
            return // avoid recursion deadlock
            ((matches(name, pattern.substring(1))) || (matches(name, pattern.substring(0, pattern.length() - 1))));
        }
        try {
            int i = pattern.indexOf('?',0);
            if (i >= 0) {
                if (!(matches(name.substring(0, i), pattern.substring(0, i)))) {
                    return false;
                }
                return (matches(name.substring(i + 1), pattern.substring(i + 1)));
            }
            i = pattern.indexOf('*',0);
            if (i >= 0) {
                if (!(name.substring(0, i).equals(pattern.substring(0, i)))) {
                    return false;
                }
                if (pattern.length() == i + 1) {
                    return true; // pattern would be '*'
                }
                return (matches(reverse(name.substring(i)), reverse(pattern.substring(i + 1)) + "*"));
            }
            return name.equals(pattern);
        } catch (final java.lang.StringIndexOutOfBoundsException e) {
            // this is normal. it's a lazy implementation
            return false;
        }
    }

    private String reverse(final String s) {
        if (s.length() < 2) {
            return s;
        }
        return reverse(s.substring(1)) + s.charAt(0);
    }

    // protocoll socket commands

    private void send(final String buf) throws IOException {
        if (this.clientOutput == null) return;
        byte[] b = buf.getBytes(StandardCharsets.UTF_8);
        this.clientOutput.write(b, 0, b.length);
        this.clientOutput.write('\r');
        this.clientOutput.write('\n');
        this.clientOutput.flush();
        if (buf.startsWith("PASS")) {
            log.info("> PASS ********");
        } else {
            log.info("> " + buf);
        }
    }

    private String receive() throws IOException {
        // last reply starts with 3 digit number followed by space
        String reply;

        while (true) {
            if (this.clientInput == null) {
                throw new IOException("Server has presumably shut down the connection.");
            }
            reply = this.clientInput.readLine();

            // sanity check
            if (reply == null) {
                throw new IOException("Server has presumably shut down the connection.");
            }

            log.info("< " + reply);
            // serverResponse.addElement(reply);

            if (reply.length() >= 4 && Character.isDigit(reply.charAt(0)) && Character.isDigit(reply.charAt(1))
                    && Character.isDigit(reply.charAt(2)) && (reply.charAt(3) == ' ')) {
                break; // end of reply
            }
        }
        // return last reply line
        return reply;
    }

    private void sendTransferType(final char type) throws IOException {
        send("TYPE " + type);

        final String reply = receive();
        if (isNotPositiveCompletion(reply)) {
            throw new IOException(reply);
        }
    }

    /**
     * @return
     * @throws IOException
     */
    private Socket getDataSocket() throws IOException {
        Socket data;
        if (isPassive()) {
            if (this.DataSocketPassive == null) {
                createDataSocket();
            }
            data = this.DataSocketPassive;
        } else {
            if (this.DataSocketActive == null) {
                createDataSocket();
            }
            data = this.DataSocketActive.accept();
        }
        return data;
    }

    /**
     * create data channel
     *
     * @throws IOException
     */
    private void createDataSocket() throws IOException {
        if (isPassive()) {
            try {
                createPassiveDataPort();
            } catch (final IOException e) {
                createActiveDataPort();
            }
        } else {
            try {
                createActiveDataPort();
            } catch (final IOException e) {
                createPassiveDataPort();
            }
        }
    }

    /**
     * use passive ftp?
     *
     * @return
     */
    private boolean isPassive() {
        return this.DataSocketPassiveMode;
    }

    private void createActiveDataPort() throws IOException {
        // create data socket and bind it to free port available
        this.DataSocketActive = new ServerSocket(0);
        this.DataSocketActive.setSoTimeout(getTimeout());
        this.DataSocketActive.setReceiveBufferSize(1440); // read http://www.cisco.com/warp/public/105/38.shtml
        applyDataSocketTimeout();

        // get port socket has been bound to
        final int DataPort = this.DataSocketActive.getLocalPort();

        // client ip
        // InetAddress LocalIp = serverCore.publicIP();
        // InetAddress LocalIp =
        // DataSocketActive.getInetAddress().getLocalHost();

        // save ip address in high byte order
        // byte[] Bytes = LocalIp.getAddress();
        final byte[] b = Domains.myPublicIPv4().iterator().next().getAddress();

        // bytes greater than 127 should not be printed as negative
        final short[] s = new short[4];
        for (int i = 0; i < 4; i++) {
            s[i] = b[i];
            if (s[i] < 0) {
                s[i] += 256;
            }
        }

        // send port command via control socket:
        // four ip address shorts encoded and two port shorts encoded
        send("PORT "
                +
                // "127,0,0,1," +
                s[0] + "," + s[1] + "," + s[2] + "," + s[3] + "," + ((DataPort & 0xff00) >> 8)
                + "," + (DataPort & 0x00ff));

        // read status of the command from the control port
        final String reply = receive();

        // check status code
        if (isNotPositiveCompletion(reply)) {
            throw new IOException(reply);
        }

        this.DataSocketPassiveMode = false;
    }

    private void createPassiveDataPort() throws IOException {
        // send port command via control socket:
        // four ip address shorts encoded and two port shorts encoded
        send("PASV");

        // read status of the command from the control port
        String reply = receive();

        // check status code
        if (getStatusCode(reply) != 227) {
            throw new IOException(reply);
        }

        // parse the status return: address should start at the first number
        int pos = 4;
        while ((pos < reply.length()) && ((reply.charAt(pos) < '0') || (reply.charAt(pos) > '9'))) {
            pos++;
        }
        if (pos >= reply.length()) {
            throw new IOException(reply + " [could not parse return code]");
        }
        reply = reply.substring(pos);
        pos = reply.length() - 1;
        while ((pos >= 0) && ((reply.charAt(pos) < '0') || (reply.charAt(pos) > '9'))) {
            pos--;
        }
        if (pos < 0) {
            throw new IOException("[could not parse return code: no numbers]");
        }
        reply = reply.substring(0, pos + 1);
        final StringTokenizer st = new StringTokenizer(reply, ",");
        if (st.countTokens() != 6) {
            throw new IOException("[could not parse return code: wrong number of numbers]");
        }

        // set the data host and port
        final int a = Integer.parseInt(st.nextToken());
        final int b = Integer.parseInt(st.nextToken());
        final int c = Integer.parseInt(st.nextToken());
        final int d = Integer.parseInt(st.nextToken());
        final InetAddress datahost = Domains.dnsResolve(a + "." + b + "." + c + "." + d);
        final int high = Integer.parseInt(st.nextToken());
        final int low = Integer.parseInt(st.nextToken());
        if (high < 0 || high > 255 || low < 0 || low > 255) {
            throw new IOException("[could not parse return code: syntax error]");
        }
        final int dataport = (high << 8) + low;

        this.DataSocketPassive = new Socket(datahost, dataport);
        applyDataSocketTimeout();
        this.DataSocketPassiveMode = true;
    }

    /**
     * closes data connection
     *
     * @throws IOException
     */
    private void closeDataSocket() throws IOException {
        if (isPassive()) {
            if (this.DataSocketPassive != null) {
                this.DataSocketPassive.close();
                this.DataSocketPassive = null;
            }
        } else {
            if (this.DataSocketActive != null) {
                this.DataSocketActive.close();
                this.DataSocketActive = null;
            }
        }
    }

    /**
     * sets the timeout for the socket
     *
     * @throws SocketException
     */
    private void applyDataSocketTimeout() throws SocketException {
        if (isPassive()) {
            if (this.DataSocketPassive != null) {
                this.DataSocketPassive.setSoTimeout(this.DataSocketTimeout * 1000);
            }
        } else {
            if (this.DataSocketActive != null) {
                this.DataSocketActive.setSoTimeout(this.DataSocketTimeout * 1000);
            }
        }
    }

    private void get(final String fileDest, final String fileName) throws IOException {
        // store time for statistics
        final long start = System.currentTimeMillis();

        createDataSocket();

        // set type of the transfer
        sendTransferType(transferType);

        // send command to the control port
        send("RETR " + fileName);

        // read status of the command from the control port
        final String reply = receive();

        // get status code
        final int status = getStatus(reply);

        // starting data transaction
        if (status == 1) {
            Socket data = null;
            InputStream ClientStream = null;
            RandomAccessFile outFile = null;
            int length = 0;
            try {
                data = getDataSocket();
                ClientStream = data.getInputStream();

                // create local file
                if (fileDest == null) {
                    outFile = new RandomAccessFile(fileName, "rw");
                } else {
                    outFile = new RandomAccessFile(fileDest, "rw");
                }

                // write remote file to local file
                final byte[] block = new byte[blockSize];
                int numRead;

                while ((numRead = ClientStream.read(block)) != -1) {
                    outFile.write(block, 0, numRead);
                    length = length + numRead;
                }
            } finally {
                // shutdown connection
                if(outFile != null) {
                    outFile.close();
                }
                if(ClientStream != null) {
                    ClientStream.close();
                }
                closeDataSocket();
            }

            // after stream is empty we should get control completion echo
            /*reply =*/ receive();
            // boolean success = !isNotPositiveCompletion(reply);
            // if (!success) throw new IOException(reply);

            // write statistics
            final long stop = System.currentTimeMillis();
            log.info(" ---- downloaded "
                    + ((length < 2048) ? length + " bytes" : (length / 1024) + " kbytes")
                    + " in "
                    + (((stop - start) < 2000) ? (stop - start) + " milliseconds"
                            : (((int) ((stop - start) / 100)) / 10) + " seconds"));
            if (start == stop) {
                log.warn("start == stop");
            } else {
                log.info(" (" + (length * 1000 / 1024 / (stop - start)) + " kbytes/second)");
            }

        } else {
            throw new IOException(reply);
        }
    }


    public byte[] get(final String fileName) throws IOException {

        createDataSocket();

        // set type of the transfer
        sendTransferType(transferType);

        // send command to the control port
        send("RETR " + fileName);

        // read status of the command from the control port
        final String reply = receive();

        // get status code
        final int status = getStatus(reply);

        // starting data transaction
        if (status == 1) {
            Socket data = null;
            InputStream ClientStream = null;
            final ByteArrayOutputStream os = new ByteArrayOutputStream();
            int length = 0;
            try {
                data = getDataSocket();
                ClientStream = data.getInputStream();

                // write remote file to local file
                final byte[] block = new byte[blockSize];
                int numRead;

                while ((numRead = ClientStream.read(block)) != -1) {
                    os.write(block, 0, numRead);
                    length = length + numRead;
                }
            } finally {
                // shutdown connection
                if (ClientStream != null) {
                    ClientStream.close();
                }
                closeDataSocket();
            }

            // after stream is empty we should get control completion echo
            /*reply =*/ receive();
            // boolean success = !isNotPositiveCompletion(reply);
            return os.toByteArray();
        }
        throw new IOException(reply);
    }


    private void put(final String fileName, final String fileDest) throws IOException {

        createDataSocket();

        // set type of the transfer
        sendTransferType(transferType);

        // send command to the control port
        if (fileDest == null) {
            send("STOR " + fileName);
        } else {
            send("STOR " + fileDest);
        }

        // read status of the command from the control port
        String reply = receive();

        // starting data transaction
        if (getStatus(reply) == 1) {
            final Socket data = getDataSocket();
            final OutputStream ClientStream = data.getOutputStream();

            // read from local file
            final RandomAccessFile inFile = new RandomAccessFile(fileName, "r");

            // write remote file to local file
            final byte[] block = new byte[blockSize];
            int numRead;

            while ((numRead = inFile.read(block)) >= 0) {
                ClientStream.write(block, 0, numRead);
            }

            // shutdown and cleanup
            inFile.close();
            ClientStream.close();

            // shutdown remote client connection
            data.close();

            // after stream is empty we should get control completion echo
            reply = receive();
            final boolean success = (getStatus(reply) == 2);

            if (!success) {
                throw new IOException(reply);
            }

        } else {
            throw new IOException(reply);
        }
    }

    /**
     * Login to server
     *
     * @param account
     * @param password
     * @throws IOException
     */
    public void login(final String account, final String password) throws IOException {
        unsetLoginData();

        // send user name
        send("USER " + account);

        String reply = receive();
        switch (getStatus(reply)) {
        case 2:
            // User logged in, proceed.
            break;
        case 5:// 530 Not logged in.
        case 4:
        case 1:// in RFC959 an error (page 57, diagram for the Login
            // sequence)
            throw new IOException(reply);
        default:
            // send password
            send("PASS " + password);

            reply = receive();
            if (isNotPositiveCompletion(reply)) {
                throw new IOException(reply);
            }
        }
        setLoginData(account, password, reply);
    }

    /**
     * we are authorized to use the server
     *
     * @return
     */
    public boolean isLoggedIn() {
        return (this.account != null && this.password != null && this.remotegreeting != null);
    }

    /**
     * remember username and password which were used to login
     *
     * @param account
     * @param password
     * @param reply
     *                remoteGreeting
     */
    private void setLoginData(final String account, final String password, final String reply) {
        this.account = account;
        this.password = password;
        this.remotegreeting = reply;
    }

    private void unsetLoginData() {
        this.account = null;
        this.password = null;
        this.remotegreeting = null;
    }

    public void sys() throws IOException {
        // send system command
        send("SYST");

        // check completion
        final String systemType = receive();
        if (isNotPositiveCompletion(systemType)) {
            throw new IOException(systemType);
        }

        // exclude status code from reply
        this.remotesystem = systemType.substring(4);
    }

    private void literal(final String commandLine) throws IOException {
        // send the complete line
        send(commandLine);

        // read reply
        final String reply = receive();

        if (getStatus(reply) == 5) {
            throw new IOException(reply);
        }
    }

    /**
     * control socket timeout
     *
     * @return
     */
    public int getTimeout() {
        return ControlSocketTimeout;
    }

    /**
     * after this time the data connection is closed
     *
     * @param timeout
     *                in seconds, 0 = infinite
     */
    public void setDataSocketTimeout(final int timeout) {
        this.DataSocketTimeout = timeout;

        try {
            applyDataSocketTimeout();
        } catch (final SocketException e) {
            log.warn("setDataSocketTimeout: " + e.getMessage());
        }
    }

    public static List<String> dir(final String host, final String remotePath, final String account,
            final String password, final boolean extended) {
        try {
            final FTPClient c = new FTPClient();
            c.cmd = new String[]{"open", host};
            c.OPEN();
            c.cmd = new String[]{"user", account, password};
            c.USER();
            c.cmd = new String[]{"ls"};
            final List<String> v = c.list(remotePath, extended);
            c.cmd = new String[]{"close"};
            c.CLOSE();
            c.cmd = new String[]{"exit"};
            c.EXIT();
            return v;
        } catch (final SecurityException | IOException e) {
            return null;
        }
    }

    private static void dir(final String host, final String remotePath, final String account, final String password) {
        try {
            final FTPClient c = new FTPClient();
            c.exec("open " + host, false);
            c.exec("user " + account + " " + password, false);
            c.exec("cd " + remotePath, false);
            c.exec("ls", true);
            c.exec("close", false);
            c.exec("exit", false);
        } catch (final SecurityException e) {
        }
    }

    /**
     * Asynchronously generate a list of all files on a ftp server using the anonymous account.
     * @param host host name or address
     * @param port ftp port
     * @param user user name
     * @param pw user password
     * @param path path on the ftp site
     * @param depth the maximum depth of the sub folders exploration.
     * @return a queue asynchronously filled with entryInfo from all files of the ftp server
     * @throws IOException when a error occurred
     */
    public static BlockingQueue<entryInfo> sitelist(final String host, final int port, final String user, final String pw, final String path, final int depth) throws IOException {
        final FTPClient ftpClient = new FTPClient();
        ftpClient.open(host, port);
        ftpClient.login(user, pw);
        final LinkedBlockingQueue<entryInfo> queue = new LinkedBlockingQueue<entryInfo>();
        new Thread("FTP.sitelist(" + host + ":" + port + ")") {
            @Override
            public void run() {
                try {
                    sitelist(ftpClient, path, queue, depth);
                    ftpClient.quit();
                } catch (final Exception e) {} finally {
                    queue.add(POISON_entryInfo);
                }
            }
        }.start();
        return queue;
    }
    
	/**
	 * Feed the queue with files under a given path on a ftp server using
	 * the anonymous account. When path is a file path, only one entry is added
	 * to the queue.
	 * 
	 * @param ftpClient
	 *            fptClient initialized with a host and login information
	 * @param path
	 *            path on the host
	 * @param queue
	 *            the entries queue to feed
	 * @param depth
	 *            the maximum depth of the sub folders exploration.
	 * @throws IOException
	 *             when a error occurred
	 */
    private static void sitelist(final FTPClient ftpClient, String path, final LinkedBlockingQueue<entryInfo> queue, int depth) {
        List<String> list;
        try {
            list = ftpClient.list(path, true);
        } catch (final IOException e) {
        	/* path might be a file path */
        	if (!path.endsWith("/")) {
				entryInfo info = ftpClient.fileInfo(path);
				if (info != null) {
					queue.add(info);
				} else {
					/* We could not get file information, but this doesn't mean the file does not exist : 
					 * we add it anyway to the queue */
					info = new entryInfo();
					info.name = path;
					queue.add(info);
				}
        	} else {
        		ConcurrentLog.logException(e);
        	}
            return;
        }
        if (!path.endsWith("/")) path += "/";
        entryInfo info;
        // first find all files and add them to the crawl list
        for (final String line : list) {
            info = parseListData(line);
            if (info != null && info.type == filetype.file && !info.name.endsWith(".") && !info.name.startsWith(".")) {
                if (!info.name.startsWith("/")) info.name = path + info.name;
                queue.add(info);
            }
        }
        // then find all directories and add them recursively if depth is over zero
        if(depth > 0) {
        	for (final String line : list) {
        		//System.out.println("LIST:" + line);
        		info = parseListData(line);
        		if (info != null && !info.name.endsWith(".") && !info.name.startsWith(".")) {
        			if (info.type == filetype.directory) {
        				sitelist(ftpClient, path + info.name, queue, depth - 1);
        			} else if (info.type == filetype.link) {
        				final int q = info.name.indexOf("->",0);
        				if (q >= 0 && info.name.indexOf("..", q) < 0) {
        					//System.out.println("*** LINK:" + line);
        					info.name = info.name.substring(0, q).trim();
        					sitelist(ftpClient, path + info.name, queue, depth - 1);
        				}

        			}
        		}
        	}
        }
    }

    public StringBuilder dirhtml(String remotePath) throws IOException {
        // returns a directory listing using an existing connection
            if (isFolder(remotePath) && '/' != remotePath.charAt(remotePath.length()-1)) {
                remotePath += '/';
            }
            final String pwd = pwd();
            final List<String> list = list(remotePath, true);
            if (this.remotesystem == null) try {sys();} catch (final IOException e) {}
            final String base = "ftp://" + ((this.account.equals(ANONYMOUS)) ? "" : (this.account + ":" + this.password + "@"))
                    + this.host + ((this.port == 21) ? "" : (":" + this.port)) + ((remotePath.length() > 0 && remotePath.charAt(0) == '/') ? "" : pwd + "/")
                    + remotePath;

            return dirhtml(base, this.remotemessage, this.remotegreeting, this.remotesystem, list, true);
    }

    private static StringBuilder dirhtml(
            final String host, final int port, final String remotePath,
            final String account, final String password) throws IOException {
        // opens a new connection and returns a directory listing as html
        final FTPClient c = new FTPClient();
        c.open(host, port);
        c.login(account, password);
        c.sys();
        final StringBuilder page = c.dirhtml(remotePath);
        c.quit();
        return page;
    }

    public static StringBuilder dirhtml(
            final String base, final String servermessage, final String greeting,
            final String system, final List<String> list,
            final boolean metaRobotNoindex) {
        // this creates the html output from collected strings
        final StringBuilder page = new StringBuilder(1024);
        final String title = "Index of " + base;

        page.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n");
        page.append("<html><head>\n");
        page.append("  <title>").append(title).append("</title>\n");
        page.append("  <meta name=\"generator\" content=\"YaCy directory listing\">\n");
        if (metaRobotNoindex) {
            page.append("  <meta name=\"robots\" content=\"noindex\">\n");
        }
        page.append("  <base href=\"").append(base).append("\">\n");
        page.append("</head><body>\n");
        page.append("  <h1>").append(title).append("</h1>\n");
        if (servermessage != null && greeting != null) {
            page.append("  <p><pre>Server \"").append(servermessage).append("\" responded:\n");
            page.append("  \n");
            page.append(greeting);
            page.append("\n");
            page.append("  </pre></p>\n");
        }
        page.append("  <hr>\n");
        page.append("  <pre>\n");
        int nameStart, nameEnd;
        entryInfo info;
        for (final String line : list) {
            info = parseListData(line);
            if (info != null) {
                // with link
                nameStart = line.indexOf(info.name);
                page.append(line.substring(0, nameStart));
                page.append("<a href=\"").append(base).append(info.name).append((info.type == filetype.directory) ? "/" : "").append("\">").append(info.name).append("</a>");
                nameEnd = nameStart + info.name.length();
                if (line.length() > nameEnd) {
                    page.append(line.substring(nameEnd));
                }
            } else if (line.startsWith("http://") || line.startsWith("ftp://") || line.startsWith("smb://") || line.startsWith("file://")) {
                page.append("<a href=\"").append(line).append("\">").append(line).append("</a>");
            } else {
               // raw
               page.append(line);
            }
            page.append('\n');
        }
        page.append("  </pre>\n");
        page.append("  <hr>\n");
        if (system != null) page.append("  <pre>System info: \"").append(system).append("\"</pre>\n");
        page.append("</body></html>\n");

        return page;
    }

    public static String put(final String host, File localFile, String remotePath, final String remoteName,
            final String account, final String password) throws IOException {
        // returns the log
        try {
            final ByteArrayOutputStream bout = new ByteArrayOutputStream();
            final PrintStream out = new PrintStream(bout);

            final ByteArrayOutputStream berr = new ByteArrayOutputStream();
            final PrintStream err = new PrintStream(berr);

            final FTPClient c = new FTPClient();
            c.exec("open " + host, false);
            c.exec("user " + account + " " + password, false);
            if (remotePath != null) {
                remotePath = remotePath.replace('\\', '/');
                c.exec("cd " + remotePath, false);
            }
            c.exec("binary", false);
            if (localFile.isAbsolute()) {
                c.exec("lcd \"" + localFile.getParent() + "\"", false);
                localFile = new File(localFile.getName());
            }
            c.exec("put " + localFile.toString() + ((remoteName.isEmpty()) ? "" : (" " + remoteName)), false);
            c.exec("close", false);
            c.exec("exit", false);

            out.close();
            err.close();

            final String outLog = bout.toString();
            bout.close();

            final String errLog = berr.toString();
            berr.close();

            if (errLog.length() > 0) {
                throw new IOException("Ftp put failed:\n" + errLog);
            }

            return outLog;
        } catch (final IOException e) {
            throw e;
        }
    }

    public static void get(final String host, String remoteFile, final File localPath, final String account, final String password) {
        try {
            final FTPClient c = new FTPClient();
            if (remoteFile.isEmpty()) {
                remoteFile = "/";
            }
            c.exec("open " + host, false);
            c.exec("user " + account + " " + password, false);
            c.exec("lcd " + localPath.getAbsolutePath(), false);
            c.exec("binary", false);
            c.exec("get " + remoteFile + " " + localPath.getAbsoluteFile().toString(), false);
            c.exec("close", false);
            c.exec("exit", false);
        } catch (final SecurityException e) {
        }
    }

    public static void getAnonymous(final String host, final String remoteFile, final File localPath) {
        get(host, remoteFile, localPath, ANONYMOUS, "anomic");
    }

    /**
     * class that puts a file on a ftp-server can be used as a thread
     */
    static class pt implements Runnable {
        String host;
        File localFile;
        String remotePath;
        String remoteName;
        String account;
        String password;

        public pt(final String h, final File l, final String rp, final String rn, final String a, final String p) {
            this.host = h;
            this.localFile = l;
            this.remotePath = rp;
            this.remoteName = rn;
            this.account = a;
            this.password = p;
        }

        @Override
        public final void run() {
            try {
                Thread.currentThread().setName("FTP.pt(" + this.host + ")");
                put(this.host, this.localFile, this.remotePath, this.remoteName, this.account, this.password);
            } catch (final IOException e) {
                log.warn(e.getMessage(), e);
            }
        }
    }

    public static Thread putAsync(final String host, final File localFile, final String remotePath,
            final String remoteName, final String account, final String password) {
        final Thread t = new Thread(new pt(host, localFile, remotePath, remoteName, account, password), "ftp to " + host);
        t.start();
        return t; // return value can be used to determine status of transfer
        // with isAlive() or join()
    }

    private static void printHelp() {
        System.out.println("FTPClient help");
        System.out.println("----------");
        System.out.println();
        System.out.println("The following commands are supported");
        System.out.println("java net.yacy.cora.protocol.ftp.FTPClient -h  -- prints this help");
        System.out.println("java net.yacy.cora.protocol.ftp.FTPClient -dir <host>[':'<port>] <path> [<account> <password>]");
        System.out.println("java net.yacy.cora.protocol.ftp.FTPClient -htmldir <host> <path>");
        System.out.println("java net.yacy.cora.protocol.ftp.FTPClient -get <host>[':'<port>] <remoteFile> <localPath> [<account> <password>]");
        System.out.println("java net.yacy.cora.protocol.ftp.FTPClient -put <host>[':'<port>] <localFile> <remotePath> <account> <password>");
        System.out.println("java net.yacy.cora.protocol.ftp.FTPClient -sitelist <host> <port> <depth>");
        System.out.println();
    }

    public static void main(final String[] args) {
    	try {
    		System.out.println("WELCOME TO THE ANOMIC FTP CLIENT v" + vDATE);
    		System.out.println("Visit http://www.anomic.de and support shareware!");
    		System.out.println("try -h for command line options");
    		System.out.println();
    		if (args.length == 1) {
    			if (args[0].equals("-h")) {
    				printHelp();
    			}
    		} else if (args.length == 2) {
    			printHelp();
    		} else if (args.length == 3) {
    			if (args[0].equals("-dir")) {
    				dir(args[1], args[2], ANONYMOUS, "anomic@");
    			} else if (args[0].equals("-htmldir")) {
					final File file = new File("dirindex.html");
    				try (FileOutputStream fos = new FileOutputStream(file);) {
    					final StringBuilder page = dirhtml(args[1], 21, args[2], ANONYMOUS, "anomic@");
    					fos.write(UTF8.getBytes(page.toString()));
    				} catch (final FileNotFoundException e) {
    					log.warn(e);
    				} catch (final IOException e) {
    					log.warn(e);
    				}
    			} else {
    				printHelp();
    			}
    		} else if (args.length == 4) {
    			if (args[0].equals("-get")) {
    				getAnonymous(args[1], args[2], new File(args[3]));
    			} else if (args[0].equals("-sitelist")) {
    				try {
    					final BlockingQueue<entryInfo> q = sitelist(args[1], Integer.parseInt(args[2]), ANONYMOUS, "anomic", "/", Integer.parseInt(args[3]));
    					entryInfo entry;
    					while ((entry = q.take()) != FTPClient.POISON_entryInfo) {
    						System.out.println(entry.toString());
    					}
    				} catch (final FileNotFoundException e) {
    					log.warn(e);
    				} catch (final IOException e) {
    					log.warn(e);
    				} catch (final InterruptedException e) {
    					log.warn(e);
    				}
    			} else {
    				printHelp();
    			}
    		} else if (args.length == 5) {
    			if (args[0].equals("-dir")) {
    				dir(args[1], args[2], args[3], args[4]);
    			} else {
    				printHelp();
    			}
    		} else if (args.length == 6) {
    			if (args[0].equals("-get")) {
    				get(args[1], args[2], new File(args[3]), args[4], args[5]);
    			} else if (args[0].equals("-put")) {
    				try {
    					put(args[1], new File(args[2]), args[3], "", args[4], args[5]);
    				} catch (final IOException e) {
    					log.warn(e.getMessage(), e);
    				}
    			} else {
    				printHelp();
    			}
    		} else {
    			printHelp();
    		}
   		} finally {
    		ConcurrentLog.shutdown();
    	}
    }

}
