IcapScanFile.java

/*
 * This file is part of Waarp Project (named also Waarp or GG).
 *
 *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
 *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
 * individual contributors.
 *
 *  All Waarp Project is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with
 * Waarp . If not, see <http://www.gnu.org/licenses/>.
 */

package org.waarp.icap;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.waarp.common.command.exception.Reply550Exception;
import org.waarp.common.file.FileUtils;
import org.waarp.common.logging.WaarpLogLevel;
import org.waarp.common.logging.WaarpLogger;
import org.waarp.common.logging.WaarpLoggerFactory;
import org.waarp.common.logging.WaarpSlf4JLoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;

/**
 * IcapScanFile command to ask an ICAP server to scan a file
 * through network ICAP protocol.<br>
 * <br>
 * Options:<br>
 * -file path_to_file <br>
 * -to hostname <br>
 * [-port port, default 1344] <br>
 * -service name | -model name <br>
 * [-previewSize size, default none] <br>
 * [-blockSize size, default 8192] <br>
 * [-receiveSize size, default 65536] <br>
 * [-maxSize size, default MAX_INTEGER] <br>
 * [-timeout in_ms, default equiv to 10 min] <br>
 * [-errorMove path | -errorDelete | -sendOnError] <br>
 * [-ignoreNetworkError] <br>
 * [-ignoreTooBigFileError] <br>
 * [-keyPreview key -stringPreview string, default none] <br>
 * [-key204 key -string204 string, default none] <br>
 * [-key200 key -string200 string, default none] <br>
 * [-stringHttp string, default none] <br>
 * [-logger DEBUG|INFO|WARN|ERROR, default none] <br>
 * <br>
 * Exit with values:<br>
 * <ul>
 *   <li>0: Scan OK</li>
 *   <li>1: Bad arguments</li>
 *   <li>2: ICAP protocol error</li>
 *   <li>3: Network error</li>
 *   <li>4: Scan KO</li>
 *   <li>5: Scan KO but post action required in error</li>
 * </ul>
 */
public class IcapScanFile {
  private static final WaarpLogger logger =
      WaarpLoggerFactory.getLogger(IcapScanFile.class);

  public static final int STATUS_OK = 0;
  public static final int STATUS_BAD_ARGUMENT = 1;
  public static final int STATUS_ICAP_ISSUE = 2;
  public static final int STATUS_NETWORK_ISSUE = 3;
  public static final int STATUS_KO_SCAN = 4;
  public static final int STATUS_KO_SCAN_POST_ACTION_ERROR = 5;

  private static final String ARGUMENTS_CANNOT_BE_EMPTY_OR_NULL =
      "Arguments cannot be empty or null";

