001/*
002 * (C) Copyright 2012-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Julien Carsique
018 *     Mathieu Guillaume
019 *     Yannis JULIENNE
020 *
021 */
022
023package org.nuxeo.launcher.connect;
024
025import java.io.Console;
026import java.io.File;
027import java.io.IOException;
028import java.io.InputStream;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.List;
034import java.util.Map;
035import java.util.Set;
036import java.util.SortedMap;
037import java.util.TreeMap;
038import java.util.zip.ZipEntry;
039import java.util.zip.ZipException;
040import java.util.zip.ZipFile;
041
042import javax.xml.parsers.DocumentBuilder;
043import javax.xml.parsers.DocumentBuilderFactory;
044import javax.xml.xpath.XPath;
045import javax.xml.xpath.XPathConstants;
046import javax.xml.xpath.XPathExpression;
047import javax.xml.xpath.XPathFactory;
048
049import org.apache.commons.collections.CollectionUtils;
050import org.apache.commons.collections.ListUtils;
051import org.apache.commons.io.FileUtils;
052import org.apache.commons.lang.ArrayUtils;
053import org.apache.commons.lang.StringUtils;
054import org.apache.commons.logging.Log;
055import org.apache.commons.logging.LogFactory;
056import org.apache.commons.logging.impl.SimpleLog;
057import org.nuxeo.common.Environment;
058import org.nuxeo.connect.CallbackHolder;
059import org.nuxeo.connect.NuxeoConnectClient;
060import org.nuxeo.connect.connector.ConnectServerError;
061import org.nuxeo.connect.data.DownloadablePackage;
062import org.nuxeo.connect.data.DownloadingPackage;
063import org.nuxeo.connect.identity.LogicalInstanceIdentifier;
064import org.nuxeo.connect.identity.LogicalInstanceIdentifier.InvalidCLID;
065import org.nuxeo.connect.identity.LogicalInstanceIdentifier.NoCLID;
066import org.nuxeo.connect.packages.PackageManager;
067import org.nuxeo.connect.packages.dependencies.CUDFHelper;
068import org.nuxeo.connect.packages.dependencies.DependencyResolution;
069import org.nuxeo.connect.update.LocalPackage;
070import org.nuxeo.connect.update.Package;
071import org.nuxeo.connect.update.PackageException;
072import org.nuxeo.connect.update.PackageState;
073import org.nuxeo.connect.update.PackageType;
074import org.nuxeo.connect.update.PackageUtils;
075import org.nuxeo.connect.update.PackageVisibility;
076import org.nuxeo.connect.update.ValidationStatus;
077import org.nuxeo.connect.update.Version;
078import org.nuxeo.connect.update.model.PackageDefinition;
079import org.nuxeo.connect.update.standalone.StandaloneUpdateService;
080import org.nuxeo.connect.update.task.Task;
081import org.nuxeo.launcher.info.CommandInfo;
082import org.nuxeo.launcher.info.CommandSetInfo;
083import org.nuxeo.launcher.info.PackageInfo;
084import org.w3c.dom.Document;
085import org.w3c.dom.NodeList;
086
087/**
088 * @since 5.6
089 */
090public class ConnectBroker {
091
092    private static final Log log = LogFactory.getLog(ConnectBroker.class);
093
094    public static final String PARAM_MP_DIR = "nuxeo.distribution.marketplace.dir";
095
096    public static final String DISTRIBUTION_MP_DIR_DEFAULT = "setupWizardDownloads";
097
098    public static final String PACKAGES_XML = "packages.xml";
099
100    protected static final String LAUNCHER_CHANGED_PROPERTY = "launcher.changed";
101
102    protected static final int LAUNCHER_CHANGED_EXIT_CODE = 128;
103
104    public static final String[] POSITIVE_ANSWERS = { "true", "yes", "y" };
105
106    private Environment env;
107
108    private StandaloneUpdateService service;
109
110    private CallbackHolder cbHolder;
111
112    private CommandSetInfo cset = new CommandSetInfo();
113
114    private String targetPlatform;
115
116    private String distributionMPDir;
117
118    private String relax = OPTION_RELAX_DEFAULT;
119
120    public static final String OPTION_RELAX_DEFAULT = "ask";
121
122    private String accept = OPTION_ACCEPT_DEFAULT;
123
124    private boolean allowSNAPSHOT = CUDFHelper.defaultAllowSNAPSHOT;
125
126    public static final String OPTION_ACCEPT_DEFAULT = "ask";
127
128    public ConnectBroker(Environment env) throws IOException, PackageException {
129        this.env = env;
130        service = new StandaloneUpdateService(env);
131        service.initialize();
132        cbHolder = new StandaloneCallbackHolder(env, service);
133        NuxeoConnectClient.setCallBackHolder(cbHolder);
134        targetPlatform = env.getProperty(Environment.DISTRIBUTION_NAME) + "-"
135                + env.getProperty(Environment.DISTRIBUTION_VERSION);
136        distributionMPDir = env.getProperty(PARAM_MP_DIR, DISTRIBUTION_MP_DIR_DEFAULT);
137    }
138
139    public String getCLID() throws NoCLID {
140        return LogicalInstanceIdentifier.instance().getCLID();
141    }
142
143    /**
144     * @throws NoCLID
145     * @since 6.0
146     */
147    public void setCLID(String file) throws NoCLID {
148        try {
149            LogicalInstanceIdentifier.load(file);
150        } catch (IOException | InvalidCLID e) {
151            throw new NoCLID("can not load CLID", e);
152        }
153    }
154
155    /**
156     * @since 8.10-HF15
157     */
158    public void saveCLID() throws IOException, NoCLID {
159        LogicalInstanceIdentifier.instance().save();
160    }
161
162    public StandaloneUpdateService getUpdateService() {
163        return service;
164    }
165
166    public PackageManager getPackageManager() {
167        return NuxeoConnectClient.getPackageManager();
168    }
169
170    public void refreshCache() {
171        getPackageManager().flushCache();
172        NuxeoConnectClient.getPackageManager().listAllPackages();
173    }
174
175    public CommandSetInfo getCommandSet() {
176        return cset;
177    }
178
179    protected LocalPackage getInstalledPackageByName(String pkgName) {
180        try {
181            return service.getPersistence().getActivePackage(pkgName);
182        } catch (PackageException e) {
183            log.error(e);
184            return null;
185        }
186    }
187
188    protected boolean isInstalledPackage(String pkgName) {
189        try {
190            return service.getPersistence().getActivePackageId(pkgName) != null;
191        } catch (PackageException e) {
192            log.error("Error checking installation of package " + pkgName, e);
193            return false;
194        }
195    }
196
197    protected boolean isLocalPackageId(String pkgId) {
198        try {
199            return service.getPackage(pkgId) != null;
200        } catch (PackageException e) {
201            log.error("Error looking for local package " + pkgId, e);
202            return false;
203        }
204    }
205
206    protected boolean isRemotePackageId(String pkgId) {
207        return PackageUtils.isValidPackageId(pkgId)
208                && NuxeoConnectClient.getPackageManager().getRemotePackage(pkgId) != null;
209    }
210
211    protected String getBestIdForNameInList(String pkgName, List<? extends Package> pkgList) {
212        String foundId = null;
213        SortedMap<Version, String> foundPkgs = new TreeMap<>();
214        SortedMap<Version, String> matchingPkgs = new TreeMap<>();
215        for (Package pkg : pkgList) {
216            if (pkg.getName().equals(pkgName)) {
217                foundPkgs.put(pkg.getVersion(), pkg.getId());
218                if (Arrays.asList(pkg.getTargetPlatforms()).contains(targetPlatform)) {
219                    matchingPkgs.put(pkg.getVersion(), pkg.getId());
220                }
221            }
222        }
223        if (matchingPkgs.size() != 0) {
224            foundId = matchingPkgs.get(matchingPkgs.lastKey());
225        } else if (foundPkgs.size() != 0) {
226            foundId = foundPkgs.get(foundPkgs.lastKey());
227        }
228        return foundId;
229    }
230
231    protected String getLocalPackageIdFromName(String pkgName) {
232        return getBestIdForNameInList(pkgName, getPkgList());
233    }
234
235    protected List<String> getAllLocalPackageIdsFromName(String pkgName) {
236        List<String> foundIds = new ArrayList<>();
237        for (Package pkg : getPkgList()) {
238            if (pkg.getName().equals(pkgName)) {
239                foundIds.add(pkg.getId());
240            }
241        }
242        return foundIds;
243    }
244
245    protected String getInstalledPackageIdFromName(String pkgName) {
246        List<LocalPackage> localPackages = getPkgList();
247        List<LocalPackage> installedPackages = new ArrayList<>();
248        for (LocalPackage pkg : localPackages) {
249            if (pkg.getPackageState().isInstalled()) {
250                installedPackages.add(pkg);
251            }
252        }
253        return getBestIdForNameInList(pkgName, installedPackages);
254    }
255
256    protected String getRemotePackageIdFromName(String pkgName) {
257        return getBestIdForNameInList(pkgName, NuxeoConnectClient.getPackageManager().findRemotePackages(pkgName));
258    }
259
260    /**
261     * Looks for a remote package from its name or id
262     *
263     * @param pkgNameOrId
264     * @return the remote package Id; null if not found
265     * @since 5.7
266     */
267    protected String getRemotePackageId(String pkgNameOrId) {
268        String pkgId = null;
269        if (isRemotePackageId(pkgNameOrId)) {
270            pkgId = pkgNameOrId;
271        } else {
272            pkgId = getRemotePackageIdFromName(pkgNameOrId);
273        }
274        return pkgId;
275    }
276
277    /**
278     * Looks for a local package from its name or id
279     *
280     * @since 5.7
281     * @param pkgIdOrName
282     * @return the local package Id; null if not found
283     * @throws PackageException
284     */
285    protected LocalPackage getLocalPackage(String pkgIdOrName) throws PackageException {
286        // Try as a package id
287        LocalPackage pkg = service.getPackage(pkgIdOrName);
288        if (pkg == null) {
289            // Check whether this is the name of a local package
290            String pkgId = getLocalPackageIdFromName(pkgIdOrName);
291            if (pkgId != null) {
292                pkg = service.getPackage(pkgId);
293            }
294        }
295        return pkg;
296    }
297
298    /**
299     * Looks for a package file from its path
300     *
301     * @param pkgFile Absolute or relative package file path
302     * @return the file if found, else null
303     */
304    protected File getLocalPackageFile(String pkgFile) {
305        if (pkgFile.startsWith("file:")) {
306            pkgFile = pkgFile.substring(5);
307        }
308        // Try absolute path
309        File fileToCheck = new File(pkgFile);
310        if (!fileToCheck.exists()) { // Try relative path
311            fileToCheck = new File(env.getServerHome(), pkgFile);
312        }
313        if (fileToCheck.exists()) {
314            return fileToCheck;
315        } else {
316            return null;
317        }
318    }
319
320    /**
321     * Load package definition from a local file or directory and get package Id from it.
322     *
323     * @return null the package definition cannot be loaded for any reason.
324     * @since 8.4
325     */
326    protected String getLocalPackageFileId(File pkgFile) {
327        PackageDefinition packageDefinition = null;
328        try {
329            if (pkgFile.isFile()) {
330                packageDefinition = service.loadPackageFromZip(pkgFile);
331            } else if (pkgFile.isDirectory()) {
332                File manifest = new File(pkgFile, LocalPackage.MANIFEST);
333                packageDefinition = service.loadPackage(manifest);
334            } else {
335                throw new PackageException("Unknown file type (not a file and not a directory) for " + pkgFile);
336            }
337        } catch (PackageException e) {
338            log.error("Error trying to load package id from " + pkgFile, e);
339            return null;
340        }
341        return packageDefinition == null ? null : packageDefinition.getId();
342    }
343
344    protected boolean isLocalPackageFile(String pkgFile) {
345        return (getLocalPackageFile(pkgFile) != null);
346    }
347
348    protected List<String> getDistributionFilenames() {
349        File distributionMPFile = new File(distributionMPDir, PACKAGES_XML);
350        List<String> md5Filenames = new ArrayList<>();
351        // Try to get md5 files from packages.xml
352        DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
353        docFactory.setNamespaceAware(true);
354        try {
355            DocumentBuilder builder = docFactory.newDocumentBuilder();
356            Document doc = builder.parse(distributionMPFile);
357            XPathFactory xpFactory = XPathFactory.newInstance();
358            XPath xpath = xpFactory.newXPath();
359            XPathExpression expr = xpath.compile("//package/@md5");
360            NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
361            for (int i = 0; i < nodes.getLength(); i++) {
362                String md5 = nodes.item(i).getNodeValue();
363                if ((md5 != null) && (md5.length() > 0)) {
364                    md5Filenames.add(md5);
365                }
366            }
367        } catch (Exception e) {
368            // Parsing failed - return empty list
369            log.error("Failed parsing " + distributionMPFile, e);
370            return new ArrayList<>();
371        }
372        return md5Filenames;
373    }
374
375    protected Map<String, PackageDefinition> getDistributionDefinitions(List<String> md5Filenames) {
376        Map<String, PackageDefinition> allDefinitions = new HashMap<>();
377        if (md5Filenames == null) {
378            return allDefinitions;
379        }
380        for (String md5Filename : md5Filenames) {
381            File md5File = new File(distributionMPDir, md5Filename);
382            if (!md5File.exists()) {
383                // distribution file has been deleted
384                continue;
385            }
386            ZipFile zipFile;
387            try {
388                zipFile = new ZipFile(md5File);
389            } catch (ZipException e) {
390                log.warn("Unzip error reading file " + md5File, e);
391                continue;
392            } catch (IOException e) {
393                log.warn("Could not read file " + md5File, e);
394                continue;
395            }
396            try {
397                ZipEntry zipEntry = zipFile.getEntry("package.xml");
398                InputStream in = zipFile.getInputStream(zipEntry);
399                PackageDefinition pd = NuxeoConnectClient.getPackageUpdateService().loadPackage(in);
400                allDefinitions.put(md5Filename, pd);
401            } catch (Exception e) {
402                log.error("Could not read package description", e);
403                continue;
404            } finally {
405                try {
406                    zipFile.close();
407                } catch (IOException e) {
408                    log.warn("Unexpected error closing file " + md5File, e);
409                }
410            }
411        }
412        return allDefinitions;
413    }
414
415    protected boolean addDistributionPackage(String md5) {
416        boolean ret = true;
417        File distributionFile = new File(distributionMPDir, md5);
418        if (distributionFile.exists()) {
419            try {
420                ret = pkgAdd(distributionFile.getCanonicalPath(), false) != null;
421            } catch (IOException e) {
422                log.warn("Could not add distribution file " + md5);
423                ret = false;
424            }
425        }
426        return ret;
427    }
428
429    public boolean addDistributionPackages() {
430        Map<String, PackageDefinition> distributionPackages = getDistributionDefinitions(getDistributionFilenames());
431        if (distributionPackages.isEmpty()) {
432            return true;
433        }
434        List<LocalPackage> localPackages = getPkgList();
435        Map<String, LocalPackage> localPackagesById = new HashMap<>();
436        if (localPackages != null) {
437            for (LocalPackage pkg : localPackages) {
438                localPackagesById.put(pkg.getId(), pkg);
439            }
440        }
441        boolean ret = true;
442        for (String md5 : distributionPackages.keySet()) {
443            PackageDefinition md5Pkg = distributionPackages.get(md5);
444            if (localPackagesById.containsKey(md5Pkg.getId())) {
445                // We have the same package Id in the local cache
446                LocalPackage localPackage = localPackagesById.get(md5Pkg.getId());
447                if (localPackage.getVersion().isSnapshot()) {
448                    // - For snapshots, until we have timestamp support, assume
449                    // distribution version is newer than cached version.
450                    // - This may (will) break the server if there are
451                    // dependencies/compatibility changes or if the package is
452                    // in installed state.
453                    if (!localPackage.getPackageState().isInstalled()) {
454                        pkgRemove(localPackage.getId());
455                        ret = addDistributionPackage(md5) && ret;
456                    }
457                }
458            } else {
459                // No package with this Id is in cache
460                ret = addDistributionPackage(md5) && ret;
461            }
462        }
463        return ret;
464    }
465
466    public List<LocalPackage> getPkgList() {
467        try {
468            return service.getPackages();
469        } catch (PackageException e) {
470            log.error("Could not read package list", e);
471            return null;
472        }
473    }
474
475    public void pkgList() {
476        log.info("Local packages:");
477        pkgList(getPkgList());
478    }
479
480    public void pkgListAll() {
481        log.info("All packages:");
482        pkgList(NuxeoConnectClient.getPackageManager().listAllPackages());
483    }
484
485    public void pkgList(List<? extends Package> packagesList) {
486        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_LIST);
487        try {
488            if (packagesList.isEmpty()) {
489                log.info("None");
490            } else {
491                NuxeoConnectClient.getPackageManager().sort(packagesList);
492                StringBuilder sb = new StringBuilder();
493                for (Package pkg : packagesList) {
494                    newPackageInfo(cmdInfo, pkg);
495                    PackageState packageState = pkg.getPackageState();
496                    String packageDescription = packageState.getLabel();
497                    packageDescription = String.format("%6s %11s\t", pkg.getType(), packageDescription);
498                    if (packageState == PackageState.REMOTE && pkg.getType() != PackageType.STUDIO
499                            && pkg.getVisibility() != PackageVisibility.PUBLIC
500                            && !LogicalInstanceIdentifier.isRegistered()) {
501                        packageDescription += "Registration required for ";
502                    }
503                    packageDescription += String.format("%s (id: %s)\n", pkg.getName(), pkg.getId());
504                    sb.append(packageDescription);
505                }
506                log.info(sb.toString());
507            }
508        } catch (Exception e) {
509            log.error(e);
510            cmdInfo.exitCode = 1;
511        }
512    }
513
514    protected void performTask(Task task) throws PackageException {
515        ValidationStatus validationStatus = task.validate();
516        if (validationStatus.hasErrors()) {
517            throw new PackageException(
518                    "Failed to validate package " + task.getPackage().getId() + " -> " + validationStatus.getErrors());
519        }
520        if (validationStatus.hasWarnings()) {
521            log.warn("Got warnings on package validation " + task.getPackage().getId() + " -> "
522                    + validationStatus.getWarnings());
523        }
524        task.run(null);
525    }
526
527    public boolean pkgReset() {
528        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_RESET);
529        if ("ask".equalsIgnoreCase(accept)) {
530            accept = readConsole(
531                    "The reset will erase the Nuxeo Packages history.\n" + "Do you want to continue (yes/no)? [yes] ",
532                    "yes");
533        }
534        if (!Boolean.parseBoolean(accept)) {
535            cmdInfo.exitCode = 1;
536            return false;
537        }
538        try {
539            service.reset();
540            log.info("Packages reset done: all packages were marked as DOWNLOADED");
541            List<LocalPackage> localPackages = service.getPackages();
542            for (LocalPackage localPackage : localPackages) {
543                localPackage.getUninstallFile().delete();
544                FileUtils.deleteDirectory(localPackage.getData().getEntry(LocalPackage.BACKUP_DIR));
545                newPackageInfo(cmdInfo, localPackage);
546            }
547            service.getRegistry().delete();
548            FileUtils.deleteDirectory(service.getBackupDir());
549        } catch (PackageException e) {
550            log.error(e);
551            cmdInfo.exitCode = 1;
552        } catch (IOException e) {
553            log.error(e);
554            cmdInfo.exitCode = 1;
555        }
556        return cmdInfo.exitCode == 0;
557    }
558
559    public boolean pkgPurge() throws PackageException {
560        List<String> localNames = new ArrayList<>();
561        // Remove packages in DOWNLOADED state first
562        // This will avoid extending the CUDF universe needlessly
563        for (LocalPackage pkg : service.getPackages()) {
564            if (pkg.getPackageState() == PackageState.DOWNLOADED) {
565                pkgRemove(pkg.getId());
566            }
567        }
568        // Process the remaining packages
569        for (LocalPackage pkg : service.getPackages()) {
570            localNames.add(pkg.getName());
571        }
572        return pkgRequest(null, null, null, localNames, true, false);
573    }
574
575    /**
576     * Uninstall a list of packages. If the list contains a package name (versus an ID), only the considered as best
577     * matching package is uninstalled.
578     *
579     * @param packageIdsToRemove The list can contain package IDs and names
580     * @see #pkgUninstall(String)
581     */
582    public boolean pkgUninstall(List<String> packageIdsToRemove) {
583        log.debug("Uninstalling: " + packageIdsToRemove);
584        for (String pkgId : packageIdsToRemove) {
585            if (pkgUninstall(pkgId) == null) {
586                log.error("Unable to uninstall " + pkgId);
587                return false;
588            }
589        }
590        return true;
591    }
592
593    /**
594     * Uninstall a local package. The package is not removed from cache.
595     *
596     * @param pkgId Package ID or Name
597     * @return The uninstalled LocalPackage or null if failed
598     */
599    public LocalPackage pkgUninstall(String pkgId) {
600        if (env.getProperty(LAUNCHER_CHANGED_PROPERTY, "false").equals("true")) {
601            System.exit(LAUNCHER_CHANGED_EXIT_CODE);
602        }
603        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_UNINSTALL);
604        cmdInfo.param = pkgId;
605        try {
606            LocalPackage pkg = service.getPackage(pkgId);
607            if (pkg == null) {
608                // Check whether this is the name of an installed package
609                String realPkgId = getInstalledPackageIdFromName(pkgId);
610                if (realPkgId != null) {
611                    pkgId = realPkgId;
612                    pkg = service.getPackage(realPkgId);
613                }
614            }
615            if (pkg == null) {
616                throw new PackageException("Package not found: " + pkgId);
617            }
618            log.info("Uninstalling " + pkgId);
619            Task uninstallTask = pkg.getUninstallTask();
620            try {
621                performTask(uninstallTask);
622            } catch (PackageException e) {
623                uninstallTask.rollback();
624                throw e;
625            }
626            // Refresh state
627            pkg = service.getPackage(pkgId);
628            newPackageInfo(cmdInfo, pkg);
629            return pkg;
630        } catch (Exception e) {
631            log.error("Failed to uninstall package: " + pkgId, e);
632            cmdInfo.exitCode = 1;
633            return null;
634        }
635    }
636
637    /**
638     * Remove a list of packages from cache. If the list contains a package name (versus an ID), all matching packages
639     * are removed.
640     *
641     * @param pkgsToRemove The list can contain package IDs and names
642     * @see #pkgRemove(String)
643     */
644    public boolean pkgRemove(List<String> pkgsToRemove) {
645        boolean cmdOk = true;
646        if (pkgsToRemove != null) {
647            log.debug("Removing: " + pkgsToRemove);
648            for (String pkgNameOrId : pkgsToRemove) {
649                List<String> allIds;
650                if (isLocalPackageId(pkgNameOrId)) {
651                    allIds = new ArrayList<>();
652                    allIds.add(pkgNameOrId);
653                } else {
654                    // Request made on a name: remove all matching packages
655                    allIds = getAllLocalPackageIdsFromName(pkgNameOrId);
656                }
657                for (String pkgId : allIds) {
658                    if (pkgRemove(pkgId) == null) {
659                        log.warn("Unable to remove " + pkgId);
660                        // Don't error out on failed (cache) removal
661                        cmdOk = false;
662                    }
663                }
664            }
665        }
666        return cmdOk;
667    }
668
669    /**
670     * Remove a package from cache. If it was installed, the package is uninstalled then removed.
671     *
672     * @param pkgId Package ID or Name
673     * @return The removed LocalPackage or null if failed
674     */
675    public LocalPackage pkgRemove(String pkgId) {
676        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_REMOVE);
677        cmdInfo.param = pkgId;
678        try {
679            LocalPackage pkg = service.getPackage(pkgId);
680            if (pkg == null) {
681                // Check whether this is the name of a local package
682                String realPkgId = getLocalPackageIdFromName(pkgId);
683                if (realPkgId != null) {
684                    pkgId = realPkgId;
685                    pkg = service.getPackage(realPkgId);
686                }
687            }
688            if (pkg == null) {
689                throw new PackageException("Package not found: " + pkgId);
690            }
691            if (pkg.getPackageState().isInstalled()) {
692                pkgUninstall(pkgId);
693                // Refresh state
694                pkg = service.getPackage(pkgId);
695            }
696            if (pkg.getPackageState() != PackageState.DOWNLOADED) {
697                throw new PackageException("Can only remove packages in DOWNLOADED, INSTALLED or STARTED state");
698            }
699            service.removePackage(pkgId);
700            log.info("Removed " + pkgId);
701            newPackageInfo(cmdInfo, pkg).state = PackageState.REMOTE;
702            return pkg;
703        } catch (Exception e) {
704            log.error("Failed to remove package: " + pkgId, e);
705            cmdInfo.exitCode = 1;
706            return null;
707        }
708    }
709
710    /**
711     * Add a list of packages into the cache, downloading them if needed and possible.
712     *
713     * @param pkgsToAdd
714     * @return true if command succeeded
715     * @see #pkgAdd(List, boolean)
716     * @see #pkgAdd(String, boolean)
717     * @deprecated Since 7.10. Use a method with an explicit value for {@code ignoreMissing}.
718     */
719    @Deprecated
720    public boolean pkgAdd(List<String> pkgsToAdd) {
721        return pkgAdd(pkgsToAdd, false);
722    }
723
724    /**
725     * Add a list of packages into the cache, downloading them if needed and possible.
726     *
727     * @since 6.0
728     * @param pkgsToAdd
729     * @param ignoreMissing
730     * @return true if command succeeded
731     * @see #pkgAdd(String, boolean)
732     */
733    public boolean pkgAdd(List<String> pkgsToAdd, boolean ignoreMissing) {
734        boolean cmdOk = true;
735        if (pkgsToAdd == null || pkgsToAdd.isEmpty()) {
736            return cmdOk;
737        }
738        List<String> pkgIdsToDownload = new ArrayList<>();
739        for (String pkgToAdd : pkgsToAdd) {
740            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_ADD);
741            cmdInfo.param = pkgToAdd;
742            try {
743                File fileToAdd = getLocalPackageFile(pkgToAdd);
744                if (fileToAdd == null) {
745                    String pkgId = getRemotePackageId(pkgToAdd);
746                    if (pkgId == null) {
747                        if (ignoreMissing) {
748                            log.warn("Could not add package: " + pkgToAdd);
749                            cmdInfo.newMessage(SimpleLog.LOG_LEVEL_INFO, "Could not add package.");
750                        } else {
751                            throw new PackageException("Could not find a remote or local (relative to "
752                                    + "current directory or to NUXEO_HOME) " + "package with name or ID " + pkgToAdd);
753                        }
754                    } else {
755                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_INFO, "Waiting for download...");
756                        pkgIdsToDownload.add(pkgId);
757                    }
758                } else {
759                    LocalPackage pkg = service.addPackage(fileToAdd);
760                    log.info("Added " + pkg);
761                    newPackageInfo(cmdInfo, pkg);
762                }
763            } catch (PackageException e) {
764                cmdOk = false;
765                cmdInfo.exitCode = 1;
766                cmdInfo.newMessage(e);
767            }
768        }
769        cmdOk = downloadPackages(pkgIdsToDownload) && cmdOk;
770        return cmdOk;
771    }
772
773    /**
774     * Add a package file into the cache
775     *
776     * @param packageFileName
777     * @return The added LocalPackage or null if failed
778     * @see #pkgAdd(List, boolean)
779     * @see #pkgAdd(String, boolean)
780     * @deprecated Since 7.10. Use a method with an explicit value for {@code ignoreMissing}.
781     */
782    @Deprecated
783    public LocalPackage pkgAdd(String packageFileName) {
784        return pkgAdd(packageFileName, false);
785    }
786
787    /**
788     * Add a package file into the cache
789     *
790     * @since 6.0
791     * @param packageFileName
792     * @param ignoreMissing
793     * @return The added LocalPackage or null if failed
794     * @see #pkgAdd(List, boolean)
795     */
796    public LocalPackage pkgAdd(String packageFileName, boolean ignoreMissing) {
797        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_ADD);
798        cmdInfo.param = packageFileName;
799        LocalPackage pkg = null;
800        try {
801            File fileToAdd = getLocalPackageFile(packageFileName);
802            if (fileToAdd == null) {
803                String pkgId = getRemotePackageId(packageFileName);
804                if (pkgId == null) {
805                    if (ignoreMissing) {
806                        log.warn("Could not add package: " + packageFileName);
807                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_INFO, "Could not add package.");
808                        return null;
809                    } else {
810                        throw new PackageException("Could not find a remote or local (relative to "
811                                + "current directory or to NUXEO_HOME) " + "package with name or ID "
812                                + packageFileName);
813                    }
814                } else if (!downloadPackages(Arrays.asList(new String[] { pkgId }))) {
815                    throw new PackageException("Could not download package " + pkgId);
816                }
817                pkg = service.getPackage(pkgId);
818                if (pkg == null) {
819                    throw new PackageException("Could not find downloaded package in cache " + pkgId);
820                }
821            } else {
822                pkg = service.addPackage(fileToAdd);
823                log.info("Added " + packageFileName);
824            }
825            newPackageInfo(cmdInfo, pkg);
826        } catch (PackageException e) {
827            cmdInfo.exitCode = 1;
828            cmdInfo.newMessage(e);
829        }
830        return pkg;
831    }
832
833    /**
834     * Install a list of local packages. If the list contains a package name (versus an ID), only the considered as best
835     * matching package is installed.
836     *
837     * @param packageIdsToInstall The list can contain package IDs and names
838     * @see #pkgInstall(List, boolean)
839     * @see #pkgInstall(String, boolean)
840     * @deprecated Since 7.10. Use a method with an explicit value for {@code ignoreMissing}.
841     */
842    @Deprecated
843    public boolean pkgInstall(List<String> packageIdsToInstall) {
844        return pkgInstall(packageIdsToInstall, false);
845    }
846
847    /**
848     * Install a list of local packages. If the list contains a package name (versus an ID), only the considered as best
849     * matching package is installed.
850     *
851     * @since 6.0
852     * @param packageIdsToInstall The list can contain package IDs and names
853     * @param ignoreMissing If true, doesn't throw an exception on unknown packages
854     * @see #pkgInstall(String, boolean)
855     */
856    public boolean pkgInstall(List<String> packageIdsToInstall, boolean ignoreMissing) {
857        log.debug("Installing: " + packageIdsToInstall);
858        for (String pkgId : packageIdsToInstall) {
859            if (pkgInstall(pkgId, ignoreMissing) == null && !ignoreMissing) {
860                return false;
861            }
862        }
863        return true;
864    }
865
866    /**
867     * Install a local package.
868     *
869     * @param pkgId Package ID or Name
870     * @return The installed LocalPackage or null if failed
871     * @see #pkgInstall(List, boolean)
872     * @see #pkgInstall(String, boolean)
873     * @deprecated Since 7.10. Use a method with an explicit value for {@code ignoreMissing}.
874     */
875    @Deprecated
876    public LocalPackage pkgInstall(String pkgId) {
877        return pkgInstall(pkgId, false);
878    }
879
880    /**
881     * Install a local package.
882     *
883     * @since 6.0
884     * @param pkgId Package ID or Name
885     * @param ignoreMissing If true, doesn't throw an exception on unknown packages
886     * @return The installed LocalPackage or null if failed
887     * @see #pkgInstall(List, boolean)
888     */
889    public LocalPackage pkgInstall(String pkgId, boolean ignoreMissing) {
890        if (env.getProperty(LAUNCHER_CHANGED_PROPERTY, "false").equals("true")) {
891            System.exit(LAUNCHER_CHANGED_EXIT_CODE);
892        }
893        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_INSTALL);
894        cmdInfo.param = pkgId;
895        try {
896            LocalPackage pkg = getLocalPackage(pkgId);
897            if (pkg != null && pkg.getPackageState().isInstalled()) {
898                if (pkg.getVersion().isSnapshot()) {
899                    log.info(String.format("Updating package %s...", pkg));
900                    // First remove it to allow SNAPSHOT upgrade
901                    pkgRemove(pkgId);
902                    pkg = null;
903                } else {
904                    log.info(String.format("Package %s is already installed.", pkg));
905                    return pkg;
906                }
907            }
908            if (pkg == null) {
909                // We don't know this package, try to add it first
910                pkg = pkgAdd(pkgId, ignoreMissing);
911            }
912            if (pkg == null) {
913                // Nothing worked - can't find the package anywhere
914                if (ignoreMissing) {
915                    log.warn("Unable to install package: " + pkgId);
916                    return null;
917                } else {
918                    throw new PackageException("Package not found: " + pkgId);
919                }
920            }
921            pkgId = pkg.getId();
922            cmdInfo.param = pkgId;
923            log.info("Installing " + pkgId);
924            Task installTask = pkg.getInstallTask();
925            try {
926                performTask(installTask);
927            } catch (PackageException e) {
928                installTask.rollback();
929                throw e;
930            }
931            // Refresh state
932            pkg = service.getPackage(pkgId);
933            newPackageInfo(cmdInfo, pkg);
934            return pkg;
935        } catch (PackageException e) {
936            log.error(String.format("Failed to install package: %s (%s)", pkgId, e.getMessage()));
937            log.debug(e, e);
938            cmdInfo.exitCode = 1;
939            cmdInfo.newMessage(e);
940            return null;
941        }
942    }
943
944    public boolean listPending(File commandsFile) {
945        return executePending(commandsFile, false, false, false);
946    }
947
948    /**
949     * @since 5.6
950     * @param commandsFile File containing the commands to execute
951     * @param doExecute Whether to execute or list the actions
952     * @param useResolver Whether to use full resolution or just execute individual actions
953     */
954    public boolean executePending(File commandsFile, boolean doExecute, boolean useResolver, boolean ignoreMissing) {
955        int errorValue = 0;
956        if (!commandsFile.isFile()) {
957            return false;
958        }
959        List<String> pkgsToAdd = new ArrayList<>();
960        List<String> pkgsToInstall = new ArrayList<>();
961        List<String> pkgsToUninstall = new ArrayList<>();
962        List<String> pkgsToRemove = new ArrayList<>();
963        List<String> lines;
964        try {
965            lines = FileUtils.readLines(commandsFile);
966            for (String line : lines) {
967                line = line.trim();
968                String[] split = line.split("\\s+", 2);
969                if (split.length == 2) {
970                    if (split[0].equals(CommandInfo.CMD_INSTALL)) {
971                        if (doExecute) {
972                            if (useResolver) {
973                                pkgsToInstall.add(split[1]);
974                            } else {
975                                pkgInstall(split[1], ignoreMissing);
976                            }
977                        } else {
978                            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_INSTALL);
979                            cmdInfo.param = split[1];
980                            cmdInfo.pending = true;
981                        }
982                    } else if (split[0].equals(CommandInfo.CMD_ADD)) {
983                        if (doExecute) {
984                            if (useResolver) {
985                                pkgsToAdd.add(split[1]);
986                            } else {
987                                pkgAdd(split[1], ignoreMissing);
988                            }
989                        } else {
990                            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_ADD);
991                            cmdInfo.param = split[1];
992                            cmdInfo.pending = true;
993                        }
994                    } else if (split[0].equals(CommandInfo.CMD_UNINSTALL)) {
995                        if (doExecute) {
996                            if (useResolver) {
997                                pkgsToUninstall.add(split[1]);
998                            } else {
999                                pkgUninstall(split[1]);
1000                            }
1001                        } else {
1002                            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_UNINSTALL);
1003                            cmdInfo.param = split[1];
1004                            cmdInfo.pending = true;
1005                        }
1006                    } else if (split[0].equals(CommandInfo.CMD_REMOVE)) {
1007                        if (doExecute) {
1008                            if (useResolver) {
1009                                pkgsToRemove.add(split[1]);
1010                            } else {
1011                                pkgRemove(split[1]);
1012                            }
1013                        } else {
1014                            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_REMOVE);
1015                            cmdInfo.param = split[1];
1016                            cmdInfo.pending = true;
1017                        }
1018                    } else {
1019                        errorValue = 1;
1020                    }
1021                } else if (split.length == 1) {
1022                    if (line.length() > 0 && !line.startsWith("#")) {
1023                        if (doExecute) {
1024                            if ("init".equals(line)) {
1025                                if (!addDistributionPackages()) {
1026                                    errorValue = 1;
1027                                }
1028                            } else {
1029                                if (useResolver) {
1030                                    pkgsToInstall.add(line);
1031                                } else {
1032                                    pkgInstall(line, ignoreMissing);
1033                                }
1034                            }
1035                        } else {
1036                            if ("init".equals(line)) {
1037                                CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_INIT);
1038                                cmdInfo.pending = true;
1039                            } else {
1040                                CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_INSTALL);
1041                                cmdInfo.param = line;
1042                                cmdInfo.pending = true;
1043                            }
1044                        }
1045                    }
1046                }
1047                if (errorValue != 0) {
1048                    log.error("Error processing pending package/command: " + line);
1049                }
1050            }
1051            if (doExecute) {
1052                if (useResolver) {
1053                    String oldAccept = accept;
1054                    String oldRelax = relax;
1055                    accept = "true";
1056                    if ("ask".equalsIgnoreCase(relax)) {
1057                        log.info("Relax mode changed from 'ask' to 'false' for executing the pending actions.");
1058                        relax = "false";
1059                    }
1060                    boolean success = pkgRequest(pkgsToAdd, pkgsToInstall, pkgsToUninstall, pkgsToRemove, true,
1061                            ignoreMissing);
1062                    accept = oldAccept;
1063                    relax = oldRelax;
1064                    if (!success) {
1065                        errorValue = 2;
1066                    }
1067                }
1068                if (errorValue != 0) {
1069                    File bak = new File(commandsFile.getPath() + ".bak");
1070                    bak.delete();
1071                    commandsFile.renameTo(bak);
1072                    log.error("Pending actions execution failed. The commands file has been moved to: " + bak);
1073                } else {
1074                    commandsFile.delete();
1075                }
1076            } else {
1077                cset.log(true);
1078            }
1079        } catch (IOException e) {
1080            log.error(e.getMessage());
1081        }
1082        return errorValue == 0;
1083    }
1084
1085    @SuppressWarnings("unused")
1086    protected boolean downloadPackages(List<String> packagesToDownload) {
1087        boolean isRegistered = LogicalInstanceIdentifier.isRegistered();
1088        List<String> packagesAlreadyDownloaded = new ArrayList<String>();
1089        for (String pkg : packagesToDownload) {
1090            LocalPackage localPackage;
1091            try {
1092                localPackage = getLocalPackage(pkg);
1093            } catch (PackageException e) {
1094                log.error(String.format("Looking for package '%s' in local cache raised an error. Aborting.", pkg), e);
1095                return false;
1096            }
1097            if (localPackage == null) {
1098                continue;
1099            }
1100            if (localPackage.getPackageState().isInstalled()) {
1101                log.error(String.format("Package '%s' is installed. Download skipped.", pkg));
1102                packagesAlreadyDownloaded.add(pkg);
1103            } else if (localPackage.getVersion().isSnapshot()) {
1104                if (localPackage.getVisibility() != PackageVisibility.PUBLIC && !isRegistered) {
1105                    log.info(String.format("Update of '%s' requires being registered.", pkg));
1106                    packagesAlreadyDownloaded.add(pkg);
1107                } else {
1108                    log.info(String.format("Download of '%s' will replace the one already in local cache.", pkg));
1109                }
1110            } else {
1111                log.info(String.format("Package '%s' is already in local cache.", pkg));
1112                packagesAlreadyDownloaded.add(pkg);
1113            }
1114        }
1115
1116        packagesToDownload.removeAll(packagesAlreadyDownloaded);
1117        if (packagesToDownload.isEmpty()) {
1118            return true;
1119        }
1120        // Queue downloads
1121        log.info("Downloading " + packagesToDownload + "...");
1122        boolean downloadOk = true;
1123        List<DownloadingPackage> pkgs = new ArrayList<DownloadingPackage>();
1124        for (String pkg : packagesToDownload) {
1125            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_DOWNLOAD);
1126            cmdInfo.param = pkg;
1127
1128            // Check registration and package visibility
1129            DownloadablePackage downloadablePkg = getPackageManager().findRemotePackageById(pkg);
1130            if (downloadablePkg != null && downloadablePkg.getVisibility() != PackageVisibility.PUBLIC
1131                    && !isRegistered) {
1132                downloadOk = false;
1133                cmdInfo.exitCode = 1;
1134                cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Registration required.");
1135                continue;
1136            }
1137
1138            // Download
1139            try {
1140                DownloadingPackage download = getPackageManager().download(pkg);
1141                if (download != null) {
1142                    pkgs.add(download);
1143                    cmdInfo.param = download.getId();
1144                    cmdInfo.newMessage(SimpleLog.LOG_LEVEL_DEBUG, "Downloading...");
1145                } else {
1146                    downloadOk = false;
1147                    cmdInfo.exitCode = 1;
1148                    cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download failed (not found).");
1149                }
1150            } catch (ConnectServerError e) {
1151                log.debug(e, e);
1152                downloadOk = false;
1153                cmdInfo.exitCode = 1;
1154                cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download failed: " + e.getMessage());
1155            }
1156        }
1157        // Check and display progress
1158        final String progress = "|/-\\";
1159        int x = 0;
1160        boolean stopDownload = false;
1161        do {
1162            System.out.print(progress.charAt(x++ % progress.length()) + "\r");
1163            try {
1164                Thread.sleep(1000);
1165            } catch (InterruptedException e) {
1166                stopDownload = true;
1167            }
1168            List<DownloadingPackage> pkgsCompleted = new ArrayList<DownloadingPackage>();
1169            for (DownloadingPackage pkg : pkgs) {
1170                if (pkg.isCompleted()) {
1171                    pkgsCompleted.add(pkg);
1172                    CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_DOWNLOAD);
1173                    cmdInfo.param = pkg.getId();
1174                    // Digest check not correctly implemented
1175                    if (false && !pkg.isDigestOk()) {
1176                        downloadOk = false;
1177                        cmdInfo.exitCode = 1;
1178                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Wrong digest.");
1179                    } else if (pkg.getPackageState() == PackageState.DOWNLOADED) {
1180                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_DEBUG, "Downloaded.");
1181                    } else {
1182                        downloadOk = false;
1183                        cmdInfo.exitCode = 1;
1184                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download failed: " + pkg.getErrorMessage());
1185                        if (pkg.isServerError()) { // Wasted effort to continue other downloads
1186                            stopDownload = true;
1187                        }
1188                    }
1189                }
1190            }
1191            pkgs.removeAll(pkgsCompleted);
1192        } while (!stopDownload && pkgs.size() > 0);
1193        if (pkgs.size() > 0) {
1194            downloadOk = false;
1195            log.error("Packages download was interrupted");
1196            for (DownloadingPackage pkg : pkgs) {
1197                CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_ADD);
1198                cmdInfo.param = pkg.getId();
1199                cmdInfo.exitCode = 1;
1200                cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download interrupted.");
1201            }
1202        }
1203        return downloadOk;
1204    }
1205
1206    /**
1207     * @deprecated Since 7.10. Use {@link #pkgRequest(List, List, List, List, boolean, boolean)} instead.
1208     */
1209    @Deprecated
1210    public boolean pkgRequest(List<String> pkgsToAdd, List<String> pkgsToInstall, List<String> pkgsToUninstall,
1211            List<String> pkgsToRemove) {
1212        return pkgRequest(pkgsToAdd, pkgsToInstall, pkgsToUninstall, pkgsToRemove, true, false);
1213    }
1214
1215    /**
1216     * @param keepExisting If false, the request will remove existing packages that are not part of the resolution
1217     * @since 5.9.2
1218     * @deprecated Since 7.10. Use {@link #pkgRequest(List, List, List, List, boolean, boolean)} instead.
1219     */
1220    @Deprecated
1221    public boolean pkgRequest(List<String> pkgsToAdd, List<String> pkgsToInstall, List<String> pkgsToUninstall,
1222            List<String> pkgsToRemove, boolean keepExisting) {
1223        return pkgRequest(pkgsToAdd, pkgsToInstall, pkgsToUninstall, pkgsToRemove, keepExisting, false);
1224    }
1225
1226    /**
1227     * @param keepExisting If false, the request will remove existing packages that are not part of the resolution
1228     * @param ignoreMissing Do not error out on missing packages, just handle the rest
1229     * @since 5.9.2
1230     */
1231    public boolean pkgRequest(List<String> pkgsToAdd, List<String> pkgsToInstall, List<String> pkgsToUninstall,
1232            List<String> pkgsToRemove, boolean keepExisting, boolean ignoreMissing) {
1233        // default is install mode
1234        return pkgRequest(pkgsToAdd, pkgsToInstall, pkgsToUninstall, pkgsToRemove, keepExisting, ignoreMissing, false);
1235    }
1236
1237    /**
1238     * @param keepExisting If false, the request will remove existing packages that are not part of the resolution
1239     * @param ignoreMissing Do not error out on missing packages, just handle the rest
1240     * @param upgradeMode If true, all packages will be upgraded to their last compliant version
1241     * @since 8.4
1242     */
1243    public boolean pkgRequest(List<String> pkgsToAdd, List<String> pkgsToInstall, List<String> pkgsToUninstall,
1244            List<String> pkgsToRemove, boolean keepExisting, boolean ignoreMissing, boolean upgradeMode) {
1245        try {
1246            boolean cmdOk = true;
1247            // Add local files
1248            cmdOk = pkgAdd(pkgsToAdd, ignoreMissing);
1249            // Build solver request
1250            List<String> solverInstall = new ArrayList<>();
1251            List<String> solverRemove = new ArrayList<>();
1252            List<String> solverUpgrade = new ArrayList<>();
1253            // Potential local cache snapshots to replace
1254            Set<String> localSnapshotsToMaybeReplace = new HashSet<>();
1255            if (pkgsToInstall != null) {
1256                List<String> namesOrIdsToInstall = new ArrayList<>();
1257                Set<String> localSnapshotsToUninstall = new HashSet<>();
1258                Set<String> localSnapshotsToReplace = new HashSet<>();
1259                cmdOk = checkLocalPackagesAndAddLocalFiles(pkgsToInstall, upgradeMode, ignoreMissing,
1260                        namesOrIdsToInstall, localSnapshotsToUninstall, localSnapshotsToReplace,
1261                        localSnapshotsToMaybeReplace);
1262
1263                // Replace snapshots to install but already in cache (requested by id or filename)
1264                if (CollectionUtils.isNotEmpty(localSnapshotsToReplace)) {
1265                    log.info(String.format(
1266                            "The following SNAPSHOT package(s) will be replaced in local cache (if available): %s",
1267                            localSnapshotsToReplace));
1268                    String initialAccept = accept;
1269                    if ("ask".equalsIgnoreCase(accept)) {
1270                        accept = readConsole("Do you want to continue (yes/no)? [yes] ", "yes");
1271                    }
1272                    if (!Boolean.parseBoolean(accept)) {
1273                        log.warn("Exit");
1274                        return false;
1275                    }
1276                    accept = initialAccept;
1277                    for (String pkgId : localSnapshotsToUninstall) {
1278                        LocalPackage uninstalledPkg = pkgUninstall(pkgId);
1279                        if (uninstalledPkg == null) {
1280                            cmdOk = false;
1281                        }
1282                    }
1283                    for (String pkgIdOrFileName : localSnapshotsToReplace) {
1284                        if (isLocalPackageFile(pkgIdOrFileName) || isRemotePackageId(pkgIdOrFileName)) {
1285                            LocalPackage addedPkg = pkgAdd(pkgIdOrFileName, ignoreMissing);
1286                            if (addedPkg == null) {
1287                                cmdOk = false;
1288                            }
1289                        } else {
1290                            log.info(String.format(
1291                                    "The SNAPSHOT package %s is not available remotely, local cache will be used.",
1292                                    pkgIdOrFileName));
1293                        }
1294                    }
1295                }
1296
1297                if (upgradeMode) {
1298                    solverUpgrade.addAll(namesOrIdsToInstall);
1299                } else {
1300                    solverInstall.addAll(namesOrIdsToInstall);
1301                }
1302            }
1303            if (pkgsToUninstall != null) {
1304                solverRemove.addAll(pkgsToUninstall);
1305            }
1306            if (pkgsToRemove != null) {
1307                // Add packages to remove to uninstall list
1308                solverRemove.addAll(pkgsToRemove);
1309            }
1310            if ((solverInstall.size() != 0) || (solverRemove.size() != 0) || (solverUpgrade.size() != 0)) {
1311                // Check whether we need to relax restriction to targetPlatform
1312                String requestPlatform = targetPlatform;
1313                List<String> requestPackages = new ArrayList<>();
1314                requestPackages.addAll(solverInstall);
1315                requestPackages.addAll(solverRemove);
1316                requestPackages.addAll(solverUpgrade);
1317                if (ignoreMissing) {
1318                    // Remove unknown packages from the list
1319                    Map<String, List<DownloadablePackage>> knownNames = getPackageManager().getAllPackagesByName();
1320                    List<String> solverInstallCopy = new ArrayList<>(solverInstall);
1321                    for (String pkgToInstall : solverInstallCopy) {
1322                        if (!knownNames.containsKey(pkgToInstall)) {
1323                            log.warn("Unable to install unknown package: " + pkgToInstall);
1324                            solverInstall.remove(pkgToInstall);
1325                            requestPackages.remove(pkgToInstall);
1326                        }
1327                    }
1328                }
1329                List<String> nonCompliantPkg = getPackageManager().getNonCompliantList(requestPackages, targetPlatform);
1330                if (nonCompliantPkg.size() > 0) {
1331                    requestPlatform = null;
1332                    if ("ask".equalsIgnoreCase(relax)) {
1333                        relax = readConsole(
1334                                "Package(s) %s not available on platform version %s.\n"
1335                                        + "Do you want to relax the constraint (yes/no)? [no] ",
1336                                "no", StringUtils.join(nonCompliantPkg, ", "), targetPlatform);
1337                    }
1338
1339                    if (Boolean.parseBoolean(relax)) {
1340                        log.warn(String.format("Relax restriction to target platform %s because of package(s) %s",
1341                                targetPlatform, StringUtils.join(nonCompliantPkg, ", ")));
1342                    } else {
1343                        if (ignoreMissing) {
1344                            for (String pkgToInstall : nonCompliantPkg) {
1345                                log.warn("Unable to install package: " + pkgToInstall);
1346                                solverInstall.remove(pkgToInstall);
1347                            }
1348                        } else {
1349                            throw new PackageException(String.format(
1350                                    "Package(s) %s not available on platform version %s (relax is not allowed)",
1351                                    StringUtils.join(nonCompliantPkg, ", "), targetPlatform));
1352                        }
1353                    }
1354                }
1355
1356                log.debug("solverInstall: " + solverInstall);
1357                log.debug("solverRemove: " + solverRemove);
1358                log.debug("solverUpgrade: " + solverUpgrade);
1359                DependencyResolution resolution = getPackageManager().resolveDependencies(solverInstall, solverRemove,
1360                        solverUpgrade, requestPlatform, allowSNAPSHOT, keepExisting);
1361                log.info(resolution);
1362                if (resolution.isFailed()) {
1363                    return false;
1364                }
1365                if (resolution.isEmpty()) {
1366                    pkgRemove(pkgsToRemove);
1367                    return cmdOk;
1368                }
1369                if ("ask".equalsIgnoreCase(accept)) {
1370                    accept = readConsole("Do you want to continue (yes/no)? [yes] ", "yes");
1371                }
1372                if (!Boolean.parseBoolean(accept)) {
1373                    log.warn("Exit");
1374                    return false;
1375                }
1376
1377                List<String> packageIdsToRemove = resolution.getOrderedPackageIdsToRemove();
1378                List<String> packageIdsToUpgrade = resolution.getUpgradePackageIds();
1379                List<String> packageIdsToInstall = resolution.getOrderedPackageIdsToInstall();
1380                List<String> packagesIdsToReInstall = new ArrayList<>();
1381
1382                // Replace snapshots to install but already in cache (requested by name)
1383                if (CollectionUtils.containsAny(packageIdsToInstall, localSnapshotsToMaybeReplace)) {
1384                    for (Object pkgIdObj : CollectionUtils.intersection(packageIdsToInstall,
1385                            localSnapshotsToMaybeReplace)) {
1386                        String pkgId = (String) pkgIdObj;
1387                        LocalPackage addedPkg = pkgAdd(pkgId, ignoreMissing);
1388                        if (addedPkg == null) {
1389                            cmdOk = false;
1390                        }
1391                    }
1392                }
1393
1394                // Download remote packages
1395                if (!downloadPackages(resolution.getDownloadPackageIds())) {
1396                    log.error("Aborting packages change request");
1397                    return false;
1398                }
1399
1400                // Uninstall
1401                if (!packageIdsToUpgrade.isEmpty()) {
1402                    // Add packages to upgrade to uninstall list
1403                    // Don't use IDs to avoid downgrade instead of uninstall
1404                    packageIdsToRemove.addAll(resolution.getLocalPackagesToUpgrade().keySet());
1405                    DependencyResolution uninstallResolution = getPackageManager().resolveDependencies(null,
1406                            packageIdsToRemove, null, requestPlatform, allowSNAPSHOT, keepExisting, true);
1407                    log.debug("Sub-resolution (uninstall) " + uninstallResolution);
1408                    if (uninstallResolution.isFailed()) {
1409                        return false;
1410                    }
1411                    List<String> newPackageIdsToRemove = uninstallResolution.getOrderedPackageIdsToRemove();
1412                    packagesIdsToReInstall = ListUtils.subtract(newPackageIdsToRemove, packageIdsToRemove);
1413                    packagesIdsToReInstall.removeAll(packageIdsToUpgrade);
1414                    packageIdsToRemove = newPackageIdsToRemove;
1415                }
1416                if (!pkgUninstall(packageIdsToRemove)) {
1417                    return false;
1418                }
1419
1420                // Install
1421                if (!packagesIdsToReInstall.isEmpty()) {
1422                    // Add list of packages uninstalled because of upgrade
1423                    packageIdsToInstall.addAll(packagesIdsToReInstall);
1424                    DependencyResolution installResolution = getPackageManager().resolveDependencies(
1425                            packageIdsToInstall, null, null, requestPlatform, allowSNAPSHOT, keepExisting, true);
1426                    log.debug("Sub-resolution (install) " + installResolution);
1427                    if (installResolution.isFailed()) {
1428                        return false;
1429                    }
1430                    packageIdsToInstall = installResolution.getOrderedPackageIdsToInstall();
1431                }
1432                if (!pkgInstall(packageIdsToInstall, ignoreMissing)) {
1433                    return false;
1434                }
1435
1436                pkgRemove(pkgsToRemove);
1437            }
1438            return cmdOk;
1439        } catch (PackageException e) {
1440            log.error(e);
1441            log.debug(e, e);
1442            return false;
1443        }
1444    }
1445
1446    private boolean checkLocalPackagesAndAddLocalFiles(List<String> pkgsToInstall, boolean upgradeMode,
1447            boolean ignoreMissing, List<String> namesOrIdsToInstall, Set<String> localSnapshotsToUninstall,
1448            Set<String> localSnapshotsToReplace, Set<String> localSnapshotsToMaybeReplace) throws PackageException {
1449        boolean cmdOk = true;
1450        for (String pkgToInstall : pkgsToInstall) {
1451            String nameOrIdToInstall = pkgToInstall;
1452            if (!upgradeMode) {
1453                boolean isLocalPackageFile = isLocalPackageFile(pkgToInstall);
1454                if (isLocalPackageFile) {
1455                    // If install request is a file name, get the id
1456                    nameOrIdToInstall = getLocalPackageFileId(getLocalPackageFile(pkgToInstall));
1457                }
1458                // get corresponding local package if present.
1459                // if request is a name, prefer installed package
1460                LocalPackage localPackage = getInstalledPackageByName(nameOrIdToInstall);
1461                if (localPackage != null) {
1462                    // as not in upgrade mode, replace the package name by the installed package id
1463                    nameOrIdToInstall = localPackage.getId();
1464                } else {
1465                    if (isLocalPackageId(nameOrIdToInstall)) {
1466                        // if request is an id, get potential package in local cache
1467                        localPackage = getLocalPackage(nameOrIdToInstall);
1468                    } else {
1469                        // if request is a name but there is no installed package matching, get the best version
1470                        // in local cache to replace it if it is a snapshot and it happens to be the actual
1471                        // version to install afterward
1472                        LocalPackage potentialMatchingPackage = getLocalPackage(nameOrIdToInstall);
1473                        if (potentialMatchingPackage != null && potentialMatchingPackage.getVersion().isSnapshot()) {
1474                            localSnapshotsToMaybeReplace.add(potentialMatchingPackage.getId());
1475                        }
1476                    }
1477                }
1478                // first install of local file or directory
1479                if (localPackage == null && isLocalPackageFile) {
1480                    LocalPackage addedPkg = pkgAdd(pkgToInstall, ignoreMissing);
1481                    if (addedPkg == null) {
1482                        cmdOk = false;
1483                    }
1484                }
1485                // if a requested SNAPSHOT package is present, mark it for replacement in local cache
1486                if (localPackage != null && localPackage.getVersion().isSnapshot()) {
1487                    if (localPackage.getPackageState().isInstalled()) {
1488                        // if it's already installed, uninstall it
1489                        localSnapshotsToUninstall.add(nameOrIdToInstall);
1490                    }
1491                    // use the local file name if given and ensure we replace the right version, in case
1492                    // nameOrIdToInstall is a name
1493                    String pkgToAdd = isLocalPackageFile ? pkgToInstall : localPackage.getId();
1494                    localSnapshotsToReplace.add(pkgToAdd);
1495                }
1496            }
1497            namesOrIdsToInstall.add(nameOrIdToInstall);
1498        }
1499        return cmdOk;
1500    }
1501
1502    /**
1503     * Installs a list of packages and uninstalls the rest (no dependency check)
1504     *
1505     * @since 5.9.2
1506     * @deprecated Since 7.10. Use #pkgSet(List, boolean) instead.
1507     */
1508    @Deprecated
1509    public boolean pkgSet(List<String> pkgList) {
1510        return pkgSet(pkgList, false);
1511    }
1512
1513    /**
1514     * Installs a list of packages and uninstalls the rest (no dependency check)
1515     *
1516     * @since 6.0
1517     */
1518    public boolean pkgSet(List<String> pkgList, boolean ignoreMissing) {
1519        boolean cmdOK = true;
1520        cmdOK = cmdOK && pkgInstall(pkgList, ignoreMissing);
1521        List<DownloadablePackage> installedPkgs = getPackageManager().listInstalledPackages();
1522        List<String> pkgsToUninstall = new ArrayList<>();
1523        for (DownloadablePackage pkg : installedPkgs) {
1524            if ((!pkgList.contains(pkg.getName())) && (!pkgList.contains(pkg.getId()))) {
1525                pkgsToUninstall.add(pkg.getId());
1526            }
1527        }
1528        if (pkgsToUninstall.size() != 0) {
1529            cmdOK = cmdOK && pkgUninstall(pkgsToUninstall);
1530        }
1531        return cmdOK;
1532    }
1533
1534    /**
1535     * Prompt user for yes/no answer
1536     *
1537     * @param message The message to display
1538     * @param defaultValue The default answer if there's no console or if "Enter" key is pressed.
1539     * @param objects Parameters to use in the message (like in {@link String#format(String, Object...)})
1540     * @return {@code "true"} if answer is in {@link #POSITIVE_ANSWERS}, else return {@code "false"}
1541     */
1542    protected String readConsole(String message, String defaultValue, Object... objects) {
1543        String answer;
1544        Console console = System.console();
1545        if (console == null || StringUtils.isEmpty(answer = console.readLine(message, objects))) {
1546            answer = defaultValue;
1547        }
1548        answer = answer.trim().toLowerCase();
1549        return parseAnswer(answer);
1550    }
1551
1552    /**
1553     * @return {@code "true"} if answer is in {@link #POSITIVE_ANSWERS}, and {@code "ask"} if answer values
1554     *         {@code "ask"}, else return {@code "false"}
1555     * @since 6.0
1556     */
1557    public static String parseAnswer(String answer) {
1558        if ("ask".equalsIgnoreCase(answer)) {
1559            return "ask";
1560        }
1561        if ("false".equalsIgnoreCase(answer)) {
1562            return "false";
1563        }
1564        for (String positive : POSITIVE_ANSWERS) {
1565            if (positive.equalsIgnoreCase(answer)) {
1566                return "true";
1567            }
1568        }
1569        return "false";
1570    }
1571
1572    public boolean pkgHotfix() {
1573        List<String> lastHotfixes = getPackageManager().listLastHotfixes(targetPlatform, allowSNAPSHOT);
1574        return pkgRequest(null, lastHotfixes, null, null, true, false);
1575    }
1576
1577    public boolean pkgUpgrade() {
1578        List<String> upgradeNames = getPackageManager().listInstalledPackagesNames(null);
1579        // use upgrade mode
1580        return pkgRequest(null, upgradeNames, null, null, true, false, true);
1581    }
1582
1583    /**
1584     * Must be called after {@link #setAccept(String)} which overwrites its value.
1585     *
1586     * @param relaxValue true, false or ask; ignored if null
1587     */
1588    public void setRelax(String relaxValue) {
1589        if (relaxValue != null) {
1590            relax = parseAnswer(relaxValue);
1591        }
1592    }
1593
1594    /**
1595     * @param acceptValue true, false or ask; if true or ask, then calls {@link #setRelax(String)} with the same value;
1596     *            ignored if null
1597     */
1598    public void setAccept(String acceptValue) {
1599        if (acceptValue != null) {
1600            accept = parseAnswer(acceptValue);
1601            if ("ask".equals(accept) || "true".equals(accept)) {
1602                setRelax(acceptValue);
1603            }
1604        }
1605    }
1606
1607    /*
1608     * Helper for adding a new PackageInfo initialized with informations gathered from the given package. It is not put
1609     * into CommandInfo to avoid adding a dependency on Connect Client
1610     */
1611    private PackageInfo newPackageInfo(CommandInfo cmdInfo, Package pkg) {
1612        PackageInfo packageInfo = new PackageInfo(pkg);
1613        cmdInfo.packages.add(packageInfo);
1614        return packageInfo;
1615    }
1616
1617    /**
1618     * @param packages List of packages identified by their ID, name or local filename.
1619     * @since 5.7
1620     */
1621    public boolean pkgShow(List<String> packages) {
1622        boolean cmdOk = true;
1623        if (packages == null || packages.isEmpty()) {
1624            return cmdOk;
1625        }
1626        StringBuilder sb = new StringBuilder();
1627        sb.append("****************************************");
1628        for (String pkg : packages) {
1629            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_SHOW);
1630            cmdInfo.param = pkg;
1631            try {
1632                PackageInfo packageInfo = newPackageInfo(cmdInfo, findPackage(pkg));
1633                sb.append("\nPackage: " + packageInfo.id);
1634                sb.append("\nState: " + packageInfo.state);
1635                sb.append("\nVersion: " + packageInfo.version);
1636                sb.append("\nName: " + packageInfo.name);
1637                sb.append("\nType: " + packageInfo.type);
1638                sb.append("\nVisibility: " + packageInfo.visibility);
1639                if (packageInfo.state == PackageState.REMOTE && packageInfo.type != PackageType.STUDIO
1640                        && packageInfo.visibility != PackageVisibility.PUBLIC
1641                        && !LogicalInstanceIdentifier.isRegistered()) {
1642                    sb.append(" (registration required)");
1643                }
1644                sb.append("\nTarget platforms: " + ArrayUtils.toString(packageInfo.targetPlatforms));
1645                appendIfNotEmpty(sb, "\nVendor: ", packageInfo.vendor);
1646                sb.append("\nSupports hot-reload: " + packageInfo.supportsHotReload);
1647                sb.append("\nSupported: " + packageInfo.supported);
1648                sb.append("\nProduction state: " + packageInfo.productionState);
1649                sb.append("\nValidation state: " + packageInfo.validationState);
1650                appendIfNotEmpty(sb, "\nProvides: ", packageInfo.provides);
1651                appendIfNotEmpty(sb, "\nDepends: ", packageInfo.dependencies);
1652                appendIfNotEmpty(sb, "\nConflicts: ", packageInfo.conflicts);
1653                appendIfNotEmpty(sb, "\nTitle: ", packageInfo.title);
1654                appendIfNotEmpty(sb, "\nDescription: ", packageInfo.description);
1655                appendIfNotEmpty(sb, "\nHomepage: ", packageInfo.homePage);
1656                appendIfNotEmpty(sb, "\nLicense: ", packageInfo.licenseType);
1657                appendIfNotEmpty(sb, "\nLicense URL: ", packageInfo.licenseUrl);
1658                sb.append("\n****************************************");
1659            } catch (PackageException e) {
1660                cmdOk = false;
1661                cmdInfo.exitCode = 1;
1662                cmdInfo.newMessage(e);
1663            }
1664        }
1665        log.info(sb.toString());
1666        return cmdOk;
1667    }
1668
1669    private void appendIfNotEmpty(StringBuilder sb, String label, Object[] array) {
1670        if (ArrayUtils.isNotEmpty(array)) {
1671            sb.append(label + ArrayUtils.toString(array));
1672        }
1673    }
1674
1675    private void appendIfNotEmpty(StringBuilder sb, String label, String value) {
1676        if (StringUtils.isNotEmpty(value)) {
1677            sb.append(label + value);
1678        }
1679    }
1680
1681    /**
1682     * 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
1683     * remote package.
1684     *
1685     * @param pkg A ZIP filename or file path, or package ID or a package name.
1686     * @return The first package found matching the given string.
1687     * @throws PackageException If no package is found or if an issue occurred while searching.
1688     * @see PackageDefinition
1689     * @see LocalPackage
1690     * @see DownloadablePackage
1691     */
1692    protected Package findPackage(String pkg) throws PackageException {
1693        // Is it a local ZIP file?
1694        File localPackageFile = getLocalPackageFile(pkg);
1695        if (localPackageFile != null) {
1696            return service.loadPackageFromZip(localPackageFile);
1697        }
1698
1699        // Is it a local package ID or name?
1700        LocalPackage localPackage = getLocalPackage(pkg);
1701        if (localPackage != null) {
1702            return localPackage;
1703        }
1704
1705        // Is it a remote package ID or name?
1706        String pkgId = getRemotePackageId(pkg);
1707        if (pkgId != null) {
1708            return getPackageManager().findPackageById(pkgId);
1709        }
1710
1711        throw new PackageException("Could not find a remote or local (relative to "
1712                + "current directory or to NUXEO_HOME) " + "package with name or ID " + pkg);
1713    }
1714
1715    /**
1716     * @since 5.9.1
1717     */
1718    public void setAllowSNAPSHOT(boolean allow) {
1719        CUDFHelper.defaultAllowSNAPSHOT = allow;
1720        allowSNAPSHOT = allow;
1721    }
1722
1723}