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