/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2013-2016 ForgeRock AS
 */
package org.opends.server.tools.upgrade;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.NavigableMap;
import java.util.TreeMap;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.server.core.LockFileManager;
import org.opends.server.util.BuildVersion;

import com.forgerock.opendj.cli.ClientException;
import com.forgerock.opendj.cli.ReturnCode;

import static com.forgerock.opendj.cli.Utils.*;
import static javax.security.auth.callback.ConfirmationCallback.*;
import static javax.security.auth.callback.TextOutputCallback.WARNING;

import static org.opends.messages.ToolMessages.*;
import static org.opends.server.tools.upgrade.FormattedNotificationCallback.*;
import static org.opends.server.tools.upgrade.LicenseFile.*;
import static org.opends.server.tools.upgrade.UpgradeTasks.*;
import static org.opends.server.tools.upgrade.UpgradeUtils.batDirectory;
import static org.opends.server.tools.upgrade.UpgradeUtils.binDirectory;
import static org.opends.server.tools.upgrade.UpgradeUtils.libDirectory;
import static org.opends.server.util.StaticUtils.*;

/**
 * This class contains the table of upgrade tasks that need performing when
 * upgrading from one version to another.
 */
public final class Upgrade
{
  /** Upgrade's logger. */
  private static LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /** Upgrade supports version from 2.4.5. */
  private static BuildVersion UPGRADESUPPORTSVERSIONFROM = BuildVersion.valueOf("2.4.5.0000");

  /** The success exit code value. */
  static final int EXIT_CODE_SUCCESS = 0;
  /** The error exit code value. */
  static final int EXIT_CODE_ERROR = 1;

  /**
   * The exit code value that will be used if upgrade requires manual
   * intervention.
   */
  static final int EXIT_CODE_MANUAL_INTERVENTION = 2;

  /** If the upgrade contains some post upgrade tasks to do. */
  static boolean hasPostUpgradeTask;

  /** Developers should register upgrade tasks below. */
  private static final NavigableMap<BuildVersion, List<UpgradeTask>> TASKS = new TreeMap<>();
  private static final List<UpgradeTask> MANDATORY_TASKS = new LinkedList<>();