  private static final String FILE = "file";
  public static final String FILE_ARG = "-" + FILE;
  private static final Option FILE_OPTION =
      Option.builder(FILE).required(true).hasArg(true)
            .desc("Specify the file path to operate on").build();
  private static final String TO = "to";
  public static final String TO_ARG = "-" + TO;
  private static final Option HOST_OPTION =
      Option.builder(TO).required(true).hasArg(true)
            .desc("Specify the requested Host").build();
  private static final String SERVICE = "service";
  public static final String SERVICE_ARG = "-" + SERVICE;
  private static final Option SERVICE_OPTION =
      Option.builder(SERVICE).required(true).hasArg(true)
            .desc("Specify the service on remote host to use").build();
  private static final String MODEL = "model";
  public static final String MODEL_ARG = "-" + MODEL;
  private static final Option MODEL_OPTION =
      Option.builder(MODEL).required(true).hasArg(true)
            .desc("Specify the model of remote host service to use").build();
  private static final String PORT_FIELD = "port";
  private static final Option PORT_OPTION =
      Option.builder(PORT_FIELD).required(false).hasArg(true)
            .desc("Specify the port on remote host to use").type(Number.class)
            .build();
  private static final String PREVIEW_SIZE = "previewSize";
  private static final Option PREVIEW_OPTION =
      Option.builder(PREVIEW_SIZE).required(false).hasArg(true)
            .desc("Specify the Preview size to use").build();
  private static final String BLOCK_SIZE = "blockSize";
  private static final Option BLOCK_OPTION =
      Option.builder(BLOCK_SIZE).required(false).hasArg(true)
            .desc("Specify the Block size to use").build();
  private static final String RECEIVE_SIZE = "receiveSize";
  private static final Option RECEIVE_OPTION =
      Option.builder(RECEIVE_SIZE).required(false).hasArg(true)
            .desc("Specify the Receive size to use").build();
  private static final String MAX_SIZE = "maxSize";
  private static final Option MAX_SIZE_OPTION =
      Option.builder(MAX_SIZE).required(false).hasArg(true)
            .desc("Specify the Max size to use").build();
  private static final String TIMEOUT_ARG = "timeout";
  private static final Option TIMEOUT_OPTION =
      Option.builder(TIMEOUT_ARG).required(false).hasArg(true)
            .desc("Specify the timeout on socket to use").build();
  private static final String ERROR_MOVE = "errorMove";
  private static final Option ERROR_MOVE_OPTION =
      Option.builder(ERROR_MOVE).required(false).hasArg(true)
            .desc("Specify the path to use if wrong scan").build();
  private static final String ERROR_DELETE = "errorDelete";
  private static final Option ERROR_DELETE_OPTION =
      Option.builder(ERROR_DELETE).required(false).hasArg(false)
            .desc("Specify the error delete action if wrong scan").build();
  private static final String ERROR_SEND = "sendOnError";
  public static final String ERROR_SEND_ARG = "-" + ERROR_SEND;
  private static final Option ERROR_SEND_OPTION =
      Option.builder(ERROR_SEND).required(false).hasArg(false)
            .desc("Specify that scan error should be followed by an r66send")
            .build();
  private static final String IGNORE_NETWORK_CONTINUE = "ignoreNetworkError";
  private static final Option IGNORE_NETWORK_CONTINUE_OPTION =
      Option.builder(IGNORE_NETWORK_CONTINUE).required(false).hasArg(false)
            .desc("Specify that a network error should not be followed by a ko")
            .build();
  private static final String IGNORE_TOO_BIG_FILE_CONTINUE =
      "ignoreTooBigFileError";
  private static final Option IGNORE_TOO_BIG_FILE_CONTINUE_OPTION =
      Option.builder(IGNORE_TOO_BIG_FILE_CONTINUE).required(false).hasArg(false)
            .desc("Specify that a too big file should not be followed by a ko")
            .build();
  private static final String KEY_PREVIEW = "keyPreview";
  private static final Option PREVIEW_KEY_OPTION =
      Option.builder(KEY_PREVIEW).required(false).hasArg(true)
            .desc("Specify the key for Options to validate").build();
  private static final String STRING_PREVIEW = "stringPreview";
  private static final Option PREVIEW_STRING_OPTION =
      Option.builder(STRING_PREVIEW).required(false).hasArg(true)
            .desc("Specify the substring for key for Options to validate")
            .build();
  private static final String KEY_204 = "key204";
  private static final Option ICAP_204_KEY_OPTION =
      Option.builder(KEY_204).required(false).hasArg(true)
            .desc("Specify the key for 204 ICAP to validate").build();
  private static final String STRING_204 = "string204";
  private static final Option ICAP_204_STRING_OPTION =
      Option.builder(STRING_204).required(false).hasArg(true)
            .desc("Specify the substring for key for 204 ICAP to validate")
            .build();
  private static final String KEY_200 = "key200";
  private static final Option ICAP_200_KEY_OPTION =
      Option.builder(KEY_200).required(false).hasArg(true)
            .desc("Specify the key for 200 ICAP to validate").build();
  private static final String STRING_200 = "string200";
  private static final Option ICAP_200_STRING_OPTION =
      Option.builder(STRING_200).required(false).hasArg(true)
            .desc("Specify the substring for key for 200 ICAP to validate")
            .build();
  private static final String STRING_HTTP = "stringHttp";
  private static final Option HTTP_STRING_OPTION =
      Option.builder(STRING_HTTP).required(false).hasArg(true)
            .desc("Specify the substring for HTTP 200 ICAP status to validate")
            .build();
  private static final String LOGGER_ARG = "logger";
  private static final String DEBUG_LEVEL = "DEBUG";
  private static final String INFO_LEVEL = "INFO";
  private static final String WARN_LEVEL = "WARN";
  private static final String ERROR_LEVEL = "ERROR";
  private static final Option LOGGER_OPTION =
      Option.builder(LOGGER_ARG).required(false).hasArg(true).desc(
          "Specify the level of log between " + DEBUG_LEVEL + " | " +
          INFO_LEVEL + " | " + WARN_LEVEL + " | " + ERROR_LEVEL).build();

