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().findPackageById(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.getVisibility() != PackageVisibility.PUBLIC && !isRegistered) {
1124                downloadOk = false;
1125                cmdInfo.exitCode = 1;
1126                cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Registration required.");
1127                continue;
1128            }
1129
1130            // Download
1131            try {
1132                DownloadingPackage download = getPackageManager().download(pkg);
1133                if (download != null) {
1134                    pkgs.add(download);
1135                    cmdInfo.param = download.getId();
1136                    cmdInfo.newMessage(SimpleLog.LOG_LEVEL_DEBUG, "Downloading...");
1137                } else {
1138                    downloadOk = false;
1139                    cmdInfo.exitCode = 1;
1140                    cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download failed (not found).");
1141                }
1142            } catch (ConnectServerError e) {
1143                log.debug(e, e);
1144                downloadOk = false;
1145                cmdInfo.exitCode = 1;
1146                cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download failed: " + e.getMessage());
1147            }
1148        }
1149        // Check and display progress
1150        final String progress = "|/-\\";
1151        int x = 0;
1152        boolean stopDownload = false;
1153        do {
1154            System.out.print(progress.charAt(x++ % progress.length()) + "\r");
1155            try {
1156                Thread.sleep(1000);
1157            } catch (InterruptedException e) {
1158                stopDownload = true;
1159            }
1160            List<DownloadingPackage> pkgsCompleted = new ArrayList<DownloadingPackage>();
1161            for (DownloadingPackage pkg : pkgs) {
1162                if (pkg.isCompleted()) {
1163                    pkgsCompleted.add(pkg);
1164                    CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_DOWNLOAD);
1165                    cmdInfo.param = pkg.getId();
1166                    // Digest check not correctly implemented
1167                    if (false && !pkg.isDigestOk()) {
1168                        downloadOk = false;
1169                        cmdInfo.exitCode = 1;
1170                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Wrong digest.");
1171                    } else if (pkg.getPackageState() == PackageState.DOWNLOADED) {
1172                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_DEBUG, "Downloaded.");
1173                    } else {
1174                        downloadOk = false;
1175                        cmdInfo.exitCode = 1;
1176                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download failed: " + pkg.getErrorMessage());
1177                        if (pkg.isServerError()) { // Wasted effort to continue other downloads
1178                            stopDownload = true;
1179                        }
1180                    }
1181                }
1182            }
1183            pkgs.removeAll(pkgsCompleted);
1184        } while (!stopDownload && pkgs.size() > 0);
1185        if (pkgs.size() > 0) {
1186            downloadOk = false;
1187            log.error("Packages download was interrupted");
1188            for (DownloadingPackage pkg : pkgs) {
1189                CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_ADD);
1190                cmdInfo.param = pkg.getId();
1191                cmdInfo.exitCode = 1;
1192                cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download interrupted.");
1193            }
1194        }
1195        return downloadOk;
1196    }
1197
1198    /**
1199     * @deprecated Since 7.10. Use {@link #pkgRequest(List, List, List, List, boolean, boolean)} instead.
1200     */
1201    @Deprecated
1202    public boolean pkgRequest(List<String> pkgsToAdd, List<String> pkgsToInstall, List<String> pkgsToUninstall,
1203            List<String> pkgsToRemove) {
1204        return pkgRequest(pkgsToAdd, pkgsToInstall, pkgsToUninstall, pkgsToRemove, true, false);
1205    }
1206
1207    /**
1208     * @param keepExisting If false, the request will remove existing packages that are not part of the resolution
1209     * @since 5.9.2
1210     * @deprecated Since 7.10. Use {@link #pkgRequest(List, List, List, List, boolean, boolean)} instead.
1211     */
1212    @Deprecated
1213    public boolean pkgRequest(List<String> pkgsToAdd, List<String> pkgsToInstall, List<String> pkgsToUninstall,
1214            List<String> pkgsToRemove, boolean keepExisting) {
1215        return pkgRequest(pkgsToAdd, pkgsToInstall, pkgsToUninstall, pkgsToRemove, keepExisting, false);
1216    }
1217
1218    /**
1219     * @param keepExisting If false, the request will remove existing packages that are not part of the resolution
1220     * @param ignoreMissing Do not error out on missing packages, just handle the rest
1221     * @since 5.9.2
1222     */
1223    public boolean pkgRequest(List<String> pkgsToAdd, List<String> pkgsToInstall, List<String> pkgsToUninstall,
1224            List<String> pkgsToRemove, boolean keepExisting, boolean ignoreMissing) {
1225        // default is install mode
1226        return pkgRequest(pkgsToAdd, pkgsToInstall, pkgsToUninstall, pkgsToRemove, keepExisting, ignoreMissing, false);
1227    }
1228
1229    /**
1230     * @param keepExisting If false, the request will remove existing packages that are not part of the resolution
1231     * @param ignoreMissing Do not error out on missing packages, just handle the rest
1232     * @param upgradeMode If true, all packages will be upgraded to their last compliant version
1233     * @since 8.4
1234     */
1235    public boolean pkgRequest(List<String> pkgsToAdd, List<String> pkgsToInstall, List<String> pkgsToUninstall,
1236            List<String> pkgsToRemove, boolean keepExisting, boolean ignoreMissing, boolean upgradeMode) {
1237        try {
1238            boolean cmdOk = true;
1239            // Add local files
1240            cmdOk = pkgAdd(pkgsToAdd, ignoreMissing);
1241            // Build solver request
1242            List<String> solverInstall = new ArrayList<>();
1243            List<String> solverRemove = new ArrayList<>();
1244            List<String> solverUpgrade = new ArrayList<>();
1245            // Potential local cache snapshots to replace
1246            Set<String> localSnapshotsToMaybeReplace = new HashSet<>();
1247            if (pkgsToInstall != null) {
1248                List<String> namesOrIdsToInstall = new ArrayList<>();
1249                Set<String> localSnapshotsToUninstall = new HashSet<>();
1250                Set<String> localSnapshotsToReplace = new HashSet<>();
1251                cmdOk = checkLocalPackagesAndAddLocalFiles(pkgsToInstall, upgradeMode, ignoreMissing,
1252                        namesOrIdsToInstall, localSnapshotsToUninstall, localSnapshotsToReplace,
1253                        localSnapshotsToMaybeReplace);
1254
1255                // Replace snapshots to install but already in cache (requested by id or filename)
1256                if (CollectionUtils.isNotEmpty(localSnapshotsToReplace)) {
1257                    log.info(String.format("The following SNAPSHOT package(s) will be replaced in local cache : %s",
1258                            localSnapshotsToReplace));
1259                    String initialAccept = accept;
1260                    if ("ask".equalsIgnoreCase(accept)) {
1261                        accept = readConsole("Do you want to continue (yes/no)? [yes] ", "yes");
1262                    }
1263                    if (!Boolean.parseBoolean(accept)) {
1264                        log.warn("Exit");
1265                        return false;
1266                    }
1267                    accept = initialAccept;
1268                    for (String pkgId : localSnapshotsToUninstall) {
1269                        LocalPackage uninstalledPkg = pkgUninstall(pkgId);
1270                        if (uninstalledPkg == null) {
1271                            cmdOk = false;
1272                        }
1273                    }
1274                    for (String pkgId : localSnapshotsToReplace) {
1275                        LocalPackage addedPkg = pkgAdd(pkgId, ignoreMissing);
1276                        if (addedPkg == null) {
1277                            cmdOk = false;
1278                        }
1279                    }
1280                }
1281
1282                if (upgradeMode) {
1283                    solverUpgrade.addAll(namesOrIdsToInstall);
1284                } else {
1285                    solverInstall.addAll(namesOrIdsToInstall);
1286                }
1287            }
1288            if (pkgsToUninstall != null) {
1289                solverRemove.addAll(pkgsToUninstall);
1290            }
1291            if (pkgsToRemove != null) {
1292                // Add packages to remove to uninstall list
1293                solverRemove.addAll(pkgsToRemove);
1294            }
1295            if ((solverInstall.size() != 0) || (solverRemove.size() != 0) || (solverUpgrade.size() != 0)) {
1296                // Check whether we need to relax restriction to targetPlatform
1297                String requestPlatform = targetPlatform;
1298                List<String> requestPackages = new ArrayList<>();
1299                requestPackages.addAll(solverInstall);
1300                requestPackages.addAll(solverRemove);
1301                requestPackages.addAll(solverUpgrade);
1302                if (ignoreMissing) {
1303                    // Remove unknown packages from the list
1304                    Map<String, List<DownloadablePackage>> knownNames = getPackageManager().getAllPackagesByName();
1305                    List<String> solverInstallCopy = new ArrayList<>(solverInstall);
1306                    for (String pkgToInstall : solverInstallCopy) {
1307                        if (!knownNames.containsKey(pkgToInstall)) {
1308                            log.warn("Unable to install unknown package: " + pkgToInstall);
1309                            solverInstall.remove(pkgToInstall);
1310                            requestPackages.remove(pkgToInstall);
1311                        }
1312                    }
1313                }
1314                List<String> nonCompliantPkg = getPackageManager().getNonCompliantList(requestPackages, targetPlatform);
1315                if (nonCompliantPkg.size() > 0) {
1316                    requestPlatform = null;
1317                    if ("ask".equalsIgnoreCase(relax)) {
1318                        relax = readConsole(
1319                                "Package(s) %s not available on platform version %s.\n"
1320                                        + "Do you want to relax the constraint (yes/no)? [no] ",
1321                                "no", StringUtils.join(nonCompliantPkg, ", "), targetPlatform);
1322                    }
1323
1324                    if (Boolean.parseBoolean(relax)) {
1325                        log.warn(String.format("Relax restriction to target platform %s because of package(s) %s",
1326                                targetPlatform, StringUtils.join(nonCompliantPkg, ", ")));
1327                    } else {
1328                        if (ignoreMissing) {
1329                            for (String pkgToInstall : nonCompliantPkg) {
1330                                log.warn("Unable to install package: " + pkgToInstall);
1331                                solverInstall.remove(pkgToInstall);
1332                            }
1333                        } else {
1334                            throw new PackageException(String.format(
1335                                    "Package(s) %s not available on platform version %s (relax is not allowed)",
1336                                    StringUtils.join(nonCompliantPkg, ", "), targetPlatform));
1337                        }
1338                    }
1339                }
1340
1341                log.debug("solverInstall: " + solverInstall);
1342                log.debug("solverRemove: " + solverRemove);
1343                log.debug("solverUpgrade: " + solverUpgrade);
1344                DependencyResolution resolution = getPackageManager().resolveDependencies(solverInstall, solverRemove,
1345                        solverUpgrade, requestPlatform, allowSNAPSHOT, keepExisting);
1346                log.info(resolution);
1347                if (resolution.isFailed()) {
1348                    return false;
1349                }
1350                if (resolution.isEmpty()) {
1351                    pkgRemove(pkgsToRemove);
1352                    return cmdOk;
1353                }
1354                if ("ask".equalsIgnoreCase(accept)) {
1355                    accept = readConsole("Do you want to continue (yes/no)? [yes] ", "yes");
1356                }
1357                if (!Boolean.parseBoolean(accept)) {
1358                    log.warn("Exit");
1359                    return false;
1360                }
1361
1362                List<String> packageIdsToRemove = resolution.getOrderedPackageIdsToRemove();
1363                List<String> packageIdsToUpgrade = resolution.getUpgradePackageIds();
1364                List<String> packageIdsToInstall = resolution.getOrderedPackageIdsToInstall();
1365                List<String> packagesIdsToReInstall = new ArrayList<>();
1366
1367                // Replace snapshots to install but already in cache (requested by name)
1368                if (CollectionUtils.containsAny(packageIdsToInstall, localSnapshotsToMaybeReplace)) {
1369                    for (Object pkgIdObj : CollectionUtils.intersection(packageIdsToInstall,
1370                            localSnapshotsToMaybeReplace)) {
1371                        String pkgId = (String) pkgIdObj;
1372                        LocalPackage addedPkg = pkgAdd(pkgId, ignoreMissing);
1373                        if (addedPkg == null) {
1374                            cmdOk = false;
1375                        }
1376                    }
1377                }
1378
1379                // Download remote packages
1380                if (!downloadPackages(resolution.getDownloadPackageIds())) {
1381                    log.error("Aborting packages change request");
1382                    return false;
1383                }
1384
1385                // Uninstall
1386                if (!packageIdsToUpgrade.isEmpty()) {
1387                    // Add packages to upgrade to uninstall list
1388                    // Don't use IDs to avoid downgrade instead of uninstall
1389                    packageIdsToRemove.addAll(resolution.getLocalPackagesToUpgrade().keySet());
1390                    DependencyResolution uninstallResolution = getPackageManager().resolveDependencies(null,
1391                            packageIdsToRemove, null, requestPlatform, allowSNAPSHOT, keepExisting);
1392                    log.debug("Sub-resolution (uninstall) " + uninstallResolution);
1393                    if (uninstallResolution.isFailed()) {
1394                        return false;
1395                    }
1396                    List<String> newPackageIdsToRemove = uninstallResolution.getOrderedPackageIdsToRemove();
1397                    packagesIdsToReInstall = ListUtils.subtract(newPackageIdsToRemove, packageIdsToRemove);
1398                    packagesIdsToReInstall.removeAll(packageIdsToUpgrade);
1399                    packageIdsToRemove = newPackageIdsToRemove;
1400                }
1401                if (!pkgUninstall(packageIdsToRemove)) {
1402                    return false;
1403                }
1404
1405                // Install
1406                if (!packagesIdsToReInstall.isEmpty()) {
1407                    // Add list of packages uninstalled because of upgrade
1408                    packageIdsToInstall.addAll(packagesIdsToReInstall);
1409                    DependencyResolution installResolution = getPackageManager().resolveDependencies(
1410                            packageIdsToInstall, null, null, requestPlatform, allowSNAPSHOT, keepExisting);
1411                    log.debug("Sub-resolution (install) " + installResolution);
1412                    if (installResolution.isFailed()) {
1413                        return false;
1414                    }
1415                    packageIdsToInstall = installResolution.getOrderedPackageIdsToInstall();
1416                }
1417                if (!pkgInstall(packageIdsToInstall, ignoreMissing)) {
1418                    return false;
1419                }
1420
1421                pkgRemove(pkgsToRemove);
1422            }
1423            return cmdOk;
1424        } catch (PackageException e) {
1425            log.error(e);
1426            log.debug(e, e);
1427            return false;
1428        }
1429    }
1430
1431    private boolean checkLocalPackagesAndAddLocalFiles(List<String> pkgsToInstall, boolean upgradeMode,
1432            boolean ignoreMissing, List<String> namesOrIdsToInstall, Set<String> localSnapshotsToUninstall,
1433            Set<String> localSnapshotsToReplace, Set<String> localSnapshotsToMaybeReplace) throws PackageException {
1434        boolean cmdOk = true;
1435        for (String pkgToInstall : pkgsToInstall) {
1436            String nameOrIdToInstall = pkgToInstall;
1437            if (!upgradeMode) {
1438                boolean isLocalPackageFile = isLocalPackageFile(pkgToInstall);
1439                if (isLocalPackageFile) {
1440                    // If install request is a file name, get the id
1441                    nameOrIdToInstall = getLocalPackageFileId(getLocalPackageFile(pkgToInstall));
1442                }
1443                // get corresponding local package if present.
1444                // if request is a name, prefer installed package
1445                LocalPackage localPackage = getInstalledPackageByName(nameOrIdToInstall);
1446                if (localPackage != null) {
1447                    // as not in upgrade mode, replace the package name by the installed package id
1448                    nameOrIdToInstall = localPackage.getId();
1449                } else {
1450                    if (isLocalPackageId(nameOrIdToInstall)) {
1451                        // if request is an id, get potential package in local cache
1452                        localPackage = getLocalPackage(nameOrIdToInstall);
1453                    } else {
1454                        // if request is a name but there is no installed package matching, get the best version
1455                        // in local cache to replace it if it is a snapshot and it happens to be the actual
1456                        // version to install afterward
1457                        LocalPackage potentialMatchingPackage = getLocalPackage(nameOrIdToInstall);
1458                        if (potentialMatchingPackage != null && potentialMatchingPackage.getVersion().isSnapshot()) {
1459                            localSnapshotsToMaybeReplace.add(potentialMatchingPackage.getId());
1460                        }
1461                    }
1462                }
1463                // first install of local file or directory
1464                if (localPackage == null && isLocalPackageFile) {
1465                    LocalPackage addedPkg = pkgAdd(pkgToInstall, ignoreMissing);
1466                    if (addedPkg == null) {
1467                        cmdOk = false;
1468                    }
1469                }
1470                // if a requested SNAPSHOT package is present, mark it for replacement in local cache
1471                if (localPackage != null && localPackage.getVersion().isSnapshot()) {
1472                    if (localPackage.getPackageState().isInstalled()) {
1473                        // if it's already installed, unintall it
1474                        localSnapshotsToUninstall.add(nameOrIdToInstall);
1475                    }
1476                    // use the local file name if given and ensure we replace the right version, in case
1477                    // nameOrIdToInstall is a name
1478                    String pkgToAdd = isLocalPackageFile ? pkgToInstall : localPackage.getId();
1479                    localSnapshotsToReplace.add(pkgToAdd);
1480                }
1481            }
1482            namesOrIdsToInstall.add(nameOrIdToInstall);
1483        }
1484        return cmdOk;
1485    }
1486
1487    /**
1488     * Installs a list of packages and uninstalls the rest (no dependency check)
1489     *
1490     * @since 5.9.2
1491     * @deprecated Since 7.10. Use #pkgSet(List, boolean) instead.
1492     */
1493    @Deprecated
1494    public boolean pkgSet(List<String> pkgList) {
1495        return pkgSet(pkgList, false);
1496    }
1497
1498    /**
1499     * Installs a list of packages and uninstalls the rest (no dependency check)
1500     *
1501     * @since 6.0
1502     */
1503    public boolean pkgSet(List<String> pkgList, boolean ignoreMissing) {
1504        boolean cmdOK = true;
1505        cmdOK = cmdOK && pkgInstall(pkgList, ignoreMissing);
1506        List<DownloadablePackage> installedPkgs = getPackageManager().listInstalledPackages();
1507        List<String> pkgsToUninstall = new ArrayList<>();
1508        for (DownloadablePackage pkg : installedPkgs) {
1509            if ((!pkgList.contains(pkg.getName())) && (!pkgList.contains(pkg.getId()))) {
1510                pkgsToUninstall.add(pkg.getId());
1511            }
1512        }
1513        if (pkgsToUninstall.size() != 0) {
1514            cmdOK = cmdOK && pkgUninstall(pkgsToUninstall);
1515        }
1516        return cmdOK;
1517    }
1518
1519    /**
1520     * Prompt user for yes/no answer
1521     *
1522     * @param message The message to display
1523     * @param defaultValue The default answer if there's no console or if "Enter" key is pressed.
1524     * @param objects Parameters to use in the message (like in {@link String#format(String, Object...)})
1525     * @return {@code "true"} if answer is in {@link #POSITIVE_ANSWERS}, else return {@code "false"}
1526     */
1527    protected String readConsole(String message, String defaultValue, Object... objects) {
1528        String answer;
1529        Console console = System.console();
1530        if (console == null || StringUtils.isEmpty(answer = console.readLine(message, objects))) {
1531            answer = defaultValue;
1532        }
1533        answer = answer.trim().toLowerCase();
1534        return parseAnswer(answer);
1535    }
1536
1537    /**
1538     * @return {@code "true"} if answer is in {@link #POSITIVE_ANSWERS}, and {@code "ask"} if answer values
1539     *         {@code "ask"}, else return {@code "false"}
1540     * @since 6.0
1541     */
1542    public static String parseAnswer(String answer) {
1543        if ("ask".equalsIgnoreCase(answer)) {
1544            return "ask";
1545        }
1546        if ("false".equalsIgnoreCase(answer)) {
1547            return "false";
1548        }
1549        for (String positive : POSITIVE_ANSWERS) {
1550            if (positive.equalsIgnoreCase(answer)) {
1551                return "true";
1552            }
1553        }
1554        return "false";
1555    }
1556
1557    public boolean pkgHotfix() {
1558        List<String> hotFixNames = getPackageManager().listHotfixesNames(targetPlatform, allowSNAPSHOT);
1559        return pkgRequest(null, hotFixNames, null, null, true, false);
1560    }
1561
1562    public boolean pkgUpgrade() {
1563        List<String> upgradeNames = getPackageManager().listInstalledPackagesNames(null);
1564        // use upgrade mode
1565        return pkgRequest(null, upgradeNames, null, null, true, false, true);
1566    }
1567
1568    /**
1569     * Must be called after {@link #setAccept(String)} which overwrites its value.
1570     *
1571     * @param relaxValue true, false or ask; ignored if null
1572     */
1573    public void setRelax(String relaxValue) {
1574        if (relaxValue != null) {
1575            relax = parseAnswer(relaxValue);
1576        }
1577    }
1578
1579    /**
1580     * @param acceptValue true, false or ask; if true or ask, then calls {@link #setRelax(String)} with the same value;
1581     *            ignored if null
1582     */
1583    public void setAccept(String acceptValue) {
1584        if (acceptValue != null) {
1585            accept = parseAnswer(acceptValue);
1586            if ("ask".equals(accept) || "true".equals(accept)) {
1587                setRelax(acceptValue);
1588            }
1589        }
1590    }
1591
1592    /*
1593     * Helper for adding a new PackageInfo initialized with informations gathered from the given package. It is not put
1594     * into CommandInfo to avoid adding a dependency on Connect Client
1595     */
1596    private PackageInfo newPackageInfo(CommandInfo cmdInfo, Package pkg) {
1597        PackageInfo packageInfo = new PackageInfo(pkg);
1598        cmdInfo.packages.add(packageInfo);
1599        return packageInfo;
1600    }
1601
1602    /**
1603     * @param packages List of packages identified by their ID, name or local filename.
1604     * @since 5.7
1605     */
1606    public boolean pkgShow(List<String> packages) {
1607        boolean cmdOk = true;
1608        if (packages == null || packages.isEmpty()) {
1609            return cmdOk;
1610        }
1611        StringBuilder sb = new StringBuilder();
1612        sb.append("****************************************");
1613        for (String pkg : packages) {
1614            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_SHOW);
1615            cmdInfo.param = pkg;
1616            try {
1617                PackageInfo packageInfo = newPackageInfo(cmdInfo, findPackage(pkg));
1618                sb.append("\nPackage: " + packageInfo.id);
1619                sb.append("\nState: " + packageInfo.state);
1620                sb.append("\nVersion: " + packageInfo.version);
1621                sb.append("\nName: " + packageInfo.name);
1622                sb.append("\nType: " + packageInfo.type);
1623                sb.append("\nVisibility: " + packageInfo.visibility);
1624                if (packageInfo.state == PackageState.REMOTE && packageInfo.type != PackageType.STUDIO
1625                        && packageInfo.visibility != PackageVisibility.PUBLIC
1626                        && !LogicalInstanceIdentifier.isRegistered()) {
1627                    sb.append(" (registration required)");
1628                }
1629                sb.append("\nTarget platforms: " + ArrayUtils.toString(packageInfo.targetPlatforms));
1630                appendIfNotEmpty(sb, "\nVendor: ", packageInfo.vendor);
1631                sb.append("\nSupports hot-reload: " + packageInfo.supportsHotReload);
1632                sb.append("\nSupported: " + packageInfo.supported);
1633                sb.append("\nProduction state: " + packageInfo.productionState);
1634                sb.append("\nValidation state: " + packageInfo.validationState);
1635                appendIfNotEmpty(sb, "\nProvides: ", packageInfo.provides);
1636                appendIfNotEmpty(sb, "\nDepends: ", packageInfo.dependencies);
1637                appendIfNotEmpty(sb, "\nConflicts: ", packageInfo.conflicts);
1638                appendIfNotEmpty(sb, "\nTitle: ", packageInfo.title);
1639                appendIfNotEmpty(sb, "\nDescription: ", packageInfo.description);
1640                appendIfNotEmpty(sb, "\nHomepage: ", packageInfo.homePage);
1641                appendIfNotEmpty(sb, "\nLicense: ", packageInfo.licenseType);
1642                appendIfNotEmpty(sb, "\nLicense URL: ", packageInfo.licenseUrl);
1643                sb.append("\n****************************************");
1644            } catch (PackageException e) {
1645                cmdOk = false;
1646                cmdInfo.exitCode = 1;
1647                cmdInfo.newMessage(e);
1648            }
1649        }
1650        log.info(sb.toString());
1651        return cmdOk;
1652    }
1653
1654    private void appendIfNotEmpty(StringBuilder sb, String label, Object[] array) {
1655        if (ArrayUtils.isNotEmpty(array)) {
1656            sb.append(label + ArrayUtils.toString(array));
1657        }
1658    }
1659
1660    private void appendIfNotEmpty(StringBuilder sb, String label, String value) {
1661        if (StringUtils.isNotEmpty(value)) {
1662            sb.append(label + value);
1663        }
1664    }
1665
1666    /**
1667     * 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
1668     * remote package.
1669     *
1670     * @param pkg A ZIP filename or file path, or package ID or a package name.
1671     * @return The first package found matching the given string.
1672     * @throws PackageException If no package is found or if an issue occurred while searching.
1673     * @see PackageDefinition
1674     * @see LocalPackage
1675     * @see DownloadablePackage
1676     */
1677    protected Package findPackage(String pkg) throws PackageException {
1678        // Is it a local ZIP file?
1679        File localPackageFile = getLocalPackageFile(pkg);
1680        if (localPackageFile != null) {
1681            return service.loadPackageFromZip(localPackageFile);
1682        }
1683
1684        // Is it a local package ID or name?
1685        LocalPackage localPackage = getLocalPackage(pkg);
1686        if (localPackage != null) {
1687            return localPackage;
1688        }
1689
1690        // Is it a remote package ID or name?
1691        String pkgId = getRemotePackageId(pkg);
1692        if (pkgId != null) {
1693            return getPackageManager().findPackageById(pkgId);
1694        }
1695
1696        throw new PackageException("Could not find a remote or local (relative to "
1697                + "current directory or to NUXEO_HOME) " + "package with name or ID " + pkg);
1698    }
1699
1700    /**
1701     * @since 5.9.1
1702     */
1703    public void setAllowSNAPSHOT(boolean allow) {
1704        CUDFHelper.defaultAllowSNAPSHOT = allow;
1705        allowSNAPSHOT = allow;
1706    }
1707
1708}