  static
  {
    // @formatter:off
    register("2.5.0",
        modifyConfigEntry(INFO_UPGRADE_TASK_6869_SUMMARY.get(),
        "(objectClass=ds-cfg-collation-matching-rule)",
        "add: ds-cfg-collation",
        "ds-cfg-collation: de:1.3.6.1.4.1.42.2.27.9.4.28.1",
        "ds-cfg-collation: de-DE:1.3.6.1.4.1.42.2.27.9.4.28.1",
        "-",
        "delete: ds-cfg-collation",
        "ds-cfg-collation: de:1.3.6.1.4.1.142.2.27.9.4.28.1",
        "ds-cfg-collation: de-DE:1.3.6.1.4.1.142.2.27.9.4.28.1"));

    register("2.5.0",
        modifyConfigEntry(INFO_UPGRADE_TASK_7192_SUMMARY.get(),
        "(objectClass=ds-cfg-password-policy)",
        "add: objectClass",
        "objectClass: ds-cfg-authentication-policy",
        "-",
        "add: ds-cfg-java-class",
        "ds-cfg-java-class: org.opends.server.core.PasswordPolicyFactory"));

    register("2.5.0",
        modifyConfigEntry(INFO_UPGRADE_TASK_7364_SUMMARY.get(),
        "(ds-cfg-java-class=org.opends.server.loggers.TextAuditLogPublisher)",
        "add: objectClass",
        "objectClass: ds-cfg-file-based-audit-log-publisher",
        "-",
        "delete: objectClass",
        "objectClass: ds-cfg-file-based-access-log-publisher"));

    register("2.5.0",
        renameSnmpSecurityConfig(INFO_UPGRADE_TASK_7466_SUMMARY.get()));

    register("2.5.0",
        newAttributeTypes(INFO_UPGRADE_TASK_7748_1_SUMMARY.get(),
        "00-core.ldif", "1.3.6.1.4.1.36733.2.1.1.59"), //etag
        addConfigEntry(INFO_UPGRADE_TASK_7748_2_SUMMARY.get(),
        "dn: cn=etag,cn=Virtual Attributes,cn=config",
        "changetype: add",
        "objectClass: top",
        "objectClass: ds-cfg-virtual-attribute",
        "objectClass: ds-cfg-entity-tag-virtual-attribute",
        "cn: etag",
        "ds-cfg-java-class: org.opends.server.extensions."
            + "EntityTagVirtualAttributeProvider",
        "ds-cfg-enabled: true",
        "ds-cfg-attribute-type: etag",
        "ds-cfg-conflict-behavior: real-overrides-virtual",
        "ds-cfg-checksum-algorithm: adler-32",
        "ds-cfg-excluded-attribute: ds-sync-hist"));

    register("2.5.0",
        addConfigEntry(INFO_UPGRADE_TASK_7834_SUMMARY.get(),
        "dn: cn=Password Expiration Time,cn=Virtual Attributes,cn=config",
        "changetype: add",
        "objectClass: top",
        "objectClass: ds-cfg-virtual-attribute",
        "objectClass: ds-cfg-password-expiration-time-virtual-attribute",
        "cn: Password Expiration Time",
        "ds-cfg-java-class: org.opends.server.extensions."
            + "PasswordExpirationTimeVirtualAttributeProvider",
        "ds-cfg-enabled: true",
        "ds-cfg-attribute-type: ds-pwp-password-expiration-time",
        "ds-cfg-conflict-behavior: virtual-overrides-real"));

    register("2.5.0",
        modifyConfigEntry(INFO_UPGRADE_TASK_7979_SUMMARY.get(),
        "(ds-cfg-java-class=org.opends.server.schema.CertificateSyntax)",
        "add: objectClass",
        "objectClass: ds-cfg-certificate-attribute-syntax",
        "-",
        "add: ds-cfg-strict-format",
        "ds-cfg-strict-format: false"));

    register("2.6.0",
        modifyConfigEntry(INFO_UPGRADE_TASK_8124_SUMMARY.get(),
        "(ds-cfg-java-class=org.opends.server.schema.JPEGSyntax)",
        "add: objectClass",
        "objectClass: ds-cfg-jpeg-attribute-syntax",
        "-",
        "add: ds-cfg-strict-format",
        "ds-cfg-strict-format: false"));

    register("2.6.0",
        modifyConfigEntry(INFO_UPGRADE_TASK_8133_SUMMARY.get(),
        "(ds-cfg-java-class=org.opends.server.schema.CountryStringSyntax)",
        "add: objectClass",
        "objectClass: ds-cfg-country-string-attribute-syntax",
        "-",
        "add: ds-cfg-strict-format",
        "ds-cfg-strict-format: false"));

    register("2.6.0",
        requireConfirmation(INFO_UPGRADE_TASK_8214_DESCRIPTION.get(), YES,
            modifyConfigEntry(INFO_UPGRADE_TASK_8214_SUMMARY.get(),
                "(ds-cfg-java-class=org.opends.server.extensions.IsMemberOfVirtualAttributeProvider)",
                "add: ds-cfg-filter",
                "ds-cfg-filter: (|(objectClass=person)(objectClass=groupOfNames)"
                    + "(objectClass=groupOfUniqueNames)(objectClass=groupOfEntries))",
                "-",
                "delete: ds-cfg-filter",
                "ds-cfg-filter: (objectClass=person)")));

    register("2.6.0",
        modifyConfigEntry(INFO_UPGRADE_TASK_8387_SUMMARY.get(),
        "(objectClass=ds-cfg-dictionary-password-validator)",
        "add: ds-cfg-check-substrings",
        "ds-cfg-check-substrings: false"));

    register("2.6.0",
        modifyConfigEntry(INFO_UPGRADE_TASK_8389_SUMMARY.get(),
        "(objectClass=ds-cfg-attribute-value-password-validator)",
        "add: ds-cfg-check-substrings",
        "ds-cfg-check-substrings: false"));

    register("2.6.0",
        addConfigEntry(INFO_UPGRADE_TASK_8487_SUMMARY.get(),
        "dn: cn=PBKDF2,cn=Password Storage Schemes,cn=config",
        "changetype: add",
        "objectClass: top",
        "objectClass: ds-cfg-password-storage-scheme",
        "objectClass: ds-cfg-pbkdf2-password-storage-scheme",
        "cn: PBKDF2",
        "ds-cfg-java-class: org.opends.server.extensions."
            + "PBKDF2PasswordStorageScheme",
        "ds-cfg-enabled: true"));

    register("2.6.0",
        addConfigFile("http-config.json"),
        addConfigEntry(INFO_UPGRADE_TASK_8613_SUMMARY.get(),
        "dn: cn=HTTP Connection Handler,cn=Connection Handlers,cn=config",
        "changetype: add",
        "objectClass: ds-cfg-http-connection-handler",
        "objectClass: ds-cfg-connection-handler",
        "objectClass: top",
        "ds-cfg-listen-port: 8080",
        "cn: HTTP Connection Handler",
        "ds-cfg-max-blocked-write-time-limit: 2 minutes",
        "ds-cfg-ssl-client-auth-policy: optional",
        "ds-cfg-use-tcp-keep-alive: true",
        "ds-cfg-max-request-size: 5 megabytes",
        "ds-cfg-use-tcp-no-delay: true",
        "ds-cfg-allow-tcp-reuse-address: true",
        "ds-cfg-accept-backlog: 128",
        "ds-cfg-authentication-required: true",
        "ds-cfg-buffer-size: 4096 bytes",
        "ds-cfg-config-file: config/http-config.json",
        "ds-cfg-listen-address: 0.0.0.0",
        "ds-cfg-java-class: " +
          "org.opends.server.protocols.http.HTTPConnectionHandler",
        "ds-cfg-keep-stats: true",
        "ds-cfg-ssl-cert-nickname: server-cert",
        "ds-cfg-use-ssl: false",
        "ds-cfg-enabled: false"));

    register("2.6.0",
        addConfigEntry(INFO_UPGRADE_TASK_8832_SUMMARY.get(),
        "dn: cn=File-Based HTTP Access Logger,cn=Loggers,cn=config",
        "changetype: add",
        "objectClass: ds-cfg-file-based-http-access-log-publisher",
        "objectClass: top",
        "objectClass: ds-cfg-http-access-log-publisher",
        "objectClass: ds-cfg-log-publisher",
        "cn: File-Based HTTP Access Logger",
        "ds-cfg-java-class: " +
          "org.opends.server.loggers.TextHTTPAccessLogPublisher",
        "ds-cfg-asynchronous: true",
        "ds-cfg-log-file: logs/http-access",
        "ds-cfg-rotation-policy: " +
          "cn=24 Hours Time Limit Rotation Policy," +
          "cn=Log Rotation Policies,cn=config",
        "ds-cfg-rotation-policy: " +
          "cn=Size Limit Rotation Policy,cn=Log Rotation Policies,cn=config",
        "ds-cfg-retention-policy: " +
          "cn=File Count Retention Policy,cn=Log Retention Policies,cn=config",
        "ds-cfg-log-file-permissions: 640",
        "ds-cfg-enabled: false"));

    register("2.6.0",
        newAttributeTypes(INFO_UPGRADE_TASK_8985_1_SUMMARY.get(),
        "00-core.ldif", "1.2.840.113549.1.9.1"), // emailAddress
        modifyConfigEntry(INFO_UPGRADE_TASK_8985_2_SUMMARY.get(),
        "&(ds-cfg-java-class=org.opends.server.extensions." +
        "SubjectAttributeToUserAttributeCertificateMapper)" +
        "(ds-cfg-subject-attribute-mapping=e:mail)",
        "delete:ds-cfg-subject-attribute-mapping",
        "ds-cfg-subject-attribute-mapping: e:mail",
        "-",
        "add:ds-cfg-subject-attribute-mapping",
        "ds-cfg-subject-attribute-mapping: emailAddress:mail"));

    /** See OPENDJ-992 */
    register("2.6.0",
        regressionInVersion("2.5.0",
            rebuildSingleIndex(INFO_UPGRADE_TASK_9013_DESCRIPTION.get(),
                "ds-sync-hist")));

    /** See OPENDJ-1284 */
    register("2.8.0", // userCertificate OID / cACertificate OID
        newAttributeTypes(INFO_UPGRADE_TASK_10133_1_SUMMARY.get(),
        "00-core.ldif", "2.5.4.36", "2.5.4.37"),
        addConfigEntry(INFO_UPGRADE_TASK_10133_2_SUMMARY.get(),
        "dn: cn=Certificate Exact Matching Rule,cn=Matching Rules,cn=config",
        "changetype: add",
        "objectClass: top",
        "objectClass: ds-cfg-matching-rule",
        "objectClass: ds-cfg-equality-matching-rule",
        "cn: Certificate Exact Matching Rule",
        "ds-cfg-java-class: "
            + "org.opends.server.schema.CertificateExactMatchingRuleFactory",
        "ds-cfg-enabled: true"));


    /** See OPENDJ-1295 */
    register("2.8.0",
        copySchemaFile("03-pwpolicyextension.ldif"));

    /** See OPENDJ-1490 and OPENDJ-1454 */
    register("2.8.0",
        deleteConfigEntry(INFO_UPGRADE_TASK_10733_1_SUMMARY.get(),
        "dn: ds-cfg-backend-id=replicationChanges,cn=Backends,cn=config"),
        modifyConfigEntry(INFO_UPGRADE_TASK_10733_2_SUMMARY.get(),
        "(objectClass=ds-cfg-dsee-compat-access-control-handler)",
        "delete: ds-cfg-global-aci",
        "ds-cfg-global-aci: "
            + "(target=\"ldap:///dc=replicationchanges\")"
            + "(targetattr=\"*\")"
            + "(version 3.0; acl \"Replication backend access\"; "
            + "deny (all) userdn=\"ldap:///anyone\";)"));

    /** See OPENDJ-1351 */
    register("2.8.0",
        modifyConfigEntry(INFO_UPGRADE_TASK_10820_SUMMARY.get(),
        "(objectClass=ds-cfg-root-dn)",
        "add: ds-cfg-default-root-privilege-name",
        "ds-cfg-default-root-privilege-name: changelog-read"));

    /** See OPENDJ-1580 */
    register("2.8.0",
        addConfigEntry(INFO_UPGRADE_TASK_10908_SUMMARY.get(),
            "dn: cn=PKCS5S2,cn=Password Storage Schemes,cn=config",
            "changetype: add",
            "objectClass: top",
            "objectClass: ds-cfg-password-storage-scheme",
            "objectClass: ds-cfg-pkcs5s2-password-storage-scheme",
            "cn: PKCS5S2",
            "ds-cfg-java-class: org.opends.server.extensions.PKCS5S2PasswordStorageScheme",
            "ds-cfg-enabled: true"));

    /** See OPENDJ-1322 and OPENDJ-1067 */
    register("2.8.0",
        rerunJavaPropertiesTool(INFO_UPGRADE_TASK_9206_SUMMARY.get()));

    register("2.8.0",
        modifyConfigEntry(INFO_UPGRADE_TASK_10214_SUMMARY.get(),
          "(ds-cfg-java-class=org.opends.server.loggers.debug.TextDebugLogPublisher)",
          "delete:ds-cfg-java-class",
          "-",
          "add:ds-cfg-java-class",
          "ds-cfg-java-class: org.opends.server.loggers.TextDebugLogPublisher"));

    register("2.8.0",
        modifyConfigEntry(INFO_UPGRADE_TASK_10232_SUMMARY.get(),
          "(objectclass=ds-cfg-file-based-debug-log-publisher)",
          "delete:ds-cfg-default-debug-level"));

    register("2.8.0",
        modifyConfigEntry(INFO_UPGRADE_TASK_10329_SUMMARY.get(),
            "&(objectclass=ds-cfg-file-based-error-log-publisher)(cn=File-Based Error Logger)",
            "delete:ds-cfg-default-severity",
            "ds-cfg-default-severity: severe-warning",
            "ds-cfg-default-severity: severe-error",
            "ds-cfg-default-severity: fatal-error",
            "-",
            "add:ds-cfg-default-severity",
            "ds-cfg-default-severity: error",
            "ds-cfg-default-severity: warning"
            ));

    register("2.8.0",
        modifyConfigEntry(INFO_UPGRADE_TASK_10339_SUMMARY.get(),
            "&(objectclass=ds-cfg-file-based-error-log-publisher)(cn=Replication Repair Logger)",
            "delete:ds-cfg-override-severity",
             "-",
             "add:ds-cfg-override-severity",
             "ds-cfg-override-severity: SYNC=INFO,ERROR,WARNING,NOTICE"));

    /** See OPENDJ-1545 */
    register("2.8.0",
        deleteConfigEntry(INFO_UPGRADE_TASK_11237_1_SUMMARY.get(),
            "dn: cn=Network Groups,cn=config"),
        deleteConfigEntry(INFO_UPGRADE_TASK_11237_2_SUMMARY.get(),
            "dn: cn=Workflows,cn=config"),
        deleteConfigEntry(INFO_UPGRADE_TASK_11237_3_SUMMARY.get(),
            "dn: cn=Workflow Elements,cn=config"));
    register("2.8.0",
        deleteConfigEntry(INFO_UPGRADE_TASK_11239_SUMMARY.get(),
            "dn: cn=Network Group,cn=Plugins,cn=config"));
    register("2.8.0",
        deleteConfigEntry(INFO_UPGRADE_TASK_11339_SUMMARY.get(),
            "dn: cn=Extensions,cn=config"));

    /** See OPENDJ-1701 */
    register("2.8.0",
        deleteConfigEntry(INFO_UPGRADE_TASK_11476_SUMMARY.get(),
            "dn: cn=File System,cn=Entry Caches,cn=config"));

    /** See OPENDJ-1869 */
    register("2.8.0",
        modifyConfigEntry(INFO_UPGRADE_TASK_12226_SUMMARY.get(),
            "(objectclass=ds-cfg-root-config)",
            "delete: ds-cfg-entry-cache-preload"));

    /** See OPENDJ-2054 */
    register("2.8.0",
        deleteFile(new File(binDirectory, "dsframework")),
        deleteFile(new File(batDirectory, "dsframework.bat")));

    /** If the upgraded version is a non OEM one, migrates local-db backends to JE Backend, see OPENDJ-2364 **/
    register("3.0.0",
        conditionalUpgradeTasks(
          new UpgradeCondition() {
              @Override
              public boolean shouldPerformUpgradeTasks(UpgradeContext context) throws ClientException {
                return !isOEMVersion();
              }

              @Override
              public String toString() {
                return "!isOEMVersion";
              }
          },
          migrateLocalDBBackendsToJEBackends(),
          modifyConfigEntry(INFO_UPGRADE_TASK_MIGRATE_JE_SUMMARY_2.get(),
              "(objectClass=ds-cfg-local-db-backend)",
              "replace: objectClass",
              "objectClass: top",
              "objectClass: ds-cfg-backend",
              "objectClass: ds-cfg-pluggable-backend",
              "objectClass: ds-cfg-je-backend",
              "-",
              "replace: ds-cfg-java-class",
              "ds-cfg-java-class: org.opends.server.backends.jeb.JEBackend",
              "-",
              "delete: ds-cfg-import-thread-count",
              "-",
              "delete: ds-cfg-import-queue-size",
              "-",
              "delete: ds-cfg-subordinate-indexes-enabled",
              "-"
          ),
          modifyConfigEntry(INFO_UPGRADE_TASK_MIGRATE_JE_SUMMARY_3.get(),
              "(objectClass=ds-cfg-local-db-index)",
              "replace: objectClass",
              "objectClass: top",
              "objectClass: ds-cfg-backend-index",
              "-"
          ),
          modifyConfigEntry(INFO_UPGRADE_TASK_MIGRATE_JE_SUMMARY_4.get(),
              "(objectClass=ds-cfg-local-db-vlv-index)",
              "replace: objectClass",
              "objectClass: top",
              "objectClass: ds-cfg-backend-vlv-index",
              "-",
              "delete: ds-cfg-max-block-size",
              "-"
          )
        )
    );

    /** If the upgraded version is OEM, migrates local-db backends to PDB, see OPENDJ-2364 **/
    register("3.0.0",
      conditionalUpgradeTasks(
        new UpgradeCondition() {
          @Override
          public boolean shouldPerformUpgradeTasks(UpgradeContext context) throws ClientException {
            return isOEMVersion();
          }

          @Override
          public String toString() {
            return "isOEMVersion";
          }
        },
        deleteFile(new File(libDirectory, "je.jar")),
        requireConfirmation(INFO_UPGRADE_TASK_LOCAL_DB_TO_PDB_1_SUMMARY.get(), NO,
                renameLocalDBBackendDirectories(),
                // Convert JE backends to PDB backends.
                modifyConfigEntry(INFO_UPGRADE_TASK_LOCAL_DB_TO_PDB_2_SUMMARY.get(),
                        "(objectclass=ds-cfg-local-db-backend)",
                        "delete: objectclass",
                        "objectclass: ds-cfg-local-db-backend",
                        "-",
                        "add: objectclass",
                        "objectclass: ds-cfg-pluggable-backend",
                        "objectclass: ds-cfg-pdb-backend",
                        "-",
                        "replace: ds-cfg-java-class",
                        "ds-cfg-java-class: org.opends.server.backends.pdb.PDBBackend",
                        "-",
                        "delete: ds-cfg-preload-time-limit",
                        "-",
                        "delete: ds-cfg-import-thread-count",
                        "-",
                        "delete: ds-cfg-import-queue-size",
                        "-",
                        "delete: ds-cfg-db-txn-write-no-sync",
                        "-",
                        "delete: ds-cfg-db-run-cleaner",
                        "-",
                        "delete: ds-cfg-db-cleaner-min-utilization",
                        "-",
                        "delete: ds-cfg-db-evictor-lru-only",
                        "-",
                        "delete: ds-cfg-db-evictor-core-threads",
                        "-",
                        "delete: ds-cfg-db-evictor-max-threads",
                        "-",
                        "delete: ds-cfg-db-evictor-keep-alive",
                        "-",
                        "delete: ds-cfg-db-evictor-nodes-per-scan",
                        "-",
                        "delete: ds-cfg-db-log-file-max",
                        "-",
                        "delete: ds-cfg-db-log-filecache-size",
                        "-",
                        "delete: ds-cfg-db-logging-file-handler-on",
                        "-",
                        "delete: ds-cfg-db-logging-level",
                        "-",
                        "delete: ds-cfg-db-checkpointer-bytes-interval",
                        "-",
                        "delete: ds-cfg-db-checkpointer-wakeup-interval",
                        "-",
                        "delete: ds-cfg-db-num-lock-tables",
                        "-",
                        "delete: ds-cfg-db-num-cleaner-threads",
                        "-",
                        "delete: ds-cfg-je-property",
                        "-",
                        "delete: ds-cfg-subordinate-indexes-enabled",
                        "-"
                ),
                // Convert JE backend indexes to PDB backend indexes.
                modifyConfigEntry(INFO_UPGRADE_TASK_LOCAL_DB_TO_PDB_3_SUMMARY.get(),
                        "(objectclass=ds-cfg-local-db-index)",
                        "delete: objectclass",
                        "objectclass: ds-cfg-local-db-index",
                        "-",
                        "add: objectclass",
                        "objectclass: ds-cfg-backend-index",
                        "-"
                ),
                // Convert JE backend VLV indexes to PDB backend VLV indexes.
                modifyConfigEntry(INFO_UPGRADE_TASK_LOCAL_DB_TO_PDB_4_SUMMARY.get(),
                        "(objectclass=ds-cfg-local-db-vlv-index)",
                        "delete: objectclass",
                        "objectclass: ds-cfg-local-db-vlv-index",
                        "-",
                        "add: objectclass",
                        "objectclass: ds-cfg-backend-vlv-index",
                        "-",
                        "delete: ds-cfg-max-block-size",
                        "-"
                )
        )
      )
    );

    /** Remove dbtest tool (replaced by backendstat in 3.0.0) - see OPENDJ-1791 **/
    register("3.0.0",
            deleteFile(new File(binDirectory, "dbtest")),
            deleteFile(new File(batDirectory, "dbtest.bat")));

    /**
     * Rebuild all indexes when upgrading to 3.0.0.
     *
     * 1) matching rules have changed in 2.8.0 and again in 3.0.0- see OPENDJ-1637
     * 2) JE backend has been migrated to pluggable architecture.
     */
    register("3.0.0",
            rebuildAllIndexes(INFO_UPGRADE_TASK_11260_SUMMARY.get()));

    /** See OPENDJ-1742 */
    register("3.0.0",
        clearReplicationDbDirectory());

    /**
     * All upgrades will refresh the server configuration schema and generate a new upgrade folder.
     */
    registerLast(
        copySchemaFile("02-config.ldif"),
        updateConfigUpgradeFolder(),
        postUpgradeRebuildIndexes());

    // @formatter:on
  }

