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