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