  /**
   * Returns a list containing all the tasks which are required in order to upgrade
   * from {@code fromVersion} to {@code toVersion}.
   *
   * @param fromVersion
   *          The old version.
   * @param toVersion
   *          The new version.
   * @return A list containing all the tasks which are required in order to upgrade
   *         from {@code fromVersion} to {@code toVersion}.
   */
  private static List<UpgradeTask> getUpgradeTasks(final BuildVersion fromVersion, final BuildVersion toVersion)
  {
    final List<UpgradeTask> tasks = new LinkedList<>();
    for (final List<UpgradeTask> subList : TASKS.subMap(fromVersion, false,
        toVersion, true).values())
    {
      tasks.addAll(subList);
    }
    tasks.addAll(MANDATORY_TASKS);
    return tasks;
  }

  /**
   * Upgrades the server from {@code fromVersion} to {@code toVersion} located in the upgrade context.
   *
   * @param context
   *          The context of the upgrade.
   * @throws ClientException
   *           If an error occurred while performing the upgrade.
   */
  public static void upgrade(final UpgradeContext context)
      throws ClientException
  {
    // Checks and validates the version number.
    isVersionCanBeUpdated(context);

    // Server must be offline.
    checkIfServerIsRunning(context);

    context.notify(INFO_UPGRADE_TITLE.get(), TITLE_CALLBACK);
    context.notify(INFO_UPGRADE_SUMMARY.get(context.getFromVersion(), context.getToVersion()), NOTICE_CALLBACK);
    context.notify(INFO_UPGRADE_GENERAL_SEE_FOR_DETAILS.get(UpgradeLog.getLogFilePath()), NOTICE_CALLBACK);

    // Checks License.
    checkLicence(context);

    logWarnAboutPatchesFolder();

    // Get the list of required upgrade tasks.
    final List<UpgradeTask> tasks =
        getUpgradeTasks(context.getFromVersion(), context.getToVersion());
    if (tasks.isEmpty())
    {
      changeBuildInfoVersion(context);
      return;
    }

    try
    {
      // Let tasks interact with the user in order to obtain user's selection.
      context.notify(INFO_UPGRADE_REQUIREMENTS.get(), TITLE_CALLBACK);
      for (final UpgradeTask task : tasks)
      {
        task.prepare(context);
      }

      // Starts upgrade
      final int userResponse = context.confirmYN(INFO_UPGRADE_DISPLAY_CONFIRM_START.get(), YES);
      if (userResponse == NO)
      {
        final LocalizableMessage message = INFO_UPGRADE_ABORTED_BY_USER.get();
        context.notify(message, WARNING);
        throw new ClientException(ReturnCode.ERROR_UNEXPECTED, message);
      }

      // Perform the upgrade tasks.
      context.notify(INFO_UPGRADE_PERFORMING_TASKS.get(), TITLE_CALLBACK);
      for (final UpgradeTask task : tasks)
      {
        task.perform(context);
      }

      if (UpgradeTasks.countErrors == 0)
      {
        /*
         * The end of a successful upgrade is marked up with the build info file update and the license,
         * if present, requires the creation of an approval file.
         */
        changeBuildInfoVersion(context);

        createFileLicenseApproved();
      }
      else
      {
        context.notify(ERR_UPGRADE_FAILS.get(UpgradeTasks.countErrors), TITLE_CALLBACK);
      }

      // Performs the post upgrade tasks.
      if (hasPostUpgradeTask && UpgradeTasks.countErrors == 0)
      {
        context.notify(INFO_UPGRADE_PERFORMING_POST_TASKS.get(), TITLE_CALLBACK);
        performPostUpgradeTasks(context, tasks);
        context.notify(INFO_UPGRADE_POST_TASKS_COMPLETE.get(), TITLE_CALLBACK);
      }
    }
    catch (final ClientException e)
    {
      context.notify(e.getMessageObject(), ERROR_CALLBACK);
      throw e;
    }
    catch (final Exception e)
    {
      final LocalizableMessage message = ERR_UPGRADE_TASKS_FAIL.get(stackTraceToSingleLineString(e));
      context.notify(message, ERROR_CALLBACK);
      throw new ClientException(ReturnCode.ERROR_UNEXPECTED, message, e);
    }
    finally
    {
      context.notify(INFO_UPGRADE_GENERAL_SEE_FOR_DETAILS.get(UpgradeLog.getLogFilePath()), NOTICE_CALLBACK);
      logger.info(INFO_UPGRADE_PROCESS_END);
    }
  }

