org.nuxeo.launcher.connect.ConnectBroker.java Source code

Java tutorial

Introduction

Here is the source code for org.nuxeo.launcher.connect.ConnectBroker.java

Source

/*
 * (C) Copyright 2012-2016 Nuxeo SA (http://nuxeo.com/) and others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Contributors:
 *     Julien Carsique
 *     Mathieu Guillaume
 *     Yannis JULIENNE
 *
 */

package org.nuxeo.launcher.connect;

import java.io.Console;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.collections.ListUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.impl.SimpleLog;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

import org.nuxeo.common.Environment;
import org.nuxeo.connect.CallbackHolder;
import org.nuxeo.connect.NuxeoConnectClient;
import org.nuxeo.connect.connector.ConnectServerError;
import org.nuxeo.connect.data.DownloadablePackage;
import org.nuxeo.connect.data.DownloadingPackage;
import org.nuxeo.connect.identity.LogicalInstanceIdentifier;
import org.nuxeo.connect.identity.LogicalInstanceIdentifier.InvalidCLID;
import org.nuxeo.connect.identity.LogicalInstanceIdentifier.NoCLID;
import org.nuxeo.connect.packages.PackageManager;
import org.nuxeo.connect.packages.dependencies.CUDFHelper;
import org.nuxeo.connect.packages.dependencies.DependencyResolution;
import org.nuxeo.connect.update.LocalPackage;
import org.nuxeo.connect.update.Package;
import org.nuxeo.connect.update.PackageException;
import org.nuxeo.connect.update.PackageState;
import org.nuxeo.connect.update.PackageType;
import org.nuxeo.connect.update.PackageUtils;
import org.nuxeo.connect.update.PackageVisibility;
import org.nuxeo.connect.update.ValidationStatus;
import org.nuxeo.connect.update.Version;
import org.nuxeo.connect.update.model.PackageDefinition;
import org.nuxeo.connect.update.standalone.StandaloneUpdateService;
import org.nuxeo.connect.update.task.Task;
import org.nuxeo.launcher.info.CommandInfo;
import org.nuxeo.launcher.info.CommandSetInfo;
import org.nuxeo.launcher.info.PackageInfo;

/**
 * @since 5.6
 */
public class ConnectBroker {

    private static final Log log = LogFactory.getLog(ConnectBroker.class);

    public static final String PARAM_MP_DIR = "nuxeo.distribution.marketplace.dir";

    public static final String DISTRIBUTION_MP_DIR_DEFAULT = "setupWizardDownloads";

    public static final String PACKAGES_XML = "packages.xml";

    protected static final String LAUNCHER_CHANGED_PROPERTY = "launcher.changed";

    protected static final int LAUNCHER_CHANGED_EXIT_CODE = 128;

    public static final String[] POSITIVE_ANSWERS = { "true", "yes", "y" };

    private Environment env;

    private StandaloneUpdateService service;

    private CallbackHolder cbHolder;

    private CommandSetInfo cset = new CommandSetInfo();

    private String targetPlatform;

    private String distributionMPDir;

    private String relax = OPTION_RELAX_DEFAULT;

    public static final String OPTION_RELAX_DEFAULT = "ask";

    private String accept = OPTION_ACCEPT_DEFAULT;

    private boolean allowSNAPSHOT = CUDFHelper.defaultAllowSNAPSHOT;

    public static final String OPTION_ACCEPT_DEFAULT = "ask";

    public ConnectBroker(Environment env) throws IOException, PackageException {
        this.env = env;
        service = new StandaloneUpdateService(env);
        service.initialize();
        cbHolder = new StandaloneCallbackHolder(env, service);
        NuxeoConnectClient.setCallBackHolder(cbHolder);
        targetPlatform = env.getProperty(Environment.DISTRIBUTION_NAME) + "-"
                + env.getProperty(Environment.DISTRIBUTION_VERSION);
        distributionMPDir = env.getProperty(PARAM_MP_DIR, DISTRIBUTION_MP_DIR_DEFAULT);
    }

    public String getCLID() throws NoCLID {
        return LogicalInstanceIdentifier.instance().getCLID();
    }

    /**
     * @throws NoCLID
     * @since 6.0
     */
    public void setCLID(String file) throws NoCLID {
        try {
            LogicalInstanceIdentifier.load(file);
        } catch (IOException | InvalidCLID e) {
            throw new NoCLID("can not load CLID", e);
        }
    }

    public StandaloneUpdateService getUpdateService() {
        return service;
    }

    public PackageManager getPackageManager() {
        return NuxeoConnectClient.getPackageManager();
    }

    public void refreshCache() {
        getPackageManager().flushCache();
        NuxeoConnectClient.getPackageManager().listAllPackages();
    }

    public CommandSetInfo getCommandSet() {
        return cset;
    }

    protected LocalPackage getInstalledPackage(String pkgName) {
        try {
            return service.getPersistence().getActivePackage(pkgName);
        } catch (PackageException e) {
            log.error(e);
            return null;
        }
    }

    protected boolean isInstalledPackage(String pkgName) {
        return service.getPersistence().getActivePackageId(pkgName) != null;
    }

    protected boolean isLocalPackageId(String pkgId) {
        try {
            return service.getPackage(pkgId) != null;
        } catch (PackageException e) {
            log.error("Error looking for local package " + pkgId, e);
            return false;
        }
    }

    protected boolean isRemotePackageId(String pkgId) {
        return PackageUtils.isValidPackageId(pkgId)
                && NuxeoConnectClient.getPackageManager().findPackageById(pkgId) != null;
    }

    protected String getBestIdForNameInList(String pkgName, List<? extends Package> pkgList) {
        String foundId = null;
        SortedMap<Version, String> foundPkgs = new TreeMap<>();
        SortedMap<Version, String> matchingPkgs = new TreeMap<>();
        for (Package pkg : pkgList) {
            if (pkg.getName().equals(pkgName)) {
                foundPkgs.put(pkg.getVersion(), pkg.getId());
                if (Arrays.asList(pkg.getTargetPlatforms()).contains(targetPlatform)) {
                    matchingPkgs.put(pkg.getVersion(), pkg.getId());
                }
            }
        }
        if (matchingPkgs.size() != 0) {
            foundId = matchingPkgs.get(matchingPkgs.lastKey());
        } else if (foundPkgs.size() != 0) {
            foundId = foundPkgs.get(foundPkgs.lastKey());
        }
        return foundId;
    }

    protected String getLocalPackageIdFromName(String pkgName) {
        return getBestIdForNameInList(pkgName, getPkgList());
    }

    protected List<String> getAllLocalPackageIdsFromName(String pkgName) {
        List<String> foundIds = new ArrayList<>();
        for (Package pkg : getPkgList()) {
            if (pkg.getName().equals(pkgName)) {
                foundIds.add(pkg.getId());
            }
        }
        return foundIds;
    }

    protected String getInstalledPackageIdFromName(String pkgName) {
        List<LocalPackage> localPackages = getPkgList();
        List<LocalPackage> installedPackages = new ArrayList<>();
        for (LocalPackage pkg : localPackages) {
            if (pkg.getPackageState().isInstalled()) {
                installedPackages.add(pkg);
            }
        }
        return getBestIdForNameInList(pkgName, installedPackages);
    }