  private static final OptionGroup ERROR_OPTIONS =
      new OptionGroup().addOption(ERROR_DELETE_OPTION)
                       .addOption(ERROR_MOVE_OPTION)
                       .addOption(ERROR_SEND_OPTION);
  private static final OptionGroup SERVICE_OPTIONS =
      new OptionGroup().addOption(SERVICE_OPTION).addOption(MODEL_OPTION);
  private static final Options ICAP_OPTIONS =
      new Options().addOption(FILE_OPTION).addOption(HOST_OPTION)
                   .addOption(PORT_OPTION).addOptionGroup(SERVICE_OPTIONS)
                   .addOption(PREVIEW_OPTION).addOption(BLOCK_OPTION)
                   .addOption(RECEIVE_OPTION).addOption(MAX_SIZE_OPTION)
                   .addOption(TIMEOUT_OPTION)
                   .addOption(IGNORE_NETWORK_CONTINUE_OPTION)
                   .addOption(IGNORE_TOO_BIG_FILE_CONTINUE_OPTION)
                   .addOption(PREVIEW_KEY_OPTION)
                   .addOption(PREVIEW_STRING_OPTION)
                   .addOption(ICAP_200_KEY_OPTION)
                   .addOption(ICAP_200_STRING_OPTION)
                   .addOption(ICAP_204_KEY_OPTION)
                   .addOption(ICAP_204_STRING_OPTION).addOption(LOGGER_OPTION)
                   .addOption(HTTP_STRING_OPTION).addOptionGroup(ERROR_OPTIONS);
  private static final Options ICAP_MODEL_OPTIONS =
      new Options().addOption(PORT_OPTION).addOption(SERVICE_OPTION)
                   .addOption(PREVIEW_OPTION).addOption(BLOCK_OPTION)
                   .addOption(RECEIVE_OPTION).addOption(MAX_SIZE_OPTION)
                   .addOption(TIMEOUT_OPTION)
                   .addOption(IGNORE_NETWORK_CONTINUE_OPTION)
                   .addOption(IGNORE_TOO_BIG_FILE_CONTINUE_OPTION)
                   .addOption(PREVIEW_KEY_OPTION)
                   .addOption(PREVIEW_STRING_OPTION)
                   .addOption(ICAP_200_KEY_OPTION)
                   .addOption(ICAP_200_STRING_OPTION)
                   .addOption(ICAP_204_KEY_OPTION)
                   .addOption(ICAP_204_STRING_OPTION).addOption(LOGGER_OPTION)
                   .addOption(HTTP_STRING_OPTION).addOptionGroup(ERROR_OPTIONS);
  public static final String SEPARATOR_SEND = "--";

  // Standard configuration
  private String serverIP = null;
  private int port = IcapClient.DEFAULT_ICAP_PORT;
  private String icapService = null;
  private IcapModel icapModel = null;
  private String filepath = null;

  // Extra configuration
  private int receiveLength = IcapClient.STD_RECEIVE_LENGTH;
  private int sendLength = IcapClient.STD_SEND_LENGTH;
  private int timeout = IcapClient.DEFAULT_TIMEOUT;
  private String keyIcapPreview = null;
  private String subStringFromKeyIcapPreview = null;
  private String substringHttpStatus200 = null;
  private String keyIcap200 = null;
  private String subStringFromKeyIcap200 = null;
  private String keyIcap204 = null;
  private String subStringFromKeyIcap204 = null;
  private long maxSize = Integer.MAX_VALUE;
  private int stdPreviewSize = -1;
  private String pathMoveError = null;
  private boolean deleteOnError = false;
  private boolean sendOnError = false;
  private boolean ignoreNetworkError = false;
  private boolean ignoreTooBigFileError = false;
  private WaarpLogLevel logLevel = null;

  private Map<String, String> result = null;

  /**
   * Private constructor
   */
  private IcapScanFile() {
    // Empty
  }

  /**
   * Partial setter from source (not file, host, icapModel)
   *
   * @param from partial source
   */
  private IcapScanFile partialSetFrom(final IcapScanFile from) {
    this.port = from.port;
    this.icapService = from.icapService;
    this.receiveLength = from.receiveLength;
    this.sendLength = from.sendLength;
    this.timeout = from.timeout;
    this.keyIcapPreview = from.keyIcapPreview;
    this.subStringFromKeyIcapPreview = from.subStringFromKeyIcapPreview;
    this.substringHttpStatus200 = from.substringHttpStatus200;
    this.keyIcap200 = from.keyIcap200;
    this.subStringFromKeyIcap200 = from.subStringFromKeyIcap200;
    this.keyIcap204 = from.keyIcap204;
    this.subStringFromKeyIcap204 = from.subStringFromKeyIcap204;
    this.maxSize = from.maxSize;
    this.stdPreviewSize = from.stdPreviewSize;
    this.pathMoveError = from.pathMoveError;
    this.deleteOnError = from.deleteOnError;
    this.sendOnError = from.sendOnError;
    this.ignoreNetworkError = from.ignoreNetworkError;
    this.ignoreTooBigFileError = from.ignoreTooBigFileError;
    this.logLevel = from.getLogLevel();
    return this;
  }

