001/* 002 * Copyright 2017 Anyware Services 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 */ 016package org.ametys.plugins.workspaces; 017 018import java.time.ZonedDateTime; 019import java.util.ArrayList; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Optional; 024import java.util.Set; 025 026import org.apache.avalon.framework.context.Context; 027import org.apache.avalon.framework.context.ContextException; 028import org.apache.avalon.framework.context.Contextualizable; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.avalon.framework.service.Serviceable; 032import org.apache.commons.lang3.ArrayUtils; 033import org.apache.commons.lang3.StringUtils; 034 035import org.ametys.cms.transformation.xslt.ResolveURIComponent; 036import org.ametys.core.observation.Event; 037import org.ametys.core.observation.ObservationManager; 038import org.ametys.core.right.RightManager; 039import org.ametys.core.user.CurrentUserProvider; 040import org.ametys.core.user.UserManager; 041import org.ametys.core.util.I18nUtils; 042import org.ametys.plugins.core.user.UserHelper; 043import org.ametys.plugins.explorer.ExplorerNode; 044import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 045import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory; 046import org.ametys.plugins.repository.AmetysObject; 047import org.ametys.plugins.repository.AmetysObjectIterable; 048import org.ametys.plugins.repository.AmetysObjectResolver; 049import org.ametys.plugins.repository.AmetysRepositoryException; 050import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 051import org.ametys.plugins.repository.activities.Activity; 052import org.ametys.plugins.repository.activities.ActivityHelper; 053import org.ametys.plugins.repository.activities.ActivityTypeExpression; 054import org.ametys.plugins.repository.query.expression.AndExpression; 055import org.ametys.plugins.repository.query.expression.Expression; 056import org.ametys.plugins.repository.query.expression.Expression.Operator; 057import org.ametys.plugins.repository.query.expression.OrExpression; 058import org.ametys.plugins.repository.query.expression.StringExpression; 059import org.ametys.plugins.workspaces.activities.AbstractWorkspacesActivityType; 060import org.ametys.plugins.workspaces.activities.activitystream.ActivityStreamClientInteraction; 061import org.ametys.plugins.workspaces.project.ProjectConstants; 062import org.ametys.plugins.workspaces.project.ProjectManager; 063import org.ametys.plugins.workspaces.project.modules.WorkspaceModule; 064import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint; 065import org.ametys.plugins.workspaces.project.objects.Project; 066import org.ametys.plugins.workspaces.project.rights.ProjectRightHelper; 067import org.ametys.plugins.workspaces.util.StatisticColumn; 068import org.ametys.plugins.workspaces.util.StatisticsColumnType; 069import org.ametys.runtime.i18n.I18nizableText; 070import org.ametys.runtime.model.ElementDefinition; 071import org.ametys.runtime.plugin.component.AbstractLogEnabled; 072import org.ametys.runtime.plugin.component.PluginAware; 073import org.ametys.web.ObservationConstants; 074import org.ametys.web.repository.page.LockablePage; 075import org.ametys.web.repository.page.ModifiablePage; 076import org.ametys.web.repository.page.MoveablePage; 077import org.ametys.web.repository.page.Page; 078import org.ametys.web.repository.page.Page.PageType; 079import org.ametys.web.repository.page.PageDAO; 080import org.ametys.web.repository.site.Site; 081import org.ametys.web.repository.sitemap.Sitemap; 082import org.ametys.web.service.Service; 083import org.ametys.web.service.ServiceExtensionPoint; 084import org.ametys.web.skin.Skin; 085import org.ametys.web.skin.SkinTemplate; 086import org.ametys.web.skin.SkinsManager; 087 088/** 089 * Abstract class for {@link WorkspaceModule} implementation 090 * 091 */ 092public abstract class AbstractWorkspaceModule extends AbstractLogEnabled implements WorkspaceModule, Serviceable, Contextualizable, PluginAware 093{ 094 095 /** Size value constants in case of size computation error */ 096 protected static final Long __SIZE_ERROR = -1L; 097 098 /** Size value constants for inactive modules */ 099 protected static final Long __SIZE_INACTIVE = -2L; 100 101 /** Project manager */ 102 protected ProjectManager _projectManager; 103 /** Project right helper */ 104 protected ProjectRightHelper _projectRightHelper; 105 /** User manager */ 106 protected UserManager _userManager; 107 /** Ametys resolver */ 108 protected AmetysObjectResolver _resolver; 109 /** The rights manager */ 110 protected RightManager _rightManager; 111 /** Observer manager. */ 112 protected ObservationManager _observationManager; 113 /** The current user provider. */ 114 protected CurrentUserProvider _currentUserProvider; 115 /** The users manager */ 116 protected UserHelper _userHelper; 117 /** The i18n utils. */ 118 protected I18nUtils _i18nUtils; 119 /** The skins manager. */ 120 protected SkinsManager _skinsManager; 121 /** The page DAO */ 122 protected PageDAO _pageDAO; 123 /** The avalon context */ 124 protected Context _context; 125 /** The plugin name */ 126 protected String _pluginName; 127 /** The services handler */ 128 protected ServiceExtensionPoint _serviceEP; 129 /** The modules extension point */ 130 protected WorkspaceModuleExtensionPoint _modulesEP; 131 /** The activity stream manager */ 132 protected ActivityStreamClientInteraction _activityStream; 133 /** Workspaces helper */ 134 protected WorkspacesHelper _wokspacesHelper; 135 136 @Override 137 public void service(ServiceManager manager) throws ServiceException 138 { 139 _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE); 140 _projectRightHelper = (ProjectRightHelper) manager.lookup(ProjectRightHelper.ROLE); 141 _wokspacesHelper = (WorkspacesHelper) manager.lookup(WorkspacesHelper.ROLE); 142 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 143 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 144 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 145 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 146 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 147 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 148 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 149 _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE); 150 _pageDAO = (PageDAO) manager.lookup(PageDAO.ROLE); 151 _serviceEP = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE); 152 _modulesEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE); 153 _activityStream = (ActivityStreamClientInteraction) manager.lookup(ActivityStreamClientInteraction.ROLE); 154 } 155 156 @Override 157 public void contextualize(Context context) throws ContextException 158 { 159 _context = context; 160 } 161 162 public void setPluginInfo(String pluginName, String featureName, String id) 163 { 164 _pluginName = pluginName; 165 } 166 167 @Override 168 public void deleteData(Project project) 169 { 170 // Delete module pages 171 _deletePages(project); 172 173 _internalDeleteData(project); 174 175 // Delete root 176 ModifiableResourceCollection moduleRoot = getModuleRoot(project, false); 177 if (moduleRoot != null) 178 { 179 moduleRoot.remove(); 180 } 181 182 // Delete activities 183 _deleteActivities(project); 184 } 185 186 @Override 187 public void deactivateModule(Project project) 188 { 189 // Hide module pages 190 _setPagesVisibility(project, false); 191 192 _internalDeactivateModule(project); 193 } 194 195 @Override 196 public void activateModule(Project project, Map<String, Object> additionalValues) 197 { 198 // create the resources root node 199 getModuleRoot(project, true); 200 _internalActivateModule(project, additionalValues); 201 202 Site site = project.getSite(); 203 if (site != null) 204 { 205 for (Sitemap sitemap : site.getSitemaps()) 206 { 207 initializeSitemap(project, sitemap); 208 } 209 } 210 211 _setPagesVisibility(project, true); 212 } 213 214 @Override 215 public void initializeSitemap(Project project, Sitemap sitemap) 216 { 217 ModifiablePage page = _createModulePage(project, sitemap, getModulePageName(), getModulePageTitle(), getModulePageTemplate()); 218 219 if (page != null) 220 { 221 page.tag("SECTION"); 222 _projectManager.tagProjectPage(page, getModuleRoot(project, true)); 223 224 initializeModulePage(page); 225 226 page.saveChanges(); 227 228 Map<String, Object> eventParams = new HashMap<>(); 229 eventParams.put(ObservationConstants.ARGS_PAGE, page); 230 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_ADDED, _currentUserProvider.getUser(), eventParams)); 231 } 232 } 233 234 @Override 235 public String getModuleUrl(Project project) 236 { 237 Optional<String> url = _projectManager.getModulePages(project, this).stream() 238 .findFirst() 239 .map(page -> ResolveURIComponent.resolve("page", page.getId())); 240 241 if (url.isPresent()) 242 { 243 return url.get(); 244 } 245 else 246 { 247 // No page found 248 return null; 249 } 250 } 251 252 /** 253 * Create a new page if not already exists 254 * @param project The module project 255 * @param sitemap The sitemap where the page will be created 256 * @param name The page's name 257 * @param pageTitle The page's title as i18nizable text 258 * @param skinTemplate The template from the skin to apply on the page 259 * @return the created page or <code>null</code> if page already exists 260 */ 261 protected ModifiablePage _createModulePage(Project project, Sitemap sitemap, String name, I18nizableText pageTitle, String skinTemplate) 262 { 263 if (!sitemap.hasChild(name)) 264 { 265 ModifiablePage page = sitemap.createChild(name, "ametys:defaultPage"); 266 267 // Title should not be missing, but just in case if the i18n message or the whole catalog does not exists in the requested language 268 // to prevent a non-user-friendly error and still generate the project workspace. 269 page.setTitle(StringUtils.defaultIfEmpty(_i18nUtils.translate(pageTitle, sitemap.getName()), "Missing title")); 270 page.setType(PageType.NODE); 271 page.setSiteName(sitemap.getSiteName()); 272 page.setSitemapName(sitemap.getName()); 273 274 Site site = page.getSite(); 275 Skin skin = _skinsManager.getSkin(site.getSkinId()); 276 277 if (skinTemplate != null) 278 { 279 SkinTemplate template = skin.getTemplate(skinTemplate); 280 if (template != null) 281 { 282 // Set the type and template. 283 page.setType(PageType.CONTAINER); 284 page.setTemplate(skinTemplate); 285 } 286 else 287 { 288 getLogger().error(String.format( 289 "The project workspace '%s' was created with the skin '%s' which doesn't possess the mandatory template '%s'.\nThe '%s' page of the project workspace could not be initialized.", 290 site.getName(), site.getSkinId(), skinTemplate, page.getName())); 291 } 292 } 293 294 sitemap.saveChanges(); 295 296 // Move module page to ensure pages order 297 for (WorkspaceModule otherModule : _modulesEP.getModules()) 298 { 299 if (otherModule.compareTo(this) > 0) 300 { 301 Set<Page> modulePages = _projectManager.getModulePages(project, otherModule); 302 if (!modulePages.isEmpty()) 303 { 304 ((MoveablePage) page).orderBefore(modulePages.iterator().next()); 305 break; 306 } 307 } 308 } 309 310 sitemap.saveChanges(); 311 312 return page; 313 } 314 else 315 { 316 return null; 317 } 318 } 319 320 /** 321 * Change the visibility of module pages if needed 322 * @param project The project 323 * @param visible visible <code>true</code> to set pages as visible, <code>false</code> otherwise 324 */ 325 protected void _setPagesVisibility(Project project, boolean visible) 326 { 327 _getModulePages(project) 328 .stream() 329 .filter(p -> !"index".equals(p.getPathInSitemap()) && (visible && !p.isVisible() || !visible && p.isVisible())) 330 .filter(page -> { 331 if (page instanceof LockablePage lockablePage && lockablePage.isLocked()) 332 { 333 getLogger().error("The page " + page.getName() + " visibility can not be change as it is currently locked."); 334 return false; 335 } 336 return true; 337 }) 338 .forEach(page -> _pageDAO.setPageVisibility((ModifiablePage) page, visible)); 339 } 340 341 /** 342 * Delete the module pages and their related contents 343 * @param project The project 344 */ 345 protected void _deletePages(Project project) 346 { 347 List<Page> modulePages = _getModulePages(project); 348 349 for (Page page : modulePages) 350 { 351 _pageDAO.deletePage((ModifiablePage) page, true); 352 } 353 } 354 355 /** 356 * Get the module pages 357 * @param project the project 358 * @return the module pages 359 */ 360 protected List<Page> _getModulePages(Project project) 361 { 362 String modulePageName = getModulePageName(); 363 List<Page> pages = new ArrayList<>(); 364 Site site = project.getSite(); 365 if (site != null) 366 { 367 for (Sitemap sitemap : site.getSitemaps()) 368 { 369 if (sitemap.hasChild(modulePageName)) 370 { 371 pages.add(sitemap.getChild(modulePageName)); 372 } 373 } 374 } 375 376 return pages; 377 } 378 379 /** 380 * Delete all activities related to this module 381 * @param project The project 382 */ 383 protected void _deleteActivities(Project project) 384 { 385 Expression projectExpression = new StringExpression(AbstractWorkspacesActivityType.PROJECT_NAME, Operator.EQ, project.getName()); 386 List<Expression> eventTypeExpressions = new ArrayList<>(); 387 for (String eventType: getAllowedEventTypes()) 388 { 389 eventTypeExpressions.add(new ActivityTypeExpression(Operator.EQ, eventType)); 390 } 391 Expression moduleActivityExpression = new AndExpression(projectExpression, new OrExpression((Expression[]) eventTypeExpressions.toArray())); 392 String query = ActivityHelper.getActivityXPathQuery(moduleActivityExpression); 393 AmetysObjectIterable<Activity> activities = _resolver.query(query); 394 for (Activity activity : activities) 395 { 396 activity.remove(); 397 } 398 } 399 400 /** 401 * Get the default value of the XSLT parameter of the given service. 402 * @param serviceId the service ID. 403 * @return the default XSLT parameter value. 404 */ 405 protected String _getDefaultXslt(String serviceId) 406 { 407 Service service = _serviceEP.hasExtension(serviceId) ? _serviceEP.getExtension(serviceId) : null; 408 if (service != null) 409 { 410 @SuppressWarnings("unchecked") 411 ElementDefinition<String> xsltParameterDefinition = (ElementDefinition<String>) service.getParameters().get("xslt"); 412 413 if (xsltParameterDefinition != null) 414 { 415 return xsltParameterDefinition.getDefaultValue(); 416 } 417 } 418 419 return StringUtils.EMPTY; 420 } 421 422 /** 423 * Returns the module page's name 424 * @return The module page's name 425 */ 426 protected abstract String getModulePageName(); 427 428 /** 429 * Returns the module page's title as i18n 430 * @return The module page's title 431 */ 432 protected abstract I18nizableText getModulePageTitle(); 433 434 /** 435 * Returns the template to use for module's page. Can be null if the page should be a node page 436 * @return The template 437 */ 438 protected String getModulePageTemplate() 439 { 440 return ProjectConstants.PROJECT_TEMPLATE; 441 } 442 443 /** 444 * Initialize the module page 445 * @param modulePage The module page 446 */ 447 protected abstract void initializeModulePage(ModifiablePage modulePage); 448 449 /** 450 * Internal process when module is deactivated 451 * @param project The project 452 */ 453 protected void _internalDeactivateModule(Project project) 454 { 455 // Empty 456 } 457 458 /** 459 * Internal process to delete data 460 * @param project The project 461 */ 462 protected void _internalDeleteData(Project project) 463 { 464 // Empty 465 } 466 467 /** 468 * Internal process when module is activated 469 * @param project The project 470 * @param additionalValues A list of optional additional values. Accepted values are : description, mailingList, inscriptionStatus, defaultProfile, tags, categoryTags, keywords and language 471 */ 472 protected void _internalActivateModule(Project project, Map<String, Object> additionalValues) 473 { 474 // Empty 475 } 476 477 /** 478 * Utility method to get or create an ametys object 479 * @param <A> A sub class of AmetysObject 480 * @param parent The parent object 481 * @param name The ametys object name 482 * @param type The ametys object type 483 * @param create True to create the object if it does not exist 484 * @return ametys object 485 * @throws AmetysRepositoryException if an repository error occurs 486 */ 487 protected <A extends AmetysObject> A _getAmetysObject(ModifiableTraversableAmetysObject parent, String name, String type, boolean create) throws AmetysRepositoryException 488 { 489 A object = null; 490 491 if (parent.hasChild(name)) 492 { 493 object = parent.getChild(name); 494 } 495 else if (create) 496 { 497 object = parent.createChild(name, type); 498 parent.saveChanges(); 499 } 500 501 return object; 502 } 503 504 @Override 505 public Map<String, Object> getStatistics(Project project) 506 { 507 Map<String, Object> statistics = new HashMap<>(); 508 509 if (ArrayUtils.contains(project.getModules(), getId())) 510 { 511 statistics.put(_getModuleLastActivityKey(), _getModuleLastActivity(project)); 512 statistics.put(_getModuleAtivateKey(), true); 513 statistics.put(getModuleSizeKey(), getModuleSize(project)); 514 statistics.putAll(_getInternalStatistics(project, true)); 515 } 516 else 517 { 518 statistics.put(_getModuleAtivateKey(), false); 519 statistics.putAll(_getInternalStatistics(project, false)); 520 // Use -2 as default empty value, so the sort in columns can work. It will be replaced by empty value in the renderer. 521 statistics.put(getModuleSizeKey(), __SIZE_INACTIVE); 522 } 523 524 return statistics; 525 } 526 527 /** 528 * Get the internal statistics of the module 529 * @param project The project 530 * @param isActive true if module is active 531 * @return a map of internal statistics 532 */ 533 protected Map<String, Object> _getInternalStatistics(Project project, boolean isActive) 534 { 535 return Map.of(); 536 } 537 538 @Override 539 public List<StatisticColumn> getStatisticModel() 540 { 541 List<StatisticColumn> statisticHeaders = new ArrayList<>(); 542 543 if (_showActivatedStatus()) 544 { 545 statisticHeaders.add(new StatisticColumn(_getModuleAtivateKey(), getModuleTitle()) 546 .withGroup(GROUP_HEADER_ACTIVATED_ID) 547 .withType(StatisticsColumnType.BOOLEAN)); 548 } 549 if (_showLastActivity()) 550 { 551 statisticHeaders.add(new StatisticColumn(_getModuleLastActivityKey(), getModuleTitle()) 552 .withGroup(GROUP_HEADER_LAST_ACTIVITY_ID) 553 .withType(StatisticsColumnType.DATE)); 554 } 555 556 if (_showModuleSize()) 557 { 558 statisticHeaders.add(new StatisticColumn(getModuleSizeKey(), getModuleTitle()) 559 .withType(StatisticsColumnType.LONG) 560 .withGroup(GROUP_HEADER_SIZE_ID) 561 .withRenderer("Ametys.plugins.workspaces.project.tool.ProjectsGridHelper.renderSize") 562 .isHidden(true)); 563 } 564 565 statisticHeaders.addAll(_getInternalStatisticModel()); 566 567 return statisticHeaders; 568 } 569 570 /** 571 * Get the headers of statistics 572 * @return a list of statistics headers 573 */ 574 protected List<StatisticColumn> _getInternalStatisticModel() 575 { 576 return List.of(); 577 } 578 579 @Override 580 public String getModuleSizeKey() 581 { 582 return getModuleName() + "$size"; 583 } 584 585 private String _getModuleAtivateKey() 586 { 587 return getModuleName() + "$activated"; 588 } 589 590 private String _getModuleLastActivityKey() 591 { 592 return getModuleName() + "$lastActivity"; 593 } 594 595 /** 596 * Get the size of module in bytes 597 * @param project The project 598 * @return the size of module in bytes 599 */ 600 public long getModuleSize(Project project) 601 { 602 return 0; 603 } 604 605 /** 606 * Check if activated status should be shown or not 607 * @return true if activated status should be shown 608 */ 609 protected boolean _showActivatedStatus() 610 { 611 return true; 612 } 613 614 /** 615 * Check if module size should be shown or not 616 * @return true if module size should be shown 617 */ 618 protected boolean _showModuleSize() 619 { 620 return false; 621 } 622 623 public Set<String> getAllEventTypes() 624 { 625 return getAllowedEventTypes(); 626 } 627 628 /** 629 * Check if the last activity should be shown or not 630 * @return true if last activity should be shown 631 */ 632 protected boolean _showLastActivity() 633 { 634 return getAllEventTypes().size() != 0; 635 } 636 637 /** 638 * Get the size of module in bytes 639 * @param project The project 640 * @return the size of module in bytes 641 */ 642 protected ZonedDateTime _getModuleLastActivity(Project project) 643 { 644 return _activityStream.getDateOfLastActivityByActivityType(project.getName(), getAllowedEventTypes()); 645 } 646 647 public ModifiableResourceCollection getModuleRoot(Project project, boolean create) 648 { 649 try 650 { 651 ExplorerNode projectRootNode = project.getExplorerRootNode(); 652 653 if (projectRootNode instanceof ModifiableResourceCollection mProjectRootNode) 654 { 655 return _getAmetysObject(mProjectRootNode, getModuleName(), JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create); 656 } 657 else 658 { 659 throw new IllegalArgumentException("Root module '" + projectRootNode.getPath() + "' is not modifiable"); 660 } 661 } 662 catch (AmetysRepositoryException e) 663 { 664 throw new AmetysRepositoryException("Error getting the " + getModuleName() + " root node.", e); 665 } 666 } 667}