    protected String getRemotePackageIdFromName(String pkgName) {
        return getBestIdForNameInList(pkgName, NuxeoConnectClient.getPackageManager().findRemotePackages(pkgName));
    }

    /**
     * Looks for a remote package from its name or id
     *
     * @param pkgNameOrId
     * @return the remote package Id; null if not found
     * @since 5.7
     */
    protected String getRemotePackageId(String pkgNameOrId) {
        String pkgId = null;
        if (isRemotePackageId(pkgNameOrId)) {
            pkgId = pkgNameOrId;
        } else {
            pkgId = getRemotePackageIdFromName(pkgNameOrId);
        }
        return pkgId;
    }

    /**
     * Looks for a local package from its name or id
     *
     * @since 5.7
     * @param pkgIdOrName
     * @return the local package Id; null if not found
     * @throws PackageException
     */
    protected LocalPackage getLocalPackage(String pkgIdOrName) throws PackageException {
        // Try as a package id
        LocalPackage pkg = service.getPackage(pkgIdOrName);
        if (pkg == null) {
            // Check whether this is the name of a local package
            String pkgId = getLocalPackageIdFromName(pkgIdOrName);
            if (pkgId != null) {
                pkg = service.getPackage(pkgId);
            }
        }
        return pkg;
    }

    /**
     * Looks for a package file from its path
     *
     * @param pkgFile Absolute or relative package file path
     * @return the file if found, else null
     */
    protected File getLocalPackageFile(String pkgFile) {
        if (pkgFile.startsWith("file:")) {
            pkgFile = pkgFile.substring(5);
        }
        // Try absolute path
        File fileToCheck = new File(pkgFile);
        if (!fileToCheck.exists()) { // Try relative path
            fileToCheck = new File(env.getServerHome(), pkgFile);
        }
        if (fileToCheck.exists()) {
            return fileToCheck;
        } else {
            return null;
        }
    }

    protected boolean isLocalPackageFile(String pkgFile) {
        return (getLocalPackageFile(pkgFile) != null);
    }

    protected List<String> getDistributionFilenames() {
        File distributionMPFile = new File(distributionMPDir, PACKAGES_XML);
        List<String> md5Filenames = new ArrayList<>();
        // Try to get md5 files from packages.xml
        DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
        docFactory.setNamespaceAware(true);
        try {
            DocumentBuilder builder = docFactory.newDocumentBuilder();
            Document doc = builder.parse(distributionMPFile);
            XPathFactory xpFactory = XPathFactory.newInstance();
            XPath xpath = xpFactory.newXPath();
            XPathExpression expr = xpath.compile("//package/@md5");
            NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
            for (int i = 0; i < nodes.getLength(); i++) {
                String md5 = nodes.item(i).getNodeValue();
                if ((md5 != null) && (md5.length() > 0)) {
                    md5Filenames.add(md5);
                }
            }
        } catch (Exception e) {
            // Parsing failed - return empty list
            log.error("Failed parsing " + distributionMPFile, e);
            return new ArrayList<>();
        }
        return md5Filenames;
    }

    protected Map<String, PackageDefinition> getDistributionDefinitions(List<String> md5Filenames) {
        Map<String, PackageDefinition> allDefinitions = new HashMap<>();
        if (md5Filenames == null) {
            return allDefinitions;
        }
        for (String md5Filename : md5Filenames) {
            File md5File = new File(distributionMPDir, md5Filename);
            if (!md5File.exists()) {
                // distribution file has been deleted
                continue;
            }
            ZipFile zipFile;
            try {
                zipFile = new ZipFile(md5File);
            } catch (ZipException e) {
                log.warn("Unzip error reading file " + md5File, e);
                continue;
            } catch (IOException e) {
                log.warn("Could not read file " + md5File, e);
                continue;
            }
            try {
                ZipEntry zipEntry = zipFile.getEntry("package.xml");
                InputStream in = zipFile.getInputStream(zipEntry);
                PackageDefinition pd = NuxeoConnectClient.getPackageUpdateService().loadPackage(in);
                allDefinitions.put(md5Filename, pd);
            } catch (Exception e) {
                log.error("Could not read package description", e);
                continue;
            } finally {
                try {
                    zipFile.close();
                } catch (IOException e) {
                    log.warn("Unexpected error closing file " + md5File, e);
                }
            }
        }
        return allDefinitions;
    }

    protected boolean addDistributionPackage(String md5) {
        boolean ret = true;
        File distributionFile = new File(distributionMPDir, md5);
        if (distributionFile.exists()) {
            try {
                ret = pkgAdd(distributionFile.getCanonicalPath(), false) != null;
            } catch (IOException e) {
                log.warn("Could not add distribution file " + md5);
                ret = false;
            }
        }
        return ret;
    }

    public boolean addDistributionPackages() {
        Map<String, PackageDefinition> distributionPackages = getDistributionDefinitions(
                getDistributionFilenames());
        if (distributionPackages.isEmpty()) {
            return true;
        }
        List<LocalPackage> localPackages = getPkgList();
        Map<String, LocalPackage> localPackagesById = new HashMap<>();
        if (localPackages != null) {
            for (LocalPackage pkg : localPackages) {
                localPackagesById.put(pkg.getId(), pkg);
            }
        }
        boolean ret = true;
        for (String md5 : distributionPackages.keySet()) {
            PackageDefinition md5Pkg = distributionPackages.get(md5);
            if (localPackagesById.containsKey(md5Pkg.getId())) {
                // We have the same package Id in the local cache
                LocalPackage localPackage = localPackagesById.get(md5Pkg.getId());
                if (localPackage.getVersion().isSnapshot()) {
                    // - For snapshots, until we have timestamp support, assume
                    // distribution version is newer than cached version.
                    // - This may (will) break the server if there are
                    // dependencies/compatibility changes or if the package is
                    // in installed state.
                    if (!localPackage.getPackageState().isInstalled()) {
                        pkgRemove(localPackage.getId());
                        ret = addDistributionPackage(md5) && ret;
                    }
                }
            } else {
                // No package with this Id is in cache
                ret = addDistributionPackage(md5) && ret;
            }
        }
        return ret;
    }

    public List<LocalPackage> getPkgList() {
        try {
            return service.getPackages();
        } catch (PackageException e) {
            log.error("Could not read package list", e);
            return null;
        }
    }

    public void pkgList() {
        log.info("Local packages:");
        pkgList(getPkgList());
    }

    public void pkgListAll() {
        log.info("All packages:");
        pkgList(NuxeoConnectClient.getPackageManager().listAllPackages());
    }

