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