  /**
   * Print to standard output the help of this command
   */
  public static void printHelp() {
    final HelpFormatter formatter = new HelpFormatter();
    formatter.printHelp("IcapScanFile", ICAP_OPTIONS);
  }

  /**
   * If file argument are -file EICARTEST, then a EICAR test file will be
   * sent.<br><br>
   * "-file path_to_file <br>
   * -to hostname <br>
   * [-port port, default 1344] <br>
   * -service name | -model name <br>
   * [-previewSize size, default none] <br>
   * [-blockSize size, default 8192] <br>
   * [-receiveSize size, default 65536] <br>
   * [-maxSize size, default MAX_INTEGER] <br>
   * [-timeout in_ms, default equiv to 10 min] <br>
   * [-errorMove path | -errorDelete | -sendOnError] <br>
   * [-ignoreNetworkError] <br>
   * [-ignoreTooBigFileError] <br>
   * [-keyPreview key -stringPreview string, default none] <br>
   * [-key204 key -string204 string, default none] <br>
   * [-key200 key -string200 string, default none] <br>
   * [-stringHttp string, default none] <br>
   * [-logger DEBUG|INFO|WARN|ERROR, default none]"<br>
   * <br>
   *
   * @param args must be already replaced values (getReplacedValue)
   *
   * @return the IcapScanFile
   *
   * @throws IcapException if an error occurs during argument parsing
   */
  public static IcapScanFile getIcapScanFileArgs(final String[] args)
      throws IcapException {
    if (args == null || args.length == 0) {
      throw new IllegalArgumentException(ARGUMENTS_CANNOT_BE_EMPTY_OR_NULL);
    }
    String[] realArgs = args;
    for (int i = 0; i < args.length; i++) {
      if (SEPARATOR_SEND.equals(args[i])) {
        realArgs = Arrays.copyOf(args, i);
        break;
      }
    }
    return getIcapScanFileArgs(realArgs, ICAP_OPTIONS);
  }

  /**
   * @param args must be already replaced values (getReplacedValue)
   * @param options the options matcher to use
   *
   * @return the IcapScanFile
   *
   * @throws IcapException if an error occurs during argument parsing
   */
  private static IcapScanFile getIcapScanFileArgs(final String[] args,
                                                  final Options options)
      throws IcapException {
    final IcapScanFile icapScanFile = new IcapScanFile();
    final CommandLineParser parser = new DefaultParser();
    try {
      final CommandLine cmd = parser.parse(options, args, true);

      if (options != ICAP_MODEL_OPTIONS && cmd.hasOption(MODEL)) {
        getModelParameters(icapScanFile, cmd);
      } else {
        icapScanFile.icapService = cmd.getOptionValue(SERVICE);
      }
      icapScanFile.filepath = cmd.getOptionValue(FILE);
      icapScanFile.serverIP = cmd.getOptionValue(TO);
      getPort(icapScanFile, cmd);
      getNumbers(icapScanFile, cmd);
      getOtherOptions(icapScanFile, cmd);
      if (cmd.hasOption(LOGGER_ARG)) {
        final String level =
            cmd.getOptionValue(LOGGER_ARG).trim().toUpperCase();
        if (DEBUG_LEVEL.equals(level)) {
          icapScanFile.logLevel = WaarpLogLevel.DEBUG;
        } else if (INFO_LEVEL.equals(level)) {
          icapScanFile.logLevel = WaarpLogLevel.INFO;
        } else if (WARN_LEVEL.equals(level)) {
          icapScanFile.logLevel = WaarpLogLevel.WARN;
        } else if (ERROR_LEVEL.equals(level)) {
          icapScanFile.logLevel = WaarpLogLevel.ERROR;
        } else {
          logger.warn("Unknown log level {}", level);
        }
      }
    } catch (final ParseException e) {
      throw new IcapException("Parsing error", e,
                              IcapError.ICAP_ARGUMENT_ERROR);
    }
    return icapScanFile;
  }

  /**
   * @param icapScanFile the original IcapScanFile
   * @param cmd the command to parse
   *
   * @throws IcapException if an error occurs during argument parsing
   */
  private static void getModelParameters(final IcapScanFile icapScanFile,
                                         final CommandLine cmd)
      throws IcapException {
    try {
      icapScanFile.icapModel = IcapModel.valueOf(cmd.getOptionValue(MODEL));
      final IcapScanFile modelIcapScanFile =
          getIcapScanFileArgs(icapScanFile.icapModel.getDefaultArgs(),
                              ICAP_MODEL_OPTIONS);
      icapScanFile.partialSetFrom(modelIcapScanFile);
    } catch (final IllegalArgumentException e) {
      throw new IcapException("Parsing error", e,
                              IcapError.ICAP_ARGUMENT_ERROR);
    }
  }