    public void pkgList(List<? extends Package> packagesList) {
        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_LIST);
        try {
            if (packagesList.isEmpty()) {
                log.info("None");
            } else {
                NuxeoConnectClient.getPackageManager().sort(packagesList);
                StringBuilder sb = new StringBuilder();
                for (Package pkg : packagesList) {
                    newPackageInfo(cmdInfo, pkg);
                    PackageState packageState = pkg.getPackageState();
                    String packageDescription = packageState.getLabel();
                    packageDescription = String.format("%6s %11s\t", pkg.getType(), packageDescription);
                    if (packageState == PackageState.REMOTE && pkg.getType() != PackageType.STUDIO
                            && pkg.getVisibility() != PackageVisibility.PUBLIC
                            && !LogicalInstanceIdentifier.isRegistered()) {
                        packageDescription += "Registration required for ";
                    }
                    packageDescription += String.format("%s (id: %s)\n", pkg.getName(), pkg.getId());
                    sb.append(packageDescription);
                }
                log.info(sb.toString());
            }
        } catch (Exception e) {
            log.error(e);
            cmdInfo.exitCode = 1;
        }
    }

    protected void performTask(Task task) throws PackageException {
        ValidationStatus validationStatus = task.validate();
        if (validationStatus.hasErrors()) {
            throw new PackageException("Failed to validate package " + task.getPackage().getId() + " -> "
                    + validationStatus.getErrors());
        }
        if (validationStatus.hasWarnings()) {
            log.warn("Got warnings on package validation " + task.getPackage().getId() + " -> "
                    + validationStatus.getWarnings());
        }
        task.run(null);
    }

    public boolean pkgReset() {
        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_RESET);
        if ("ask".equalsIgnoreCase(accept)) {
            accept = readConsole("The reset will erase the Nuxeo Packages history.\n"
                    + "Do you want to continue (yes/no)? [yes] ", "yes");
        }
        if (!Boolean.parseBoolean(accept)) {
            cmdInfo.exitCode = 1;
            return false;
        }
        try {
            service.reset();
            log.info("Packages reset done: all packages were marked as DOWNLOADED");
            List<LocalPackage> localPackages = service.getPackages();
            for (LocalPackage localPackage : localPackages) {
                localPackage.getUninstallFile().delete();
                FileUtils.deleteDirectory(localPackage.getData().getEntry(LocalPackage.BACKUP_DIR));
                newPackageInfo(cmdInfo, localPackage);
            }
            service.getRegistry().delete();
            FileUtils.deleteDirectory(service.getBackupDir());
        } catch (PackageException e) {
            log.error(e);
            cmdInfo.exitCode = 1;
        } catch (IOException e) {
            log.error(e);
            cmdInfo.exitCode = 1;
        }
        return cmdInfo.exitCode == 0;
    }

    public boolean pkgPurge() throws PackageException {
        List<String> localNames = new ArrayList<>();
        // Remove packages in DOWNLOADED state first
        // This will avoid extending the CUDF universe needlessly
        for (LocalPackage pkg : service.getPackages()) {
            if (pkg.getPackageState() == PackageState.DOWNLOADED) {
                pkgRemove(pkg.getId());
            }
        }
        // Process the remaining packages
        for (LocalPackage pkg : service.getPackages()) {
            localNames.add(pkg.getName());
        }
        return pkgRequest(null, null, null, localNames, true, false);
    }

    /**
     * Uninstall a list of packages. If the list contains a package name (versus an ID), only the considered as best
     * matching package is uninstalled.
     *
     * @param packageIdsToRemove The list can contain package IDs and names
     * @see #pkgUninstall(String)
     */
    public boolean pkgUninstall(List<String> packageIdsToRemove) {
        log.debug("Uninstalling: " + packageIdsToRemove);
        for (String pkgId : packageIdsToRemove) {
            if (pkgUninstall(pkgId) == null) {
                log.error("Unable to uninstall " + pkgId);
                return false;
            }
        }
        return true;
    }

    /**
     * Uninstall a local package. The package is not removed from cache.
     *
     * @param pkgId Package ID or Name
     * @return The uninstalled LocalPackage or null if failed
     */
    public LocalPackage pkgUninstall(String pkgId) {
        if (env.getProperty(LAUNCHER_CHANGED_PROPERTY, "false").equals("true")) {
            System.exit(LAUNCHER_CHANGED_EXIT_CODE);
        }
        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_UNINSTALL);
        cmdInfo.param = pkgId;
        try {
            LocalPackage pkg = service.getPackage(pkgId);
            if (pkg == null) {
                // Check whether this is the name of an installed package
                String realPkgId = getInstalledPackageIdFromName(pkgId);
                if (realPkgId != null) {
                    pkgId = realPkgId;
                    pkg = service.getPackage(realPkgId);
                }
            }
            if (pkg == null) {
                throw new PackageException("Package not found: " + pkgId);
            }
            log.info("Uninstalling " + pkgId);
            Task uninstallTask = pkg.getUninstallTask();
            try {
                performTask(uninstallTask);
            } catch (PackageException e) {
                uninstallTask.rollback();
                throw e;
            }
            // Refresh state
            pkg = service.getPackage(pkgId);
            newPackageInfo(cmdInfo, pkg);
            return pkg;
        } catch (Exception e) {
            log.error("Failed to uninstall package: " + pkgId, e);
            cmdInfo.exitCode = 1;
            return null;
        }
    }

    /**
     * Remove a list of packages from cache. If the list contains a package name (versus an ID), all matching packages
     * are removed.
     *
     * @param pkgsToRemove The list can contain package IDs and names
     * @see #pkgRemove(String)
     */
    public boolean pkgRemove(List<String> pkgsToRemove) {
        boolean cmdOk = true;
        if (pkgsToRemove != null) {
            log.debug("Removing: " + pkgsToRemove);
            for (String pkgNameOrId : pkgsToRemove) {
                List<String> allIds;
                if (isLocalPackageId(pkgNameOrId)) {
                    allIds = new ArrayList<>();
                    allIds.add(pkgNameOrId);
                } else {
                    // Request made on a name: remove all matching packages
                    allIds = getAllLocalPackageIdsFromName(pkgNameOrId);
                }
                for (String pkgId : allIds) {
                    if (pkgRemove(pkgId) == null) {
                        log.warn("Unable to remove " + pkgId);
                        // Don't error out on failed (cache) removal
                        cmdOk = false;
                    }
                }
            }
        }
        return cmdOk;
    }

    /**
     * Remove a package from cache. If it was installed, the package is uninstalled then removed.
     *
     * @param pkgId Package ID or Name
     * @return The removed LocalPackage or null if failed
     */
    public LocalPackage pkgRemove(String pkgId) {
        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_REMOVE);
        cmdInfo.param = pkgId;
        try {
            LocalPackage pkg = service.getPackage(pkgId);
            if (pkg == null) {
                // Check whether this is the name of a local package
                String realPkgId = getLocalPackageIdFromName(pkgId);
                if (realPkgId != null) {
                    pkgId = realPkgId;
                    pkg = service.getPackage(realPkgId);
                }
            }
            if (pkg == null) {
                throw new PackageException("Package not found: " + pkgId);
            }
            if (pkg.getPackageState().isInstalled()) {
                pkgUninstall(pkgId);
                // Refresh state
                pkg = service.getPackage(pkgId);
            }
            if (pkg.getPackageState() != PackageState.DOWNLOADED) {
                throw new PackageException("Can only remove packages in DOWNLOADED, INSTALLED or STARTED state");
            }
            service.removePackage(pkgId);
            log.info("Removed " + pkgId);
            newPackageInfo(cmdInfo, pkg).state = PackageState.REMOTE;
            return pkg;
        } catch (Exception e) {
            log.error("Failed to remove package: " + pkgId, e);
            cmdInfo.exitCode = 1;
            return null;
        }
    }

    /**
     * Add a list of packages into the cache, downloading them if needed and possible.
     *
     * @param pkgsToAdd
     * @return true if command succeeded
     * @see #pkgAdd(List, boolean)
     * @see #pkgAdd(String, boolean)
     * @deprecated Since 7.10. Use a method with an explicit value for {@code ignoreMissing}.
     */
    @Deprecated
    public boolean pkgAdd(List<String> pkgsToAdd) {
        return pkgAdd(pkgsToAdd, false);
    }

    /**
     * Add a list of packages into the cache, downloading them if needed and possible.
     *
     * @since 6.0
     * @param pkgsToAdd
     * @param ignoreMissing
     * @return true if command succeeded
     * @see #pkgAdd(String, boolean)
     */
    public boolean pkgAdd(List<String> pkgsToAdd, boolean ignoreMissing) {
        boolean cmdOk = true;
        if (pkgsToAdd == null || pkgsToAdd.isEmpty()) {
            return cmdOk;
        }
        List<String> pkgIdsToDownload = new ArrayList<>();
        for (String pkgToAdd : pkgsToAdd) {
            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_ADD);
            cmdInfo.param = pkgToAdd;
            try {
                File fileToAdd = getLocalPackageFile(pkgToAdd);
                if (fileToAdd == null) {
                    String pkgId = getRemotePackageId(pkgToAdd);
                    if (pkgId == null) {
                        if (ignoreMissing) {
                            log.warn("Could not add package: " + pkgToAdd);
                            cmdInfo.newMessage(SimpleLog.LOG_LEVEL_INFO, "Could not add package.");
                        } else {
                            throw new PackageException("Could not find a remote or local (relative to "
                                    + "current directory or to NUXEO_HOME) " + "package with name or ID "
                                    + pkgToAdd);
                        }
                    } else {
                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_INFO, "Waiting for download...");
                        pkgIdsToDownload.add(pkgId);
                    }
                } else {
                    LocalPackage pkg = service.addPackage(fileToAdd);
                    log.info("Added " + pkg);
                    newPackageInfo(cmdInfo, pkg);
                }
            } catch (PackageException e) {
                cmdOk = false;
                cmdInfo.exitCode = 1;
                cmdInfo.newMessage(e);
            }
        }
        cmdOk = downloadPackages(pkgIdsToDownload) && cmdOk;
        return cmdOk;
    }

    /**
     * Add a package file into the cache
     *
     * @param packageFileName
     * @return The added LocalPackage or null if failed
     * @see #pkgAdd(List, boolean)
     * @see #pkgAdd(String, boolean)
     * @deprecated Since 7.10. Use a method with an explicit value for {@code ignoreMissing}.
     */
    @Deprecated
    public LocalPackage pkgAdd(String packageFileName) {
        return pkgAdd(packageFileName, false);
    }

    /**
     * Add a package file into the cache
     *
     * @since 6.0
     * @param packageFileName
     * @param ignoreMissing
     * @return The added LocalPackage or null if failed
     * @see #pkgAdd(List, boolean)
     */
    public LocalPackage pkgAdd(String packageFileName, boolean ignoreMissing) {
        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_ADD);
        cmdInfo.param = packageFileName;
        LocalPackage pkg = null;
        try {
            File fileToAdd = getLocalPackageFile(packageFileName);
            if (fileToAdd == null) {
                String pkgId = getRemotePackageId(packageFileName);
                if (pkgId == null) {
                    if (ignoreMissing) {
                        log.warn("Could not add package: " + packageFileName);
                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_INFO, "Could not add package.");
                        return null;
                    } else {
                        throw new PackageException("Could not find a remote or local (relative to "
                                + "current directory or to NUXEO_HOME) " + "package with name or ID "
                                + packageFileName);
                    }
                } else if (!downloadPackages(Arrays.asList(new String[] { pkgId }))) {
                    throw new PackageException("Could not download package " + pkgId);
                }
                pkg = service.getPackage(pkgId);
                if (pkg == null) {
                    throw new PackageException("Could not find downloaded package in cache " + pkgId);
                }
            } else {
                pkg = service.addPackage(fileToAdd);
            }
            log.info("Added " + packageFileName);
            newPackageInfo(cmdInfo, pkg);
        } catch (PackageException e) {
            cmdInfo.exitCode = 1;
            cmdInfo.newMessage(e);
        }
        return pkg;
    }

    /**
     * Install a list of local packages. If the list contains a package name (versus an ID), only the considered as best
     * matching package is installed.
     *
     * @param packageIdsToInstall The list can contain package IDs and names
     * @see #pkgInstall(List, boolean)
     * @see #pkgInstall(String, boolean)
     * @deprecated Since 7.10. Use a method with an explicit value for {@code ignoreMissing}.
     */
    @Deprecated
    public boolean pkgInstall(List<String> packageIdsToInstall) {
        return pkgInstall(packageIdsToInstall, false);
    }

    /**
     * Install a list of local packages. If the list contains a package name (versus an ID), only the considered as best
     * matching package is installed.
     *
     * @since 6.0
     * @param packageIdsToInstall The list can contain package IDs and names
     * @param ignoreMissing If true, doesn't throw an exception on unknown packages
     * @see #pkgInstall(String, boolean)
     */
    public boolean pkgInstall(List<String> packageIdsToInstall, boolean ignoreMissing) {
        log.debug("Installing: " + packageIdsToInstall);
        for (String pkgId : packageIdsToInstall) {
            if (pkgInstall(pkgId, ignoreMissing) == null && !ignoreMissing) {
                return false;
            }
        }
        return true;
    }

    /**
     * Install a local package.
     *
     * @param pkgId Package ID or Name
     * @return The installed LocalPackage or null if failed
     * @see #pkgInstall(List, boolean)
     * @see #pkgInstall(String, boolean)
     * @deprecated Since 7.10. Use a method with an explicit value for {@code ignoreMissing}.
     */
    @Deprecated
    public LocalPackage pkgInstall(String pkgId) {
        return pkgInstall(pkgId, false);
    }

    /**
     * Install a local package.
     *
     * @since 6.0
     * @param pkgId Package ID or Name
     * @param ignoreMissing If true, doesn't throw an exception on unknown packages
     * @return The installed LocalPackage or null if failed
     * @see #pkgInstall(List, boolean)
     */
    public LocalPackage pkgInstall(String pkgId, boolean ignoreMissing) {
        if (env.getProperty(LAUNCHER_CHANGED_PROPERTY, "false").equals("true")) {
            System.exit(LAUNCHER_CHANGED_EXIT_CODE);
        }
        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_INSTALL);
        cmdInfo.param = pkgId;
        try {
            LocalPackage pkg = getLocalPackage(pkgId);
            if (pkg != null && pkg.getPackageState().isInstalled()) {
                if (pkg.getVersion().isSnapshot()) {
                    log.info(String.format("Updating package %s...", pkg));
                    // First remove it to allow SNAPSHOT upgrade
                    pkgRemove(pkgId);
                    pkg = null;
                } else {
                    log.info(String.format("Package %s is already installed.", pkg));
                    return pkg;
                }
            }
            if (pkg == null) {
                // We don't know this package, try to add it first
                pkg = pkgAdd(pkgId, ignoreMissing);
            }
            if (pkg == null) {
                // Nothing worked - can't find the package anywhere
                if (ignoreMissing) {
                    log.warn("Unable to install package: " + pkgId);
                    return null;
                } else {
                    throw new PackageException("Package not found: " + pkgId);
                }
            }
            pkgId = pkg.getId();
            cmdInfo.param = pkgId;
            log.info("Installing " + pkgId);
            Task installTask = pkg.getInstallTask();
            try {
                performTask(installTask);
            } catch (PackageException e) {
                installTask.rollback();
                throw e;
            }
            // Refresh state
            pkg = service.getPackage(pkgId);
            newPackageInfo(cmdInfo, pkg);
            return pkg;
        } catch (PackageException e) {
            log.error(String.format("Failed to install package: %s (%s)", pkgId, e.getMessage()));
            log.debug(e, e);
            cmdInfo.exitCode = 1;
            cmdInfo.newMessage(e);
            return null;
        }
    }

    public boolean listPending(File commandsFile) {
        return executePending(commandsFile, false, false, false);
    }

    /**
     * @since 5.6
     * @param commandsFile File containing the commands to execute
     * @param doExecute Whether to execute or list the actions
     * @param useResolver Whether to use full resolution or just execute individual actions
     */
    public boolean executePending(File commandsFile, boolean doExecute, boolean useResolver,
            boolean ignoreMissing) {
        int errorValue = 0;
        if (!commandsFile.isFile()) {
            return false;
        }
        List<String> pkgsToAdd = new ArrayList<>();
        List<String> pkgsToInstall = new ArrayList<>();
        List<String> pkgsToUninstall = new ArrayList<>();
        List<String> pkgsToRemove = new ArrayList<>();
        List<String> lines;
        try {
            lines = FileUtils.readLines(commandsFile);
            for (String line : lines) {
                line = line.trim();
                String[] split = line.split("\\s+", 2);
                if (split.length == 2) {
                    if (split[0].equals(CommandInfo.CMD_INSTALL)) {
                        if (doExecute) {
                            if (useResolver) {
                                pkgsToInstall.add(split[1]);
                            } else {
                                pkgInstall(split[1], ignoreMissing);
                            }
                        } else {
                            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_INSTALL);
                            cmdInfo.param = split[1];
                            cmdInfo.pending = true;
                        }
                    } else if (split[0].equals(CommandInfo.CMD_ADD)) {
                        if (doExecute) {
                            if (useResolver) {
                                pkgsToAdd.add(split[1]);
                            } else {
                                pkgAdd(split[1], ignoreMissing);
                            }
                        } else {
                            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_ADD);
                            cmdInfo.param = split[1];
                            cmdInfo.pending = true;
                        }
                    } else if (split[0].equals(CommandInfo.CMD_UNINSTALL)) {
                        if (doExecute) {
                            if (useResolver) {
                                pkgsToUninstall.add(split[1]);
                            } else {
                                pkgUninstall(split[1]);
                            }
                        } else {
                            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_UNINSTALL);
                            cmdInfo.param = split[1];
                            cmdInfo.pending = true;
                        }
                    } else if (split[0].equals(CommandInfo.CMD_REMOVE)) {
                        if (doExecute) {
                            if (useResolver) {
                                pkgsToRemove.add(split[1]);
                            } else {
                                pkgRemove(split[1]);
                            }
                        } else {
                            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_REMOVE);
                            cmdInfo.param = split[1];
                            cmdInfo.pending = true;
                        }
                    } else {
                        errorValue = 1;
                    }
                } else if (split.length == 1) {
                    if (line.length() > 0 && !line.startsWith("#")) {
                        if (doExecute) {
                            if ("init".equals(line)) {
                                if (!addDistributionPackages()) {
                                    errorValue = 1;
                                }
                            } else {
                                if (useResolver) {
                                    pkgsToInstall.add(line);
                                } else {
                                    pkgInstall(line, ignoreMissing);
                                }
                            }
                        } else {
                            if ("init".equals(line)) {
                                CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_INIT);
                                cmdInfo.pending = true;
                            } else {
                                CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_INSTALL);
                                cmdInfo.param = line;
                                cmdInfo.pending = true;
                            }
                        }
                    }
                }
                if (errorValue != 0) {
                    log.error("Error processing pending package/command: " + line);
                }
            }
            if (doExecute) {
                if (useResolver) {
                    String oldAccept = accept;
                    String oldRelax = relax;
                    accept = "true";
                    if ("ask".equalsIgnoreCase(relax)) {
                        log.info("Relax mode changed from 'ask' to 'false' for executing the pending actions.");
                        relax = "false";
                    }
                    boolean success = pkgRequest(pkgsToAdd, pkgsToInstall, pkgsToUninstall, pkgsToRemove, true,
                            ignoreMissing);
                    accept = oldAccept;
                    relax = oldRelax;
                    if (!success) {
                        errorValue = 2;
                    }
                }
                if (errorValue != 0) {
                    File bak = new File(commandsFile.getPath() + ".bak");
                    bak.delete();
                    commandsFile.renameTo(bak);
                    log.error("Pending actions execution failed. The commands file has been moved to: " + bak);
                } else {
                    commandsFile.delete();
                }
            } else {
                cset.log(true);
            }
        } catch (IOException e) {
            log.error(e.getMessage());
        }
        return errorValue == 0;
    }

    @SuppressWarnings("unused")
    protected boolean downloadPackages(List<String> packagesToDownload) {
        boolean isRegistered = LogicalInstanceIdentifier.isRegistered();
        List<String> packagesAlreadyDownloaded = new ArrayList<String>();
        Map<String, String> packagesToRemove = new HashMap<String, String>();
        for (String pkg : packagesToDownload) {
            LocalPackage localPackage;
            try {
                localPackage = getLocalPackage(pkg);
            } catch (PackageException e) {
                log.error(String.format("Looking for package '%s' in local cache raised an error. Aborting.", pkg),
                        e);
                return false;
            }
            if (localPackage == null) {
                continue;
            }
            if (localPackage.getPackageState().isInstalled()) {
                log.error(String.format("Package '%s' is installed. Download skipped.", pkg));
                packagesAlreadyDownloaded.add(pkg);
            } else if (localPackage.getVersion().isSnapshot()) {
                if (localPackage.getVisibility() != PackageVisibility.PUBLIC && !isRegistered) {
                    log.info(String.format("Update of '%s' requires being registered.", pkg));
                    packagesAlreadyDownloaded.add(pkg);
                } else {
                    log.info(String.format("Download of '%s' will replace the one already in local cache.", pkg));
                    packagesToRemove.put(localPackage.getId(), pkg);
                }
            } else {
                log.info(String.format("Package '%s' is already in local cache.", pkg));
                packagesAlreadyDownloaded.add(pkg);
            }
        }

        // First remove SNAPSHOT packages to replace
        for (String pkgToRemove : packagesToRemove.keySet()) {
            if (pkgRemove(pkgToRemove) == null) {
                log.error(String.format("Failed to remove '%s'. Download of '%s' skipped", pkgToRemove,
                        packagesToRemove.get(pkgToRemove)));
                packagesToDownload.remove(packagesToRemove.get(pkgToRemove));
            }
        }

        packagesToDownload.removeAll(packagesAlreadyDownloaded);
        if (packagesToDownload.isEmpty()) {
            return true;
        }
        // Queue downloads
        log.info("Downloading " + packagesToDownload + "...");
        boolean downloadOk = true;
        List<DownloadingPackage> pkgs = new ArrayList<DownloadingPackage>();
        for (String pkg : packagesToDownload) {
            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_DOWNLOAD);
            cmdInfo.param = pkg;

            // Check registration and package visibility
            DownloadablePackage downloadablePkg = getPackageManager().findRemotePackageById(pkg);
            if (downloadablePkg.getVisibility() != PackageVisibility.PUBLIC && !isRegistered) {
                downloadOk = false;
                cmdInfo.exitCode = 1;
                cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Registration required.");
                continue;
            }

            // Download
            try {
                DownloadingPackage download = getPackageManager().download(pkg);
                if (download != null) {
                    pkgs.add(download);
                    cmdInfo.param = download.getId();
                    cmdInfo.newMessage(SimpleLog.LOG_LEVEL_DEBUG, "Downloading...");
                } else {
                    downloadOk = false;
                    cmdInfo.exitCode = 1;
                    cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download failed (not found).");
                }
            } catch (ConnectServerError e) {
                log.debug(e, e);
                downloadOk = false;
                cmdInfo.exitCode = 1;
                cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download failed: " + e.getMessage());
            }
        }
        // Check and display progress
        final String progress = "|/-\\";
        int x = 0;
        boolean stopDownload = false;
        do {
            System.out.print(progress.charAt(x++ % progress.length()) + "\r");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                stopDownload = true;
            }
            List<DownloadingPackage> pkgsCompleted = new ArrayList<DownloadingPackage>();
            for (DownloadingPackage pkg : pkgs) {
                if (pkg.isCompleted()) {
                    pkgsCompleted.add(pkg);
                    CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_DOWNLOAD);
                    cmdInfo.param = pkg.getId();
                    // Digest check not correctly implemented
                    if (false && !pkg.isDigestOk()) {
                        downloadOk = false;
                        cmdInfo.exitCode = 1;
                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Wrong digest.");
                    } else if (pkg.getPackageState() == PackageState.DOWNLOADED) {
                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_DEBUG, "Downloaded.");
                    } else {
                        downloadOk = false;
                        cmdInfo.exitCode = 1;
                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download failed: " + pkg.getErrorMessage());
                        if (pkg.isServerError()) { // Wasted effort to continue other downloads
                            stopDownload = true;
                        }
                    }
                }
            }
            pkgs.removeAll(pkgsCompleted);
        } while (!stopDownload && pkgs.size() > 0);
        if (pkgs.size() > 0) {
            downloadOk = false;
            log.error("Packages download was interrupted");
            for (DownloadingPackage pkg : pkgs) {
                CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_ADD);
                cmdInfo.param = pkg.getId();
                cmdInfo.exitCode = 1;
                cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download interrupted.");
            }
        }
        return downloadOk;
    }

    /**
     * @deprecated Since 7.10. Use {@link #pkgRequest(List, List, List, List, boolean, boolean)} instead.
     */
    @Deprecated
    public boolean pkgRequest(List<String> pkgsToAdd, List<String> pkgsToInstall, List<String> pkgsToUninstall,
            List<String> pkgsToRemove) {
        return pkgRequest(pkgsToAdd, pkgsToInstall, pkgsToUninstall, pkgsToRemove, true, false);
    }

    /**
     * @param keepExisting If false, the request will remove existing packages that are not part of the resolution
     * @since 5.9.2
     * @deprecated Since 7.10. Use {@link #pkgRequest(List, List, List, List, boolean, boolean)} instead.
     */
    @Deprecated
    public boolean pkgRequest(List<String> pkgsToAdd, List<String> pkgsToInstall, List<String> pkgsToUninstall,
            List<String> pkgsToRemove, boolean keepExisting) {
        return pkgRequest(pkgsToAdd, pkgsToInstall, pkgsToUninstall, pkgsToRemove, keepExisting, false);
    }

    /**
     * @param keepExisting If false, the request will remove existing packages that are not part of the resolution
     * @param ignoreMissing Do not error out on missing packages, just handle the rest
     * @since 5.9.2
     */
    public boolean pkgRequest(List<String> pkgsToAdd, List<String> pkgsToInstall, List<String> pkgsToUninstall,
            List<String> pkgsToRemove, boolean keepExisting, boolean ignoreMissing) {
        // default is install mode
        return pkgRequest(pkgsToAdd, pkgsToInstall, pkgsToUninstall, pkgsToRemove, keepExisting, ignoreMissing,
                false);
    }

    /**
     * @param keepExisting If false, the request will remove existing packages that are not part of the resolution
     * @param ignoreMissing Do not error out on missing packages, just handle the rest
     * @param upgradeMode If true, all packages will be upgraded to their last compliant version
     * @since 8.4
     */
    public boolean pkgRequest(List<String> pkgsToAdd, List<String> pkgsToInstall, List<String> pkgsToUninstall,
            List<String> pkgsToRemove, boolean keepExisting, boolean ignoreMissing, boolean upgradeMode) {
        try {
            boolean cmdOk = true;
            // Add local files
            cmdOk = pkgAdd(pkgsToAdd, ignoreMissing);
            // Build solver request
            List<String> solverInstall = new ArrayList<>();
            List<String> solverRemove = new ArrayList<>();
            List<String> solverUpgrade = new ArrayList<>();
            if (pkgsToInstall != null) {
                // If install request is a file name, add to cache and get the
                // id
                List<String> namesOrIdsToInstall = new ArrayList<>();
                for (String pkgToInstall : pkgsToInstall) {
                    if (isLocalPackageFile(pkgToInstall)) {
                        LocalPackage addedPkg = pkgAdd(pkgToInstall, ignoreMissing);
                        if (addedPkg != null) {
                            namesOrIdsToInstall.add(addedPkg.getId());
                        } else {
                            cmdOk = false;
                        }
                        // TODO: set flag to prefer local package
                    } else {
                        namesOrIdsToInstall.add(pkgToInstall);
                    }
                }

                if (upgradeMode) {
                    solverUpgrade.addAll(namesOrIdsToInstall);
                } else {
                    solverInstall.addAll(namesOrIdsToInstall);
                }
            }
            if (pkgsToUninstall != null) {
                solverRemove.addAll(pkgsToUninstall);
            }
            if (pkgsToRemove != null) {
                // Add packages to remove to uninstall list
                solverRemove.addAll(pkgsToRemove);
            }
            if ((solverInstall.size() != 0) || (solverRemove.size() != 0) || (solverUpgrade.size() != 0)) {
                // Check whether we need to relax restriction to targetPlatform
                String requestPlatform = targetPlatform;
                List<String> requestPackages = new ArrayList<>();
                requestPackages.addAll(solverInstall);
                requestPackages.addAll(solverRemove);
                requestPackages.addAll(solverUpgrade);
                if (ignoreMissing) {
                    // Remove unknown packages from the list
                    Map<String, List<DownloadablePackage>> knownNames = getPackageManager().getAllPackagesByName();
                    List<String> solverInstallCopy = new ArrayList<>(solverInstall);
                    for (String pkgToInstall : solverInstallCopy) {
                        if (!knownNames.containsKey(pkgToInstall)) {
                            log.warn("Unable to install unknown package: " + pkgToInstall);
                            solverInstall.remove(pkgToInstall);
                            requestPackages.remove(pkgToInstall);
                        }
                    }
                }
                List<String> nonCompliantPkg = getPackageManager().getNonCompliantList(requestPackages,
                        targetPlatform);
                if (nonCompliantPkg.size() > 0) {
                    requestPlatform = null;
                    if ("ask".equalsIgnoreCase(relax)) {
                        relax = readConsole(
                                "Package(s) %s not available on platform version %s.\n"
                                        + "Do you want to relax the constraint (yes/no)? [no] ",
                                "no", StringUtils.join(nonCompliantPkg, ", "), targetPlatform);
                    }

                    if (Boolean.parseBoolean(relax)) {
                        log.warn(String.format("Relax restriction to target platform %s because of package(s) %s",
                                targetPlatform, StringUtils.join(nonCompliantPkg, ", ")));
                    } else {
                        if (ignoreMissing) {
                            for (String pkgToInstall : nonCompliantPkg) {
                                log.warn("Unable to install package: " + pkgToInstall);
                                solverInstall.remove(pkgToInstall);
                            }
                        } else {
                            throw new PackageException(String.format(
                                    "Package(s) %s not available on platform version %s (relax is not allowed)",
                                    StringUtils.join(nonCompliantPkg, ", "), targetPlatform));
                        }
                    }
                }

                log.debug("solverInstall: " + solverInstall);
                log.debug("solverRemove: " + solverRemove);
                log.debug("solverUpgrade: " + solverUpgrade);
                DependencyResolution resolution = getPackageManager().resolveDependencies(solverInstall,
                        solverRemove, solverUpgrade, requestPlatform, allowSNAPSHOT, keepExisting);
                log.info(resolution);
                if (resolution.isFailed()) {
                    return false;
                }
                if (resolution.isEmpty()) {
                    pkgRemove(pkgsToRemove);
                    return cmdOk;
                }
                if ("ask".equalsIgnoreCase(accept)) {
                    accept = readConsole("Do you want to continue (yes/no)? [yes] ", "yes");
                }
                if (!Boolean.parseBoolean(accept)) {
                    log.warn("Exit");
                    return false;
                }

                List<String> packageIdsToRemove = resolution.getOrderedPackageIdsToRemove();
                List<String> packageIdsToUpgrade = resolution.getUpgradePackageIds();
                List<String> packageIdsToInstall = resolution.getOrderedPackageIdsToInstall();
                List<String> packagesIdsToReInstall = new ArrayList<>();

                // Download remote packages
                if (!downloadPackages(resolution.getDownloadPackageIds())) {
                    log.error("Aborting packages change request");
                    return false;
                }

                // Uninstall
                if (!packageIdsToUpgrade.isEmpty()) {
                    // Add packages to upgrade to uninstall list
                    // Don't use IDs to avoid downgrade instead of uninstall
                    packageIdsToRemove.addAll(resolution.getLocalPackagesToUpgrade().keySet());
                    DependencyResolution uninstallResolution = getPackageManager().resolveDependencies(null,
                            packageIdsToRemove, null, requestPlatform, allowSNAPSHOT, keepExisting);
                    log.debug("Sub-resolution (uninstall) " + uninstallResolution);
                    if (uninstallResolution.isFailed()) {
                        return false;
                    }
                    List<String> newPackageIdsToRemove = uninstallResolution.getOrderedPackageIdsToRemove();
                    packagesIdsToReInstall = ListUtils.subtract(newPackageIdsToRemove, packageIdsToRemove);
                    packagesIdsToReInstall.removeAll(packageIdsToUpgrade);
                    packageIdsToRemove = newPackageIdsToRemove;
                }
                if (!pkgUninstall(packageIdsToRemove)) {
                    return false;
                }

                // Install
                if (!packagesIdsToReInstall.isEmpty()) {
                    // Add list of packages uninstalled because of upgrade
                    packageIdsToInstall.addAll(packagesIdsToReInstall);
                    DependencyResolution installResolution = getPackageManager().resolveDependencies(
                            packageIdsToInstall, null, null, requestPlatform, allowSNAPSHOT, keepExisting);
                    log.debug("Sub-resolution (install) " + installResolution);
                    if (installResolution.isFailed()) {
                        return false;
                    }
                    packageIdsToInstall = installResolution.getOrderedPackageIdsToInstall();
                }
                fixupPackageOrder(packageIdsToInstall);
                if (!pkgInstall(packageIdsToInstall, ignoreMissing)) {
                    return false;
                }

                pkgRemove(pkgsToRemove);
            }
            return cmdOk;
        } catch (PackageException e) {
            log.error(e);
            log.debug(e, e);
            return false;
        }
    }

    /**
     * Hack to make sure nuxeo-jsf-ui and nuxeo-web-ui are installed first. This is done to allow conditional execution
     * checking for these packages.
     *
     * @since 8.4
     */
    protected void fixupPackageOrder(List<String> packageIdsToInstall) {
        List<String> addFirst = new ArrayList<String>();
        for (Iterator<String> it = packageIdsToInstall.iterator(); it.hasNext();) {
            String pkg = it.next();
            if (pkg.startsWith("nuxeo-jsf-ui") || pkg.startsWith("nuxeo-web-ui")) {
                it.remove();
                addFirst.add(pkg);
            }
        }
        packageIdsToInstall.addAll(0, addFirst);
    }

    /**
     * Installs a list of packages and uninstalls the rest (no dependency check)
     *
     * @since 5.9.2
     * @deprecated Since 7.10. Use #pkgSet(List, boolean) instead.
     */
    @Deprecated
    public boolean pkgSet(List<String> pkgList) {
        return pkgSet(pkgList, false);
    }

    /**
     * Installs a list of packages and uninstalls the rest (no dependency check)
     *
     * @since 6.0
     */
    public boolean pkgSet(List<String> pkgList, boolean ignoreMissing) {
        boolean cmdOK = true;
        cmdOK = cmdOK && pkgInstall(pkgList, ignoreMissing);
        List<DownloadablePackage> installedPkgs = getPackageManager().listInstalledPackages();
        List<String> pkgsToUninstall = new ArrayList<>();
        for (DownloadablePackage pkg : installedPkgs) {
            if ((!pkgList.contains(pkg.getName())) && (!pkgList.contains(pkg.getId()))) {
                pkgsToUninstall.add(pkg.getId());
            }
        }
        if (pkgsToUninstall.size() != 0) {
            cmdOK = cmdOK && pkgUninstall(pkgsToUninstall);
        }
        return cmdOK;
    }

    /**
     * Prompt user for yes/no answer
     *
     * @param message The message to display
     * @param defaultValue The default answer if there's no console or if "Enter" key is pressed.
     * @param objects Parameters to use in the message (like in {@link String#format(String, Object...)})
     * @return {@code "true"} if answer is in {@link #POSITIVE_ANSWERS}, else return {@code "false"}
     */
    protected String readConsole(String message, String defaultValue, Object... objects) {
        String answer;
        Console console = System.console();
        if (console == null || StringUtils.isEmpty(answer = console.readLine(message, objects))) {
            answer = defaultValue;
        }
        answer = answer.trim().toLowerCase();
        return parseAnswer(answer);
    }

    /**
     * @return {@code "true"} if answer is in {@link #POSITIVE_ANSWERS}, and {@code "ask"} if answer values
     *         {@code "ask"}, else return {@code "false"}
     * @since 6.0
     */
    public static String parseAnswer(String answer) {
        if ("ask".equalsIgnoreCase(answer)) {
            return "ask";
        }
        if ("false".equalsIgnoreCase(answer)) {
            return "false";
        }
        for (String positive : POSITIVE_ANSWERS) {
            if (positive.equalsIgnoreCase(answer)) {
                return "true";
            }
        }
        return "false";
    }

    public boolean pkgHotfix() {
        List<String> hotFixNames = getPackageManager().listHotfixesNames(targetPlatform, allowSNAPSHOT);
        return pkgRequest(null, hotFixNames, null, null, true, false);
    }

    public boolean pkgUpgrade() {
        List<String> upgradeNames = getPackageManager().listInstalledPackagesNames(null);
        // use upgrade mode
        return pkgRequest(null, upgradeNames, null, null, true, false, true);
    }

    /**
     * Must be called after {@link #setAccept(String)} which overwrites its value.
     *
     * @param relaxValue true, false or ask; ignored if null
     */
    public void setRelax(String relaxValue) {
        if (relaxValue != null) {
            relax = parseAnswer(relaxValue);
        }
    }

    /**
     * @param acceptValue true, false or ask; if true or ask, then calls {@link #setRelax(String)} with the same value;
     *            ignored if null
     */
    public void setAccept(String acceptValue) {
        if (acceptValue != null) {
            accept = parseAnswer(acceptValue);
            if ("ask".equals(accept) || "true".equals(accept)) {
                setRelax(acceptValue);
            }
        }
    }

    /*
     * Helper for adding a new PackageInfo initialized with informations gathered from the given package. It is not put
     * into CommandInfo to avoid adding a dependency on Connect Client
     */
    private PackageInfo newPackageInfo(CommandInfo cmdInfo, Package pkg) {
        PackageInfo packageInfo = new PackageInfo(pkg);
        cmdInfo.packages.add(packageInfo);
        return packageInfo;
    }

    /**
     * @param packages List of packages identified by their ID, name or local filename.
     * @since 5.7
     */
    public boolean pkgShow(List<String> packages) {
        boolean cmdOk = true;
        if (packages == null || packages.isEmpty()) {
            return cmdOk;
        }
        StringBuilder sb = new StringBuilder();
        sb.append("****************************************");
        for (String pkg : packages) {
            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_SHOW);
            cmdInfo.param = pkg;
            try {
                PackageInfo packageInfo = newPackageInfo(cmdInfo, findPackage(pkg));
                sb.append("\nPackage: " + packageInfo.id);
                sb.append("\nState: " + packageInfo.state);
                sb.append("\nVersion: " + packageInfo.version);
                sb.append("\nName: " + packageInfo.name);
                sb.append("\nType: " + packageInfo.type);
                sb.append("\nVisibility: " + packageInfo.visibility);
                if (packageInfo.state == PackageState.REMOTE && packageInfo.type != PackageType.STUDIO
                        && packageInfo.visibility != PackageVisibility.PUBLIC
                        && !LogicalInstanceIdentifier.isRegistered()) {
                    sb.append(" (registration required)");
                }
                sb.append("\nTarget platforms: " + ArrayUtils.toString(packageInfo.targetPlatforms));
                appendIfNotEmpty(sb, "\nVendor: ", packageInfo.vendor);
                sb.append("\nSupports hot-reload: " + packageInfo.supportsHotReload);
                sb.append("\nSupported: " + packageInfo.supported);
                sb.append("\nProduction state: " + packageInfo.productionState);
                sb.append("\nValidation state: " + packageInfo.validationState);
                appendIfNotEmpty(sb, "\nProvides: ", packageInfo.provides);
                appendIfNotEmpty(sb, "\nDepends: ", packageInfo.dependencies);
                appendIfNotEmpty(sb, "\nConflicts: ", packageInfo.conflicts);
                appendIfNotEmpty(sb, "\nTitle: ", packageInfo.title);
                appendIfNotEmpty(sb, "\nDescription: ", packageInfo.description);
                appendIfNotEmpty(sb, "\nHomepage: ", packageInfo.homePage);
                appendIfNotEmpty(sb, "\nLicense: ", packageInfo.licenseType);
                appendIfNotEmpty(sb, "\nLicense URL: ", packageInfo.licenseUrl);
                sb.append("\n****************************************");
            } catch (PackageException e) {
                cmdOk = false;
                cmdInfo.exitCode = 1;
                cmdInfo.newMessage(e);
            }
        }
        log.info(sb.toString());
        return cmdOk;
    }

    private void appendIfNotEmpty(StringBuilder sb, String label, Object[] array) {
        if (ArrayUtils.isNotEmpty(array)) {
            sb.append(label + ArrayUtils.toString(array));
        }
    }

    private void appendIfNotEmpty(StringBuilder sb, String label, String value) {
        if (StringUtils.isNotEmpty(value)) {
            sb.append(label + value);
        }
    }

    /**
     * Looks for a package. First look if it's a local ZIP file, second if it's a local package and finally if it's a
     * remote package.
     *
     * @param pkg A ZIP filename or file path, or package ID or a package name.
     * @return The first package found matching the given string.
     * @throws PackageException If no package is found or if an issue occurred while searching.
     * @see PackageDefinition
     * @see LocalPackage
     * @see DownloadablePackage
     */
    protected Package findPackage(String pkg) throws PackageException {
        // Is it a local ZIP file?
        File localPackageFile = getLocalPackageFile(pkg);
        if (localPackageFile != null) {
            return service.loadPackageFromZip(localPackageFile);
        }

        // Is it a local package ID or name?
        LocalPackage localPackage = getLocalPackage(pkg);
        if (localPackage != null) {
            return localPackage;
        }

        // Is it a remote package ID or name?
        String pkgId = getRemotePackageId(pkg);
        if (pkgId != null) {
            return getPackageManager().findPackageById(pkgId);
        }

        throw new PackageException("Could not find a remote or local (relative to "
                + "current directory or to NUXEO_HOME) " + "package with name or ID " + pkg);
    }

    /**
     * @since 5.9.1
     */
    public void setAllowSNAPSHOT(boolean allow) {
        CUDFHelper.defaultAllowSNAPSHOT = allow;
        allowSNAPSHOT = allow;
    }

}