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