  private static void performPostUpgradeTasks(final UpgradeContext context, final List<UpgradeTask> tasks)
      throws ClientException
  {
    boolean isOk = true;
    for (final UpgradeTask task : tasks)
    {
      if (isOk)
      {
        try
        {
          task.postUpgrade(context);
        }
        catch (ClientException e)
        {
          context.notify(e.getMessageObject(), WARNING);
          isOk = false;
        }
      }
      else
      {
        task.postponePostUpgrade(context);
      }
    }
  }

  private static void register(final String versionString,
      final UpgradeTask... tasks)
  {
    final BuildVersion version = BuildVersion.valueOf(versionString);
    List<UpgradeTask> taskList = TASKS.get(version);
    if (taskList == null)
    {
      taskList = new LinkedList<>();
      TASKS.put(version, taskList);
    }
    taskList.addAll(Arrays.asList(tasks));
  }

  private static void registerLast(final UpgradeTask... tasks)
  {
    MANDATORY_TASKS.addAll(Arrays.asList(tasks));
  }

  /**
   * The server must be offline during the upgrade.
   *
   * @throws ClientException
   *           An exception is thrown if the server is currently running.
   */
  private static void checkIfServerIsRunning(final UpgradeContext context) throws ClientException
  {
    final String lockFile = LockFileManager.getServerLockFileName();

    final StringBuilder failureReason = new StringBuilder();
    try
    {
      // Assume that if we cannot acquire the lock file the server is running.
      if (!LockFileManager.acquireExclusiveLock(lockFile, failureReason))
      {
        final LocalizableMessage message = ERR_UPGRADE_REQUIRES_SERVER_OFFLINE.get();
        context.notify(message, NOTICE_CALLBACK);
        throw new ClientException(ReturnCode.ERROR_UNEXPECTED, message);
      }
    }
    finally
    {
      LockFileManager.releaseLock(lockFile, failureReason);
    }
  }

