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