  /**
   * Get the other options from command
   *
   * @param icapScanFile the current IcapScanFile
   * @param cmd the command to parse from
   */
  private static void getOtherOptions(final IcapScanFile icapScanFile,
                                      final CommandLine cmd) {
    if (cmd.hasOption(ERROR_MOVE)) {
      icapScanFile.pathMoveError = cmd.getOptionValue(ERROR_MOVE);
    }
    if (cmd.hasOption(ERROR_DELETE)) {
      icapScanFile.deleteOnError = true;
    }
    if (cmd.hasOption(ERROR_SEND)) {
      icapScanFile.sendOnError = true;
    }
    if (cmd.hasOption(IGNORE_NETWORK_CONTINUE)) {
      icapScanFile.ignoreNetworkError = true;
    }
    if (cmd.hasOption(IGNORE_TOO_BIG_FILE_CONTINUE)) {
      icapScanFile.ignoreTooBigFileError = true;
    }
    if (cmd.hasOption(KEY_PREVIEW)) {
      icapScanFile.keyIcapPreview = cmd.getOptionValue(KEY_PREVIEW);
    }
    if (cmd.hasOption(STRING_PREVIEW)) {
      icapScanFile.subStringFromKeyIcapPreview =
          cmd.getOptionValue(STRING_PREVIEW);
    }
    if (cmd.hasOption(KEY_204)) {
      icapScanFile.keyIcap204 = cmd.getOptionValue(KEY_204);
    }
    if (cmd.hasOption(STRING_204)) {
      icapScanFile.subStringFromKeyIcap204 = cmd.getOptionValue(STRING_204);
    }
    if (cmd.hasOption(KEY_200)) {
      icapScanFile.keyIcap200 = cmd.getOptionValue(KEY_200);
    }
    if (cmd.hasOption(STRING_200)) {
      icapScanFile.subStringFromKeyIcap200 = cmd.getOptionValue(STRING_200);
    }
    if (cmd.hasOption(STRING_HTTP)) {
      icapScanFile.substringHttpStatus200 = cmd.getOptionValue(STRING_HTTP);
    }
  }

  /**
   * Check the numbers
   *
   * @param icapScanFile the IcapScanFile object
   * @param cmd the command line to check On
   *
   * @throws IcapException
   */
  private static void getNumbers(final IcapScanFile icapScanFile,
                                 final CommandLine cmd) throws IcapException {
    try {
      if (cmd.hasOption(PREVIEW_SIZE)) {
        icapScanFile.stdPreviewSize =
            Integer.parseInt(cmd.getOptionValue(PREVIEW_SIZE));
        if (icapScanFile.stdPreviewSize < 0) {
          throw new NumberFormatException("Preview size must be positive or 0");
        }
      }
      if (cmd.hasOption(BLOCK_SIZE)) {
        icapScanFile.sendLength =
            Integer.parseInt(cmd.getOptionValue(BLOCK_SIZE));
        if (icapScanFile.sendLength < IcapClient.MINIMAL_SIZE) {
          throw new NumberFormatException(
              "Block size must be greater than " + IcapClient.MINIMAL_SIZE);
        }
      }
      if (cmd.hasOption(RECEIVE_SIZE)) {
        icapScanFile.receiveLength =
            Integer.parseInt(cmd.getOptionValue(RECEIVE_SIZE));
        if (icapScanFile.receiveLength < IcapClient.MINIMAL_SIZE) {
          throw new NumberFormatException(
              "Receive size must be greater than " + IcapClient.MINIMAL_SIZE);
        }
      }
      if (cmd.hasOption(MAX_SIZE)) {
        icapScanFile.maxSize = Long.parseLong(cmd.getOptionValue(MAX_SIZE));
        if (icapScanFile.maxSize < IcapClient.MINIMAL_SIZE) {
          throw new NumberFormatException(
              "Max file size must be greater than " + IcapClient.MINIMAL_SIZE);
        }
      }
      if (cmd.hasOption(TIMEOUT_ARG)) {
        icapScanFile.timeout =
            Integer.parseInt(cmd.getOptionValue(TIMEOUT_ARG));
        if (icapScanFile.timeout < IcapClient.MINIMAL_SIZE) {
          throw new NumberFormatException(
              "Timeout must be greater than " + IcapClient.MINIMAL_SIZE);
        }
      }
    } catch (final NumberFormatException e) {
      throw new IcapException("Incorrect Number Format", e,
                              IcapError.ICAP_ARGUMENT_ERROR);
    }
  }