  /**
   * Checks if the version can be updated.
   *
   * @param context
   *          The current context which running the upgrade.
   * @throws ClientException
   *           If an exception occurs - stops the process.
   */
  private static void isVersionCanBeUpdated(final UpgradeContext context)
      throws ClientException
  {
    if (context.getFromVersion().equals(context.getToVersion()))
    {
      // If the server is already up to date then treat it as a successful upgrade so that upgrade is idempotent.
      final LocalizableMessage message = ERR_UPGRADE_VERSION_UP_TO_DATE.get(context.getToVersion());
      context.notify(message, NOTICE_CALLBACK);
      throw new ClientException(ReturnCode.SUCCESS, message);
    }

    // The upgrade only supports version >= 2.4.5.
    if (context.getFromVersion().compareTo(UPGRADESUPPORTSVERSIONFROM) < 0)
    {
      final LocalizableMessage message =
          INFO_UPGRADE_VERSION_IS_NOT_SUPPORTED.get(UPGRADESUPPORTSVERSIONFROM, UPGRADESUPPORTSVERSIONFROM);
      context.notify(message, NOTICE_CALLBACK);
      throw new ClientException(ReturnCode.ERROR_UNEXPECTED, message);
    }
  }

  /**
   * Writes the up to date's version number within the build info file.
   *
   * @param context
   *          The current context which running the upgrade.
   * @throws ClientException
   *           If an exception occurs when displaying the message.
   */
  private static void changeBuildInfoVersion(final UpgradeContext context)
      throws ClientException
  {
    File buildInfoFile = new File(UpgradeUtils.configDirectory, Installation.BUILDINFO_RELATIVE_PATH);
    try (FileWriter buildInfo = new FileWriter(buildInfoFile, false))
    {

      // Write the new version
      buildInfo.write(context.getToVersion().toString());

      context.notify(INFO_UPGRADE_SUCCESSFUL.get(context.getFromVersion(), context.getToVersion()), TITLE_CALLBACK);
    }
    catch (IOException e)
    {
      final LocalizableMessage message = LocalizableMessage.raw(e.getMessage());
      context.notify(message, ERROR_CALLBACK);
      throw new ClientException(ReturnCode.ERROR_UNEXPECTED, message);
    }
  }

