/*
* (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Nuxeo - initial API and implementation
*
*/
package org.nuxeo.connect.client.jsf;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.attribute.FileTime;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import javax.faces.context.FacesContext;
import javax.faces.model.SelectItem;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.faces.FacesMessages;
import org.jboss.seam.international.StatusMessage;
import org.nuxeo.common.utils.ExceptionUtils;
import org.nuxeo.connect.client.ui.SharedPackageListingsSettings;
import org.nuxeo.connect.client.vindoz.InstallAfterRestart;
import org.nuxeo.connect.client.we.StudioSnapshotHelper;
import org.nuxeo.connect.connector.ConnectServerError;
import org.nuxeo.connect.connector.http.ConnectUrlConfig;
import org.nuxeo.connect.data.DownloadablePackage;
import org.nuxeo.connect.data.DownloadingPackage;
import org.nuxeo.connect.packages.PackageManager;
import org.nuxeo.connect.packages.dependencies.DependencyResolution;
import org.nuxeo.connect.packages.dependencies.TargetPlatformFilterHelper;
import org.nuxeo.connect.update.LocalPackage;
import org.nuxeo.connect.update.PackageDependency;
import org.nuxeo.connect.update.PackageException;
import org.nuxeo.connect.update.PackageState;
import org.nuxeo.connect.update.PackageType;
import org.nuxeo.connect.update.PackageUpdateService;
import org.nuxeo.connect.update.ValidationStatus;
import org.nuxeo.connect.update.task.Task;
import org.nuxeo.ecm.admin.AdminViewManager;
import org.nuxeo.ecm.admin.runtime.PlatformVersionHelper;
import org.nuxeo.ecm.admin.setup.SetupWizardActionBean;
import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
import org.nuxeo.ecm.webapp.seam.NuxeoSeamHotReloadContextKeeper;
import org.nuxeo.launcher.config.ConfigurationException;
import org.nuxeo.launcher.config.ConfigurationGenerator;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.services.config.ConfigurationService;
/**
* Manages JSF views for Package Management.
*
* @author <a href="mailto:td@nuxeo.com">Thierry Delprat</a>
*/
@Name("appsViews")
@Scope(ScopeType.CONVERSATION)
public class AppCenterViewsManager implements Serializable {
private static final long serialVersionUID = 1L;
protected static final Log log = LogFactory.getLog(AppCenterViewsManager.class);
private static final String LABEL_STUDIO_UPDATE_STATUS = "label.studio.update.status.";
/**
* FIXME JC: should follow or simply reuse {@link PackageState}
*/
protected enum SnapshotStatus {
downloading, saving, installing, error, completed, restartNeeded;
}
protected static final Map<String, String> view2PackageListName = new HashMap<String, String>() {
private static final long serialVersionUID = 1L;
{
put("ConnectAppsUpdates", "updates");
put("ConnectAppsStudio", "studio");
put("ConnectAppsRemote", "remote");
put("ConnectAppsLocal", "local");
}
};
@In(create = true)
protected String currentAdminSubViewId;
@In(create = true)
protected NuxeoSeamHotReloadContextKeeper seamReloadContext;
@In(create = true)
protected SetupWizardActionBean setupWizardAction;
@In(create = true, required = false)
protected FacesMessages facesMessages;
@In(create = true)
protected Map<String, String> messages;
protected String searchString;
protected SnapshotStatus studioSnapshotStatus;
protected int studioSnapshotDownloadProgress;
protected boolean isStudioSnapshopUpdateInProgress = false;
protected String studioSnapshotUpdateError;
/**
* Boolean indicating is Studio snapshot package validation should be done.
*
* @since 5.7.1
*/
protected Boolean validateStudioSnapshot;
/**
* Last validation status of the Studio snapshot package
*
* @since 5.7.1
*/
protected ValidationStatus studioSnapshotValidationStatus;
private FileTime lastUpdate = null;
protected DownloadablePackage studioSnapshotPackage;
/**
* Using a dedicated property because studioSnapshotPackage might be null.
*
* @since 7.10
*/
protected Boolean studioSnapshotPackageCached = false;
public String getSearchString() {
if (searchString == null) {
return "";
}
return searchString;
}
public void setSearchString(String searchString) {
this.searchString = searchString;
}
public boolean getOnlyRemote() {
return SharedPackageListingsSettings.instance().get("remote").isOnlyRemote();
}
public void setOnlyRemote(boolean onlyRemote) {
SharedPackageListingsSettings.instance().get("remote").setOnlyRemote(onlyRemote);
}
protected String getListName() {
return view2PackageListName.get(currentAdminSubViewId);
}
public void setPlatformFilter(boolean doFilter) {
SharedPackageListingsSettings.instance().get(getListName()).setPlatformFilter(doFilter);
}
public boolean getPlatformFilter() {
return SharedPackageListingsSettings.instance().get(getListName()).getPlatformFilter();
}
public String getPackageTypeFilter() {
return SharedPackageListingsSettings.instance().get(getListName()).getPackageTypeFilter();
}
public void setPackageTypeFilter(String filter) {
SharedPackageListingsSettings.instance().get(getListName()).setPackageTypeFilter(filter);
}
public List<SelectItem> getPackageTypes() {
List<SelectItem> types = new ArrayList<>();
SelectItem allItem = new SelectItem("", "label.packagetype.all");
types.add(allItem);
for (PackageType ptype : PackageType.values()) {
// if (!ptype.equals(PackageType.STUDIO)) {
SelectItem item = new SelectItem(ptype.getValue(), "label.packagetype." + ptype.getValue());
types.add(item);
// }
}
return types;
}
public void flushCache() {
PackageManager pm = Framework.getLocalService(PackageManager.class);
pm.flushCache();
}
/**
* Method binding for the update button: needs to perform a real redirection (as ajax context is broken after hot
* reload) and to provide an outcome so that redirection through the URL service goes ok (even if it just reset its
* navigation handler cache).
*
* @since 5.6
*/
public String installStudioSnapshotAndRedirect() {
installStudioSnapshot();
return AdminViewManager.VIEW_ADMIN;
}
public void installStudioSnapshot() {
if (isStudioSnapshopUpdateInProgress) {
return;
}
PackageManager pm = Framework.getLocalService(PackageManager.class);
// TODO NXP-16228: should directly request the SNAPSHOT package (if only we knew its name!)
List<DownloadablePackage> pkgs = pm.listRemoteAssociatedStudioPackages();
DownloadablePackage snapshotPkg = StudioSnapshotHelper.getSnapshot(pkgs);
studioSnapshotUpdateError = null;
resetStudioSnapshotValidationStatus();
if (snapshotPkg != null) {
isStudioSnapshopUpdateInProgress = true;
try {
StudioAutoInstaller studioAutoInstaller = new StudioAutoInstaller(pm, snapshotPkg.getId(),
shouldValidateStudioSnapshot());
studioAutoInstaller.run();
} finally {
isStudioSnapshopUpdateInProgress = false;
}
} else {
studioSnapshotUpdateError = translate("label.studio.update.error.noSnapshotPackageFound");
}
}
public boolean isStudioSnapshopUpdateInProgress() {
return isStudioSnapshopUpdateInProgress;
}
/**
* Returns true if validation should be performed
*
* @since 5.7.1
*/
public Boolean getValidateStudioSnapshot() {
return validateStudioSnapshot;
}
/**
* @since 5.7.1
*/
public void setValidateStudioSnapshot(Boolean validateStudioSnapshot) {
this.validateStudioSnapshot = validateStudioSnapshot;
}
/**
* Returns true if Studio snapshot module should be validated.
* <p>
* Validation can be skipped by user, or can be globally disabled by setting framework property
* "studio.snapshot.disablePkgValidation" to true.
*
* @since 5.7.1
*/
protected boolean shouldValidateStudioSnapshot() {
ConfigurationService cs = Framework.getService(ConfigurationService.class);
if (cs.isBooleanPropertyTrue("studio.snapshot.disablePkgValidation")) {
return false;
}
return Boolean.TRUE.equals(getValidateStudioSnapshot());
}
protected static String translate(String label, Object... params) {
return ComponentUtils.translate(FacesContext.getCurrentInstance(), label, params);
}
protected FileTime getLastUpdateDate() {
if (lastUpdate == null) {
DownloadablePackage snapshotPkg = getStudioProjectSnapshot();
if (snapshotPkg != null) {
PackageUpdateService pus = Framework.getLocalService(PackageUpdateService.class);
try {
LocalPackage pkg = pus.getPackage(snapshotPkg.getId());
if (pkg != null) {
lastUpdate = pus.getInstallDate(pkg.getId());
}
} catch (PackageException e) {
log.error(e);
}
}
}
return lastUpdate;
}
/**
* @since 7.10
*/
public String getStudioUrl() {
return ConnectUrlConfig.getStudioUrl(getSnapshotStudioProjectName());
}
/**
* @since 7.10
*/
public DownloadablePackage getStudioProjectSnapshot() {
if (!studioSnapshotPackageCached) {
PackageManager pm = Framework.getLocalService(PackageManager.class);
// TODO NXP-16228: should directly request the SNAPSHOT package (if only we knew its name!)
List<DownloadablePackage> pkgs = pm.listRemoteAssociatedStudioPackages();
studioSnapshotPackage = StudioSnapshotHelper.getSnapshot(pkgs);
studioSnapshotPackageCached = true;
}
return studioSnapshotPackage;
}
/**
* @return null if there is no SNAPSHOT package
* @since 7.10
*/
public String getSnapshotStudioProjectName() {
DownloadablePackage snapshotPkg = getStudioProjectSnapshot();
if (snapshotPkg != null) {
return snapshotPkg.getName();
}
return null;
}
public String getStudioInstallationStatus() {
if (studioSnapshotStatus == null) {
LocalPackage pkg = null;
DownloadablePackage snapshotPkg = getStudioProjectSnapshot();
if (snapshotPkg != null) {
try {
PackageUpdateService pus = Framework.getLocalService(PackageUpdateService.class);
pkg = pus.getPackage(snapshotPkg.getId());
} catch (PackageException e) {
log.error(e);
}
}
if (pkg == null) {
return translate(LABEL_STUDIO_UPDATE_STATUS + "noStatus");
}
PackageState studioPkgState = pkg.getPackageState();
if (studioPkgState == PackageState.DOWNLOADING) {
studioSnapshotStatus = SnapshotStatus.downloading;
} else if (studioPkgState == PackageState.DOWNLOADED) {
studioSnapshotStatus = SnapshotStatus.saving;
} else if (studioPkgState == PackageState.INSTALLING) {
studioSnapshotStatus = SnapshotStatus.installing;
} else if (studioPkgState.isInstalled()) {
studioSnapshotStatus = SnapshotStatus.completed;
} else {
studioSnapshotStatus = SnapshotStatus.error;
}
}
Object[] params = new Object[0];
if (SnapshotStatus.error.equals(studioSnapshotStatus)) {
if (studioSnapshotUpdateError == null) {
studioSnapshotUpdateError = "???";
}
params = new Object[] { studioSnapshotUpdateError };
} else if (SnapshotStatus.downloading.equals(studioSnapshotStatus)) {
params = new Object[] { String.valueOf(studioSnapshotDownloadProgress) };
} else {
FileTime update = getLastUpdateDate();
if (update != null) {
DateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
df.setTimeZone(TimeZone.getDefault());
params = new Object[] { df.format(new Date(update.toMillis())) };
}
}
return translate(LABEL_STUDIO_UPDATE_STATUS + studioSnapshotStatus.name(), params);
}
// TODO: plug a notifier for status to be shown to the user
protected class StudioAutoInstaller implements Runnable {
protected final String packageId;
protected final PackageManager pm;
/**
* @since 5.7.1
*/
protected final boolean validate;
protected StudioAutoInstaller(PackageManager pm, String packageId, boolean validate) {
this.pm = pm;
this.packageId = packageId;
this.validate = validate;
}
@Override
public void run() {
if (validate) {
ValidationStatus status = new ValidationStatus();
pm.flushCache();
DownloadablePackage remotePkg = pm.findRemotePackageById(packageId);
if (remotePkg == null) {
status.addError(String.format("Cannot perform validation: remote package '%s' not found", packageId));
return;
}
PackageDependency[] pkgDeps = remotePkg.getDependencies();
if (log.isDebugEnabled()) {
log.debug(String.format("%s target platforms: %s", remotePkg,
ArrayUtils.toString(remotePkg.getTargetPlatforms())));
log.debug(String.format("%s dependencies: %s", remotePkg, ArrayUtils.toString(pkgDeps)));
}
// TODO NXP-11776: replace errors by internationalized labels
String targetPlatform = PlatformVersionHelper.getPlatformFilter();
if (!TargetPlatformFilterHelper.isCompatibleWithTargetPlatform(remotePkg, targetPlatform)) {
status.addError(String.format("This package is not validated for your current platform: %s",
targetPlatform));
}
// check deps requirements
if (pkgDeps != null && pkgDeps.length > 0) {
DependencyResolution resolution = pm.resolveDependencies(packageId, targetPlatform);
if (resolution.isFailed() && targetPlatform != null) {
// retry without PF filter in case it gives more information
resolution = pm.resolveDependencies(packageId, null);
}
if (resolution.isFailed()) {
status.addError(String.format("Dependency check has failed for package '%s' (%s)", packageId,
resolution));
} else {
List<String> pkgToInstall = resolution.getInstallPackageIds();
if (pkgToInstall != null && pkgToInstall.size() == 1 && packageId.equals(pkgToInstall.get(0))) {
// ignore
} else if (resolution.requireChanges()) {
// do not install needed deps: they may not be hot-reloadable and that's not what the
// "update snapshot" button is for.
status.addError(resolution.toString().trim().replaceAll("\n", "<br />"));
}
}
}
if (status.hasErrors()) {
setStatus(SnapshotStatus.error, translate("label.studio.update.validation.error"), status);
return;
}
}
// Effective install
if (Framework.isDevModeSet()) {
try {
PackageUpdateService pus = Framework.getLocalService(PackageUpdateService.class);
LocalPackage pkg = pus.getPackage(packageId);
// Uninstall and/or remove if needed
if (pkg != null) {
log.info(String.format("Removing package %s before update...", pkg));
if (pkg.getPackageState().isInstalled()) {
// First remove it to allow SNAPSHOT upgrade
log.info("Uninstalling " + packageId);
Task uninstallTask = pkg.getUninstallTask();
try {
performTask(uninstallTask);
} catch (PackageException e) {
uninstallTask.rollback();
throw e;
}
}
pus.removePackage(packageId);
}
// Download
setStatus(SnapshotStatus.downloading, null);
DownloadingPackage downloadingPkg = pm.download(packageId);
while (!downloadingPkg.isCompleted()) {
studioSnapshotDownloadProgress = downloadingPkg.getDownloadProgress();
log.debug("downloading studio snapshot package");
Thread.sleep(100);
}
studioSnapshotDownloadProgress = downloadingPkg.getDownloadProgress();
setStatus(SnapshotStatus.saving, null);
// Install
setStatus(SnapshotStatus.installing, null);
log.info("Installing " + packageId);
pkg = pus.getPackage(packageId);
if (pkg == null || PackageState.DOWNLOADED != pkg.getPackageState()) {
log.error("Error while downloading studio snapshot " + pkg);
setStatus(SnapshotStatus.error, translate("label.studio.update.downloading.error", pkg));
return;
}
Task installTask = pkg.getInstallTask();
try {
performTask(installTask);
} catch (PackageException e) {
installTask.rollback();
throw e;
}
// Refresh state
pkg = pus.getPackage(packageId);
lastUpdate = pus.getInstallDate(packageId);
setStatus(SnapshotStatus.completed, null);
} catch (ConnectServerError e) {
setStatus(SnapshotStatus.error, e.getMessage());
} catch (InterruptedException e) {
log.error("Error while downloading studio snapshot", e);
setStatus(SnapshotStatus.error, translate("label.studio.update.downloading.error", e.getMessage()));
ExceptionUtils.checkInterrupt(e);
} catch (PackageException e) {
log.error("Error while installing studio snapshot", e);
setStatus(SnapshotStatus.error, translate("label.studio.update.installation.error", e.getMessage()));
}
} else {
InstallAfterRestart.addPackageForInstallation(packageId);
setStatus(SnapshotStatus.restartNeeded, null);
setupWizardAction.setNeedsRestart(true);
}
}
protected void performTask(Task task) throws PackageException {
ValidationStatus validationStatus = task.validate();
if (validationStatus.hasErrors()) {
throw new PackageException("Failed to validate package " + task.getPackage().getId() + " -> "
+ validationStatus.getErrors());
}
if (validationStatus.hasWarnings()) {
log.warn("Got warnings on package validation " + task.getPackage().getId() + " -> "
+ validationStatus.getWarnings());
}
task.run(null);
}
}
protected void setStatus(SnapshotStatus status, String errorMessage) {
studioSnapshotStatus = status;
studioSnapshotUpdateError = errorMessage;
}
protected void setStatus(SnapshotStatus status, String errorMessage, ValidationStatus validationStatus) {
setStatus(status, errorMessage);
setStudioSnapshotValidationStatus(validationStatus);
}
/**
* @since 5.7.1
*/
public ValidationStatus getStudioSnapshotValidationStatus() {
return studioSnapshotValidationStatus;
}
/**
* @since 5.7.1
*/
public void setStudioSnapshotValidationStatus(ValidationStatus status) {
studioSnapshotValidationStatus = status;
}
/**
* @since 5.7.1
*/
public void resetStudioSnapshotValidationStatus() {
setStudioSnapshotValidationStatus(null);
}
public void setDevMode(boolean value) {
String feedbackCompId = "changeDevModeForm";
ConfigurationGenerator conf = setupWizardAction.getConfigurationGenerator();
boolean configurable = conf.isConfigurable();
if (!configurable) {
facesMessages.addToControl(feedbackCompId, StatusMessage.Severity.ERROR,
translate("label.setup.nuxeo.org.nuxeo.dev.changingDevModeNotConfigurable"));
return;
}
Map<String, String> params = new HashMap<>();
params.put(Framework.NUXEO_DEV_SYSTEM_PROP, Boolean.toString(value));
try {
conf.saveFilteredConfiguration(params);
conf.getServerConfigurator().dumpProperties(conf.getUserConfig());
// force reload of framework properties to ensure it's immediately
// taken into account by all code checking for
// Framework#isDevModeSet
Framework.getRuntime().reloadProperties();
if (value) {
facesMessages.addToControl(feedbackCompId, StatusMessage.Severity.WARN,
translate("label.admin.center.devMode.justActivated"));
} else {
facesMessages.addToControl(feedbackCompId, StatusMessage.Severity.INFO,
translate("label.admin.center.devMode.justDisabled"));
}
} catch (ConfigurationException | IOException e) {
log.error(e, e);
facesMessages.addToControl(feedbackCompId, StatusMessage.Severity.ERROR,
translate("label.admin.center.devMode.errorSaving", e.getMessage()));
} finally {
setupWizardAction.setNeedsRestart(true);
setupWizardAction.resetParameters();
Contexts.getEventContext().remove("nxDevModeSet");
}
}
}