  /**
   * Get the port
   *
   * @param icapScanFile the IcapScanFile object
   * @param cmd the command line to check On
   *
   * @throws IcapException
   */
  private static void getPort(final IcapScanFile icapScanFile,
                              final CommandLine cmd) throws IcapException {
    try {
      if (cmd.hasOption(PORT_FIELD)) {
        icapScanFile.port = Integer.parseInt(cmd.getOptionValue(PORT_FIELD));
        if (icapScanFile.port < 0) {
          throw new NumberFormatException("Port must be positive");
        }
      }
    } catch (final NumberFormatException e) {
      throw new IcapException("Port incorrect", e,
                              IcapError.ICAP_ARGUMENT_ERROR);
    }
  }

  /**
   * Create the IcapClient according to IcapScanFile
   *
   * @param icapScanFile used to setup IcapClient
   *
   * @return the IcapClient
   */
  public static IcapClient getIcapClient(final IcapScanFile icapScanFile) {
    if (icapScanFile == null) {
      throw new IllegalArgumentException(ARGUMENTS_CANNOT_BE_EMPTY_OR_NULL);
    }
    final IcapClient icapClient =
        new IcapClient(icapScanFile.serverIP, icapScanFile.port,
                       icapScanFile.icapService, icapScanFile.stdPreviewSize);
    icapClient.setSendLength(icapScanFile.sendLength)
              .setReceiveLength(icapScanFile.receiveLength)
              .setMaxSize(icapScanFile.maxSize).setTimeout(icapScanFile.timeout)
              .setKeyIcapPreview(icapScanFile.keyIcapPreview)
              .setSubStringFromKeyIcapPreview(
                  icapScanFile.subStringFromKeyIcapPreview)
              .setKeyIcap204(icapScanFile.keyIcap204)
              .setSubStringFromKeyIcap204(icapScanFile.subStringFromKeyIcap204)
              .setKeyIcap200(icapScanFile.keyIcap200)
              .setSubStringFromKeyIcap200(icapScanFile.subStringFromKeyIcap200)
              .setSubstringHttpStatus200(icapScanFile.substringHttpStatus200);
    return icapClient;
  }

  /**
   * Finalize the current ICAP scan when an error occurs
   *
   * @param icapScanFile used to get options for Error tasks
   *
   * @throws IOException if an error occurs during post error tasks
   */
  public static void finalizeOnError(final IcapScanFile icapScanFile)
      throws IOException {
    if (icapScanFile == null) {
      throw new IllegalArgumentException(ARGUMENTS_CANNOT_BE_EMPTY_OR_NULL);
    }
    logger.error("Scan is incorrect: {}",
                 icapScanFile.getResult() == null? "No Result" :
                     icapScanFile.getResult());
    if (icapScanFile.deleteOnError) {
      final File file = new File(icapScanFile.filepath);
      if (!file.delete()) {
        logger.error("File cannot be deleted!");
        throw new IOException("File cannot be deleted!");
      } else {
        logger.warn("File is deleted");
      }
    } else if (icapScanFile.pathMoveError != null) {
      final File file = new File(icapScanFile.filepath);
      final File dir = new File(icapScanFile.pathMoveError);
      if (dir.exists()) {
        if (dir.isDirectory()) {
          try {
            final File to = new File(dir, file.getName());
            FileUtils.copy(file, to, true, false);
            logger.warn("File is moved to " + to.getAbsolutePath());
          } catch (final Reply550Exception e) {
            logger.error("Cannot move to directory: {}", e.getMessage());
            throw new IOException("Cannot move to directory", e);
          }
        } else {
          logger.error("Move path already exists and is not a directory");
          throw new IOException(
              "Move path already exists and is not a directory");
        }
      } else {
        if (dir.getParentFile().isDirectory()) {
          try {
            FileUtils.copy(file, dir, true, false);
            logger.warn("File is moved to " + dir.getAbsolutePath());
          } catch (final Reply550Exception e) {
            logger.error("Cannot move to file: {}", e.getMessage());
          }
        } else {
          logger.error("Move path is not a directory or existing sub-path");
          throw new IOException(
              "Move path is not a directory or existing sub-path");
        }
      }
    }
  }

  /**
   * @return the file path
   */
  public final String getFilePath() {
    return filepath;
  }

  /**
   * @param filePath the file path to use
   *
   * @return This
   */
  public final IcapScanFile setFilePath(final String filePath) {
    this.filepath = filePath;
    return this;
  }

  /**
   * @return the server IP
   */
  public final String getServerIP() {
    return serverIP;
  }