  private static void checkLicence(final UpgradeContext context)
      throws ClientException
  {
    // Check license
    if (LicenseFile.exists() && !LicenseFile.isAlreadyApproved())
    {
      context.notify(LocalizableMessage.raw(LINE_SEPARATOR + LicenseFile.getText()));
      context.notify(INFO_LICENSE_DETAILS_CLI_LABEL.get());
      if (!context.isAcceptLicenseMode())
      {
        final int answer;

        // The force cannot answer yes to the license's question, which is not a task even if it requires a user
        // interaction OR -an accept license mode to continue the process.
        if (context.isForceUpgradeMode())
        {
          answer = NO;
          context.notify(
              LocalizableMessage.raw(INFO_LICENSE_ACCEPT.get() + " " + INFO_PROMPT_NO_COMPLETE_ANSWER.get()));
        }
        else
        {
          answer = context.confirmYN(INFO_LICENSE_ACCEPT.get(), NO);
        }

        if (answer == NO)
        {
          System.exit(EXIT_CODE_SUCCESS);
        }
        else if (answer == YES)
        {
          LicenseFile.setApproval(true);
        }
      }
      else
      {
        // We automatically accept the license with this option.
        context.notify(
            LocalizableMessage.raw(INFO_LICENSE_ACCEPT.get() + " " + INFO_PROMPT_YES_COMPLETE_ANSWER.get()));
        LicenseFile.setApproval(true);
      }
    }
  }

