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