  /**
   * @param serverIP the server IP to use
   *
   * @return This
   */
  public final IcapScanFile setServerIP(final String serverIP) {
    this.serverIP = serverIP;
    return this;
  }

  /**
   * @return the Icap Model if any (null if none)
   */
  public final IcapModel getIcapModel() {
    return icapModel;
  }

  /**
   * @return the path to move in error or null
   */
  public final String getPathMoveError() {
    return pathMoveError;
  }

  /**
   * @return True if the file will be deleted in error
   */
  public final boolean isDeleteOnError() {
    return deleteOnError;
  }

  /**
   * @return True if the send on error option is set
   */
  public final boolean isSendOnError() {
    return sendOnError;
  }

  /**
   * @return True if a network error option is set to ignore such
   */
  public final boolean isIgnoreNetworkError() {
    return ignoreNetworkError;
  }

  /**
   * @return True if a too big file error option is set to ignore such
   */
  public final boolean isIgnoreTooBigFileError() {
    return ignoreTooBigFileError;
  }

  /**
   * @return the Logger Level desired during ICAP operation or null if none
   */
  public final WaarpLogLevel getLogLevel() {
    return logLevel;
  }

  /**
   * @return the Map of key from ICAP if any (null if none)
   */
  public final Map<String, String> getResult() {
    return result;
  }

  /**
   * If file argument are -file EICARTEST, then a EICAR test file will be
   * sent.<br><br>
   * "-file path_to_file <br>
   * -to hostname <br>
   * [-port port, default 1344] <br>
   * -service name | -model name <br>
   * [-previewSize size, default none] <br>
   * [-blockSize size, default 8192] <br>
   * [-receiveSize size, default 65536] <br>
   * [-maxSize size, default MAX_INTEGER] <br>
   * [-timeout in_ms, default equiv to 10 min] <br>
   * [-errorMove path | -errorDelete | -sendOnError] <br>
   * [-ignoreNetworkError] <br>
   * [-ignoreTooBigFileError] <br>
   * [-keyPreview key -stringPreview string, default none] <br>
   * [-key204 key -string204 string, default none] <br>
   * [-key200 key -string200 string, default none] <br>
   * [-stringHttp string, default none] <br>
   * [-logger DEBUG|INFO|WARN|ERROR, default none]"<br>
   * <br>
   * <br>
   * Exit with values:<br>
   * <ul>
   *   <li>0: Scan OK</li>
   *   <li>1: Bad arguments</li>
   *   <li>2: ICAP protocol error</li>
   *   <li>3: Network error</li>
   *   <li>4: Scan KO</li>
   *   <li>5: Scan KO but post action required in error</li>
   * </ul>
   *
   * @param args to get parameters from
   */
  public static void main(final String[] args) {
    WaarpLoggerFactory.setDefaultFactoryIfNotSame(
        new WaarpSlf4JLoggerFactory(null));
    System.exit(scanFile(args));
  }

  /**
   * If file argument are -file EICARTEST, then a EICAR test file will be
   * sent.<br><br>
   * "-file path_to_file <br>
   * -to hostname <br>
   * [-port port, default 1344] <br>
   * Not -service name | -model name (should be set through Model)<br>
   * [-previewSize size, default none] <br>
   * [-blockSize size, default 8192] <br>
   * [-receiveSize size, default 65536] <br>
   * [-maxSize size, default MAX_INTEGER] <br>
   * [-timeout in_ms, default equiv to 10 min] <br>
   * [-errorMove path | -errorDelete | -sendOnError] <br>
   * [-ignoreNetworkError] <br>
   * [-ignoreTooBigFileError] <br>
   * [-keyPreview key -stringPreview string, default none] <br>
   * [-key204 key -string204 string, default none] <br>
   * [-key200 key -string200 string, default none] <br>
   * [-stringHttp string, default none] <br>
   * [-logger DEBUG|INFO|WARN|ERROR, default none]"<br>
   * <br>
   *
   * @param model default model to apply in addition to parameters
   * @param args to get parameters from
   *
   * @return 0 if OK, 1 if arguments are incorrect, 2 if an issue occurs
   *     during ICAP protool, 3 if a nework error occurs, 4 if scan KO, 5 if
   *     the post actions are in error while scan is KO
   */
  public static int scanFile(final String[] model, final String[] args) {
    if (model == null || model.length == 0 || args == null ||
        args.length == 0) {
      throw new IllegalArgumentException(ARGUMENTS_CANNOT_BE_EMPTY_OR_NULL);
    }
    final String[] realArgs = Arrays.copyOf(model, model.length + args.length);
    System.arraycopy(args, 0, realArgs, model.length, args.length);
    return scanFile(realArgs);
  }

