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