  /**
   * The classes folder is renamed by the script launcher to avoid
   * incompatibility between patches and upgrade process. If a folder
   * "classes.disabled" is found, this function just displays a warning in the
   * log file, meaning the "classes" folder has been renamed. See upgrade.sh /
   * upgrade.bat scripts which hold the renaming process. (OPENDJ-1098)
   */
  private static void logWarnAboutPatchesFolder()
  {
    try
    {
      final File backup = new File(UpgradeUtils.getInstancePath(), "classes.disabled");
      if (backup.exists()) {
        final File[] files = backup.listFiles();
        if (files != null && files.length > 0)
        {
          logger.warn(INFO_UPGRADE_CLASSES_FOLDER_RENAMED, backup.getAbsoluteFile());
        }
      }
    }
    catch (SecurityException e)
    {
      logger.debug(LocalizableMessage.raw(e.getMessage()), e);
    }
  }

  /**
   * Returns {@code true} if the current upgrade contains post upgrade tasks.
   *
   * @return {@code true} if the current upgrade contains post upgrade tasks.
   */
  static boolean hasPostUpgradeTask()
  {
    return hasPostUpgradeTask;
  }

  /**
   * Sets {@code true} if the current upgrade contains post upgrade tasks.
   *
   * @param hasPostUpgradeTask
   *          {@code true} if the current upgrade contains post upgrade tasks.
   */
  static void setHasPostUpgradeTask(boolean hasPostUpgradeTask)
  {
    Upgrade.hasPostUpgradeTask = hasPostUpgradeTask;
  }

  /** Prevent instantiation. */
  private Upgrade()
  {
    // Nothing to do.
  }
}
