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