001/*
002 * (C) Copyright 2006-2015 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nuxeo - initial API and implementation
016 *
017 */
018
019package org.nuxeo.connect.client.jsf;
020
021import java.io.IOException;
022import java.io.Serializable;
023import java.nio.file.attribute.FileTime;
024import java.text.DateFormat;
025import java.text.SimpleDateFormat;
026import java.util.ArrayList;
027import java.util.Date;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Locale;
031import java.util.Map;
032import java.util.TimeZone;
033
034import javax.faces.context.FacesContext;
035import javax.faces.model.SelectItem;
036
037import org.apache.commons.lang.ArrayUtils;
038import org.apache.commons.logging.Log;
039import org.apache.commons.logging.LogFactory;
040import org.jboss.seam.ScopeType;
041import org.jboss.seam.annotations.In;
042import org.jboss.seam.annotations.Name;
043import org.jboss.seam.annotations.Scope;
044import org.jboss.seam.contexts.Contexts;
045import org.jboss.seam.faces.FacesMessages;
046import org.jboss.seam.international.StatusMessage;
047
048import org.nuxeo.common.utils.ExceptionUtils;
049import org.nuxeo.connect.client.ui.SharedPackageListingsSettings;
050import org.nuxeo.connect.client.vindoz.InstallAfterRestart;
051import org.nuxeo.connect.client.we.StudioSnapshotHelper;
052import org.nuxeo.connect.connector.ConnectServerError;
053import org.nuxeo.connect.connector.http.ConnectUrlConfig;
054import org.nuxeo.connect.data.DownloadablePackage;
055import org.nuxeo.connect.data.DownloadingPackage;
056import org.nuxeo.connect.packages.PackageManager;
057import org.nuxeo.connect.packages.dependencies.DependencyResolution;
058import org.nuxeo.connect.packages.dependencies.TargetPlatformFilterHelper;
059import org.nuxeo.connect.update.LocalPackage;
060import org.nuxeo.connect.update.PackageDependency;
061import org.nuxeo.connect.update.PackageException;
062import org.nuxeo.connect.update.PackageState;
063import org.nuxeo.connect.update.PackageType;
064import org.nuxeo.connect.update.PackageUpdateService;
065import org.nuxeo.connect.update.ValidationStatus;
066import org.nuxeo.connect.update.task.Task;
067import org.nuxeo.ecm.admin.AdminViewManager;
068import org.nuxeo.ecm.admin.runtime.PlatformVersionHelper;
069import org.nuxeo.ecm.admin.setup.SetupWizardActionBean;
070import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
071import org.nuxeo.ecm.webapp.seam.NuxeoSeamHotReloadContextKeeper;
072import org.nuxeo.launcher.config.ConfigurationException;
073import org.nuxeo.launcher.config.ConfigurationGenerator;
074import org.nuxeo.runtime.api.Framework;
075import org.nuxeo.runtime.services.config.ConfigurationService;
076
077/**
078 * Manages JSF views for Package Management.
079 *
080 * @author <a href="mailto:td@nuxeo.com">Thierry Delprat</a>
081 */
082@Name("appsViews")
083@Scope(ScopeType.CONVERSATION)
084public class AppCenterViewsManager implements Serializable {
085
086    private static final long serialVersionUID = 1L;
087
088    protected static final Log log = LogFactory.getLog(AppCenterViewsManager.class);
089
090    private static final String LABEL_STUDIO_UPDATE_STATUS = "label.studio.update.status.";
091
092    /**
093     * FIXME JC: should follow or simply reuse {@link PackageState}
094     */
095    protected enum SnapshotStatus {
096        downloading, saving, installing, error, completed, restartNeeded;
097    }
098
099    protected static final Map<String, String> view2PackageListName = new HashMap<String, String>() {
100        private static final long serialVersionUID = 1L;
101        {
102            put("ConnectAppsUpdates", "updates");
103            put("ConnectAppsStudio", "studio");
104            put("ConnectAppsRemote", "remote");
105            put("ConnectAppsLocal", "local");
106        }
107    };
108
109    @In(create = true)
110    protected String currentAdminSubViewId;
111
112    @In(create = true)
113    protected NuxeoSeamHotReloadContextKeeper seamReloadContext;
114
115    @In(create = true)
116    protected SetupWizardActionBean setupWizardAction;
117
118    @In(create = true, required = false)
119    protected FacesMessages facesMessages;
120
121    @In(create = true)
122    protected Map<String, String> messages;
123
124    protected String searchString;
125
126    protected SnapshotStatus studioSnapshotStatus;
127
128    protected int studioSnapshotDownloadProgress;
129
130    protected boolean isStudioSnapshopUpdateInProgress = false;
131
132    protected String studioSnapshotUpdateError;
133
134    /**
135     * Boolean indicating is Studio snapshot package validation should be done.
136     *
137     * @since 5.7.1
138     */
139    protected Boolean validateStudioSnapshot;
140
141    /**
142     * Last validation status of the Studio snapshot package
143     *
144     * @since 5.7.1
145     */
146    protected ValidationStatus studioSnapshotValidationStatus;
147
148    private FileTime lastUpdate = null;
149
150    protected DownloadablePackage studioSnapshotPackage;
151
152    /**
153     * Using a dedicated property because studioSnapshotPackage might be null.
154     *
155     * @since 7.10
156     */
157    protected Boolean studioSnapshotPackageCached = false;
158
159    public String getSearchString() {
160        if (searchString == null) {
161            return "";
162        }
163        return searchString;
164    }
165
166    public void setSearchString(String searchString) {
167        this.searchString = searchString;
168    }
169
170    public boolean getOnlyRemote() {
171        return SharedPackageListingsSettings.instance().get("remote").isOnlyRemote();
172    }
173
174    public void setOnlyRemote(boolean onlyRemote) {
175        SharedPackageListingsSettings.instance().get("remote").setOnlyRemote(onlyRemote);
176    }
177
178    protected String getListName() {
179        return view2PackageListName.get(currentAdminSubViewId);
180    }
181
182    public void setPlatformFilter(boolean doFilter) {
183        SharedPackageListingsSettings.instance().get(getListName()).setPlatformFilter(doFilter);
184    }
185
186    public boolean getPlatformFilter() {
187        return SharedPackageListingsSettings.instance().get(getListName()).getPlatformFilter();
188    }
189
190    public String getPackageTypeFilter() {
191        return SharedPackageListingsSettings.instance().get(getListName()).getPackageTypeFilter();
192    }
193
194    public void setPackageTypeFilter(String filter) {
195        SharedPackageListingsSettings.instance().get(getListName()).setPackageTypeFilter(filter);
196    }
197
198    public List<SelectItem> getPackageTypes() {
199        List<SelectItem> types = new ArrayList<>();
200        SelectItem allItem = new SelectItem("", "label.packagetype.all");
201        types.add(allItem);
202        for (PackageType ptype : PackageType.values()) {
203            // if (!ptype.equals(PackageType.STUDIO)) {
204            SelectItem item = new SelectItem(ptype.getValue(), "label.packagetype." + ptype.getValue());
205            types.add(item);
206            // }
207        }
208        return types;
209    }
210
211    public void flushCache() {
212        PackageManager pm = Framework.getLocalService(PackageManager.class);
213        pm.flushCache();
214    }
215
216    /**
217     * Method binding for the update button: needs to perform a real redirection (as ajax context is broken after hot
218     * reload) and to provide an outcome so that redirection through the URL service goes ok (even if it just reset its
219     * navigation handler cache).
220     *
221     * @since 5.6
222     */
223    public String installStudioSnapshotAndRedirect() {
224        installStudioSnapshot();
225        return AdminViewManager.VIEW_ADMIN;
226    }
227
228    public void installStudioSnapshot() {
229        if (isStudioSnapshopUpdateInProgress) {
230            return;
231        }
232        PackageManager pm = Framework.getLocalService(PackageManager.class);
233        // TODO NXP-16228: should directly request the SNAPSHOT package (if only we knew its name!)
234        List<DownloadablePackage> pkgs = pm.listRemoteAssociatedStudioPackages();
235        DownloadablePackage snapshotPkg = StudioSnapshotHelper.getSnapshot(pkgs);
236        studioSnapshotUpdateError = null;
237        resetStudioSnapshotValidationStatus();
238        if (snapshotPkg != null) {
239            isStudioSnapshopUpdateInProgress = true;
240            try {
241                StudioAutoInstaller studioAutoInstaller = new StudioAutoInstaller(pm, snapshotPkg.getId(),
242                        shouldValidateStudioSnapshot());
243                studioAutoInstaller.run();
244            } finally {
245                isStudioSnapshopUpdateInProgress = false;
246            }
247        } else {
248            studioSnapshotUpdateError = translate("label.studio.update.error.noSnapshotPackageFound");
249        }
250    }
251
252    public boolean isStudioSnapshopUpdateInProgress() {
253        return isStudioSnapshopUpdateInProgress;
254    }
255
256    /**
257     * Returns true if validation should be performed
258     *
259     * @since 5.7.1
260     */
261    public Boolean getValidateStudioSnapshot() {
262        return validateStudioSnapshot;
263    }
264
265    /**
266     * @since 5.7.1
267     */
268    public void setValidateStudioSnapshot(Boolean validateStudioSnapshot) {
269        this.validateStudioSnapshot = validateStudioSnapshot;
270    }
271
272    /**
273     * Returns true if Studio snapshot module should be validated.
274     * <p>
275     * Validation can be skipped by user, or can be globally disabled by setting framework property
276     * "studio.snapshot.disablePkgValidation" to true.
277     *
278     * @since 5.7.1
279     */
280    protected boolean shouldValidateStudioSnapshot() {
281        ConfigurationService cs = Framework.getService(ConfigurationService.class);
282        if (cs.isBooleanPropertyTrue("studio.snapshot.disablePkgValidation")) {
283            return false;
284        }
285        return Boolean.TRUE.equals(getValidateStudioSnapshot());
286    }
287
288    protected static String translate(String label, Object... params) {
289        return ComponentUtils.translate(FacesContext.getCurrentInstance(), label, params);
290    }
291
292    protected FileTime getLastUpdateDate() {
293        if (lastUpdate == null) {
294            DownloadablePackage snapshotPkg = getStudioProjectSnapshot();
295            if (snapshotPkg != null) {
296                PackageUpdateService pus = Framework.getLocalService(PackageUpdateService.class);
297                try {
298                    LocalPackage pkg = pus.getPackage(snapshotPkg.getId());
299                    if (pkg != null) {
300                        lastUpdate = pus.getInstallDate(pkg.getId());
301                    }
302                } catch (PackageException e) {
303                    log.error(e);
304                }
305            }
306        }
307        return lastUpdate;
308    }
309
310    /**
311     * @since 7.10
312     */
313    public String getStudioUrl() {
314        return ConnectUrlConfig.getStudioUrl(getSnapshotStudioProjectName());
315    }
316
317    /**
318     * @since 7.10
319     */
320    public DownloadablePackage getStudioProjectSnapshot() {
321        if (!studioSnapshotPackageCached) {
322            PackageManager pm = Framework.getLocalService(PackageManager.class);
323            // TODO NXP-16228: should directly request the SNAPSHOT package (if only we knew its name!)
324            List<DownloadablePackage> pkgs = pm.listRemoteAssociatedStudioPackages();
325            studioSnapshotPackage = StudioSnapshotHelper.getSnapshot(pkgs);
326            studioSnapshotPackageCached = true;
327        }
328        return studioSnapshotPackage;
329    }
330
331    /**
332     * @return null if there is no SNAPSHOT package
333     * @since 7.10
334     */
335    public String getSnapshotStudioProjectName() {
336        DownloadablePackage snapshotPkg = getStudioProjectSnapshot();
337        if (snapshotPkg != null) {
338            return snapshotPkg.getName();
339        }
340        return null;
341    }
342
343    public String getStudioInstallationStatus() {
344        if (studioSnapshotStatus == null) {
345            LocalPackage pkg = null;
346            DownloadablePackage snapshotPkg = getStudioProjectSnapshot();
347            if (snapshotPkg != null) {
348                try {
349                    PackageUpdateService pus = Framework.getLocalService(PackageUpdateService.class);
350                    pkg = pus.getPackage(snapshotPkg.getId());
351                } catch (PackageException e) {
352                    log.error(e);
353                }
354            }
355            if (pkg == null) {
356                return translate(LABEL_STUDIO_UPDATE_STATUS + "noStatus");
357            }
358            PackageState studioPkgState = pkg.getPackageState();
359            if (studioPkgState == PackageState.DOWNLOADING) {
360                studioSnapshotStatus = SnapshotStatus.downloading;
361            } else if (studioPkgState == PackageState.DOWNLOADED) {
362                studioSnapshotStatus = SnapshotStatus.saving;
363            } else if (studioPkgState == PackageState.INSTALLING) {
364                studioSnapshotStatus = SnapshotStatus.installing;
365            } else if (studioPkgState.isInstalled()) {
366                studioSnapshotStatus = SnapshotStatus.completed;
367            } else {
368                studioSnapshotStatus = SnapshotStatus.error;
369            }
370        }
371
372        Object[] params = new Object[0];
373        if (SnapshotStatus.error.equals(studioSnapshotStatus)) {
374            if (studioSnapshotUpdateError == null) {
375                studioSnapshotUpdateError = "???";
376            }
377            params = new Object[] { studioSnapshotUpdateError };
378        } else if (SnapshotStatus.downloading.equals(studioSnapshotStatus)) {
379            params = new Object[] { String.valueOf(studioSnapshotDownloadProgress) };
380        } else {
381            FileTime update = getLastUpdateDate();
382            if (update != null) {
383                DateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
384                df.setTimeZone(TimeZone.getDefault());
385                params = new Object[] { df.format(new Date(update.toMillis())) };
386            }
387        }
388
389        return translate(LABEL_STUDIO_UPDATE_STATUS + studioSnapshotStatus.name(), params);
390    }
391
392    // TODO: plug a notifier for status to be shown to the user
393    protected class StudioAutoInstaller implements Runnable {
394
395        protected final String packageId;
396
397        protected final PackageManager pm;
398
399        /**
400         * @since 5.7.1
401         */
402        protected final boolean validate;
403
404        protected StudioAutoInstaller(PackageManager pm, String packageId, boolean validate) {
405            this.pm = pm;
406            this.packageId = packageId;
407            this.validate = validate;
408        }
409
410        @Override
411        public void run() {
412            if (validate) {
413                ValidationStatus status = new ValidationStatus();
414
415                pm.flushCache();
416                DownloadablePackage remotePkg = pm.findRemotePackageById(packageId);
417                if (remotePkg == null) {
418                    status.addError(String.format("Cannot perform validation: remote package '%s' not found", packageId));
419                    return;
420                }
421                PackageDependency[] pkgDeps = remotePkg.getDependencies();
422                if (log.isDebugEnabled()) {
423                    log.debug(String.format("%s target platforms: %s", remotePkg,
424                            ArrayUtils.toString(remotePkg.getTargetPlatforms())));
425                    log.debug(String.format("%s dependencies: %s", remotePkg, ArrayUtils.toString(pkgDeps)));
426                }
427
428                // TODO NXP-11776: replace errors by internationalized
429                // labels
430                String targetPlatform = PlatformVersionHelper.getPlatformFilter();
431                if (!TargetPlatformFilterHelper.isCompatibleWithTargetPlatform(remotePkg, targetPlatform)) {
432                    status.addError(String.format("This package is not validated for your current platform: %s",
433                            targetPlatform));
434                }
435                // check deps requirements
436                if (pkgDeps != null && pkgDeps.length > 0) {
437                    DependencyResolution resolution = pm.resolveDependencies(packageId, targetPlatform);
438                    if (resolution.isFailed() && targetPlatform != null) {
439                        // retry without PF filter in case it gives more
440                        // information
441                        resolution = pm.resolveDependencies(packageId, null);
442                    }
443                    if (resolution.isFailed()) {
444                        status.addError(String.format("Dependency check has failed for package '%s' (%s)", packageId,
445                                resolution));
446                    } else {
447                        List<String> pkgToInstall = resolution.getInstallPackageIds();
448                        if (pkgToInstall != null && pkgToInstall.size() == 1 && packageId.equals(pkgToInstall.get(0))) {
449                            // ignore
450                        } else if (resolution.requireChanges()) {
451                            // do not install needed deps: they may not be
452                            // hot-reloadable and that's not what the
453                            // "update snapshot" button is for.
454                            status.addError(resolution.toString().trim().replaceAll("\n", "<br />"));
455                        }
456                    }
457                }
458
459                if (status.hasErrors()) {
460                    setStatus(SnapshotStatus.error, translate("label.studio.update.validation.error"), status);
461                    return;
462                }
463            }
464
465            // Effective install
466            if (Framework.isDevModeSet()) {
467                try {
468                    PackageUpdateService pus = Framework.getLocalService(PackageUpdateService.class);
469                    LocalPackage pkg = pus.getPackage(packageId);
470
471                    // Uninstall and/or remove if needed
472                    if (pkg != null) {
473                        log.info(String.format("Updating package %s...", pkg));
474                        if (pkg.getPackageState().isInstalled()) {
475                            // First remove it to allow SNAPSHOT upgrade
476                            log.info("Uninstalling " + packageId);
477                            Task uninstallTask = pkg.getUninstallTask();
478                            try {
479                                performTask(uninstallTask);
480                            } catch (PackageException e) {
481                                uninstallTask.rollback();
482                                throw e;
483                            }
484                        }
485                        pus.removePackage(packageId);
486                    }
487
488                    // Download
489                    setStatus(SnapshotStatus.downloading, null);
490                    DownloadingPackage downloadingPkg;
491                    try {
492                        downloadingPkg = pm.download(packageId);
493                    } catch (ConnectServerError e) {
494                        setStatus(SnapshotStatus.error, e.getMessage());
495                        return;
496                    }
497                    try {
498                        while (!downloadingPkg.isCompleted()) {
499                            studioSnapshotDownloadProgress = downloadingPkg.getDownloadProgress();
500                            Thread.sleep(100);
501                            log.debug("downloading studio snapshot package");
502                        }
503                        log.debug("studio snapshot package download completed, starting installation");
504                        Thread.sleep(200);
505                        setStatus(SnapshotStatus.saving, null);
506                        // FIXME JC: Is this a workaround for some issue?
507                        // downloadingPkg.isCompleted() is true!
508                        while (pus.getPackage(downloadingPkg.getId()) == null) {
509                            studioSnapshotDownloadProgress = downloadingPkg.getDownloadProgress();
510                            Thread.sleep(50);
511                            log.debug("downloading studio snapshot package");
512                        }
513                    } catch (PackageException | InterruptedException e) {
514                        ExceptionUtils.checkInterrupt(e);
515                        log.error("Error while downloading studio snapshot", e);
516                        setStatus(SnapshotStatus.error,
517                                translate("label.studio.update.downloading.error", e.getMessage()));
518                        return;
519                    }
520
521                    // Install
522                    setStatus(SnapshotStatus.installing, null);
523                    log.info("Installing " + packageId);
524                    pkg = pus.getPackage(packageId);
525                    Task installTask = pkg.getInstallTask();
526                    try {
527                        performTask(installTask);
528                    } catch (PackageException e) {
529                        installTask.rollback();
530                        throw e;
531                    }
532                    // Refresh state
533                    pkg = pus.getPackage(packageId);
534                    lastUpdate = pus.getInstallDate(packageId);
535                    setStatus(SnapshotStatus.completed, null);
536                } catch (PackageException e) {
537                    log.error("Error while installing studio snapshot", e);
538                    setStatus(SnapshotStatus.error, translate("label.studio.update.installation.error", e.getMessage()));
539                }
540            } else {
541                InstallAfterRestart.addPackageForInstallation(packageId);
542                setStatus(SnapshotStatus.restartNeeded, null);
543                setupWizardAction.setNeedsRestart(true);
544            }
545        }
546
547        protected void performTask(Task task) throws PackageException {
548            ValidationStatus validationStatus = task.validate();
549            if (validationStatus.hasErrors()) {
550                throw new PackageException("Failed to validate package " + task.getPackage().getId() + " -> "
551                        + validationStatus.getErrors());
552            }
553            if (validationStatus.hasWarnings()) {
554                log.warn("Got warnings on package validation " + task.getPackage().getId() + " -> "
555                        + validationStatus.getWarnings());
556            }
557            task.run(null);
558        }
559    }
560
561    protected void setStatus(SnapshotStatus status, String errorMessage) {
562        studioSnapshotStatus = status;
563        studioSnapshotUpdateError = errorMessage;
564    }
565
566    protected void setStatus(SnapshotStatus status, String errorMessage, ValidationStatus validationStatus) {
567        setStatus(status, errorMessage);
568        setStudioSnapshotValidationStatus(validationStatus);
569    }
570
571    /**
572     * @since 5.7.1
573     */
574    public ValidationStatus getStudioSnapshotValidationStatus() {
575        return studioSnapshotValidationStatus;
576    }
577
578    /**
579     * @since 5.7.1
580     */
581    public void setStudioSnapshotValidationStatus(ValidationStatus status) {
582        studioSnapshotValidationStatus = status;
583    }
584
585    /**
586     * @since 5.7.1
587     */
588    public void resetStudioSnapshotValidationStatus() {
589        setStudioSnapshotValidationStatus(null);
590    }
591
592    public void setDevMode(boolean value) {
593        String feedbackCompId = "changeDevModeForm";
594        ConfigurationGenerator conf = setupWizardAction.getConfigurationGenerator();
595        boolean configurable = conf.isConfigurable();
596        if (!configurable) {
597            facesMessages.addToControl(feedbackCompId, StatusMessage.Severity.ERROR,
598                    translate("label.setup.nuxeo.org.nuxeo.dev.changingDevModeNotConfigurable"));
599            return;
600        }
601        Map<String, String> params = new HashMap<>();
602        params.put(Framework.NUXEO_DEV_SYSTEM_PROP, Boolean.toString(value));
603        try {
604            conf.saveFilteredConfiguration(params);
605            conf.getServerConfigurator().dumpProperties(conf.getUserConfig());
606            // force reload of framework properties to ensure it's immediately
607            // taken into account by all code checking for
608            // Framework#isDevModeSet
609            Framework.getRuntime().reloadProperties();
610
611            if (value) {
612                facesMessages.addToControl(feedbackCompId, StatusMessage.Severity.WARN,
613                        translate("label.admin.center.devMode.justActivated"));
614            } else {
615                facesMessages.addToControl(feedbackCompId, StatusMessage.Severity.INFO,
616                        translate("label.admin.center.devMode.justDisabled"));
617            }
618        } catch (ConfigurationException | IOException e) {
619            log.error(e, e);
620            facesMessages.addToControl(feedbackCompId, StatusMessage.Severity.ERROR,
621                    translate("label.admin.center.devMode.errorSaving", e.getMessage()));
622        } finally {
623            setupWizardAction.setNeedsRestart(true);
624            setupWizardAction.resetParameters();
625            Contexts.getEventContext().remove("nxDevModeSet");
626        }
627    }
628}