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}