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}