  /**
   * If file argument are -file EICARTEST, then a EICAR test file will be
   * sent.<br><br>
   * "-file path_to_file <br>
   * -to hostname <br>
   * [-port port, default 1344] <br>
   * -service name | -model name
   * [-previewSize size, default none] <br>
   * [-blockSize size, default 8192] <br>
   * [-receiveSize size, default 65536] <br>
   * [-maxSize size, default MAX_INTEGER] <br>
   * [-timeout in_ms, default equiv to 10 min] <br>
   * [-errorMove path | -errorDelete | -sendOnError] <br>
   * [-ignoreNetworkError] <br>
   * [-ignoreTooBigFileError] <br>
   * [-keyPreview key -stringPreview string, default none] <br>
   * [-key204 key -string204 string, default none] <br>
   * [-key200 key -string200 string, default none] <br>
   * [-stringHttp string, default none] <br>
   * [-logger DEBUG|INFO|WARN|ERROR, default none]"<br>
   * <br>
   *
   * @param args to get parameters from
   *
   * @return 0 if OK, 1 if arguments are incorrect, 2 if an issue occurs
   *     during ICAP protool, 3 if a nework error occurs, 4 if scan KO, 5 if
   *     the post actions are in error while scan is KO
   */
  public static int scanFile(final String[] args) {
    if (args == null || args.length == 0) {
      throw new IllegalArgumentException(ARGUMENTS_CANNOT_BE_EMPTY_OR_NULL);
    }
    WaarpLoggerFactory.setDefaultFactoryIfNotSame(
        new WaarpSlf4JLoggerFactory(null));
    final IcapScanFile icapScanFile;
    try {
      icapScanFile = getIcapScanFileArgs(args);
    } catch (final IcapException e) {
      printHelp();
      logger.error("Arguments are incorrect: {}", e.getMessage());
      return STATUS_BAD_ARGUMENT;
    }
    try {
      return icapScanFile.scanFile();
    } catch (final IcapException e) {
      logger.error("Error during scan: {}", e.getMessage());
      if (e.getError() == IcapError.ICAP_CANT_CONNECT ||
          e.getError() == IcapError.ICAP_NETWORK_ERROR ||
          e.getError() == IcapError.ICAP_TIMEOUT_ERROR) {
        if (icapScanFile.ignoreNetworkError) {
          return STATUS_OK;
        }
        return STATUS_NETWORK_ISSUE;
      }
      if (e.getError() == IcapError.ICAP_ARGUMENT_ERROR ||
          e.getError() == IcapError.ICAP_FILE_LENGTH_ERROR) {
        if (icapScanFile.ignoreTooBigFileError &&
            e.getError() == IcapError.ICAP_FILE_LENGTH_ERROR) {
          return STATUS_OK;
        }
        return STATUS_BAD_ARGUMENT;
      }
      return STATUS_ICAP_ISSUE;
    } catch (final IOException e) {
      logger.error("Moving file is in error: {}", e.getMessage());
      return STATUS_KO_SCAN_POST_ACTION_ERROR;
    }
  }

  /**
   * Scan a file through ICAP antivirus server from IcapScanFile
   *
   * @return 0 if scan is OK, 1 if the file is infected
   *
   * @throws IcapException if an error occurs during connection or ICAP protocol
   * @throws IOException if the file is infected and a post error action is
   *     in error
   */
  public final int scanFile() throws IcapException, IOException {
    final WaarpLogLevel waarpLogLevel = getLogLevel();
    WaarpLogLevel oldLevel = null;
    try {
      if (waarpLogLevel != null) {
        if (logger.isTraceEnabled()) {
          oldLevel = WaarpLogLevel.TRACE;
        } else if (logger.isDebugEnabled()) {
          oldLevel = WaarpLogLevel.DEBUG;
        } else if (logger.isInfoEnabled()) {
          oldLevel = WaarpLogLevel.INFO;
        } else if (logger.isWarnEnabled()) {
          oldLevel = WaarpLogLevel.WARN;
        } else if (logger.isErrorEnabled()) {
          oldLevel = WaarpLogLevel.ERROR;
        }
        WaarpLoggerFactory.setLogLevel(waarpLogLevel);
      }
      final IcapClient icapClient = getIcapClient(this);
      if (icapClient.scanFile(filepath)) {
        icapClient.close();
        logger.info("File is OK");
        return STATUS_OK;
      } else {
        icapClient.close();
        result = icapClient.getFinalResult();
        finalizeOnError(this);
        return STATUS_KO_SCAN;
      }
    } finally {
      if (waarpLogLevel != null && oldLevel != null) {
        WaarpLoggerFactory.setLogLevel(oldLevel);
      }
    }
  }
}