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