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; 025import java.util.stream.Collectors; 026 027import org.apache.avalon.framework.context.Context; 028import org.apache.avalon.framework.context.ContextException; 029import org.apache.avalon.framework.context.Contextualizable; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.commons.lang.ArrayUtils; 034import org.apache.commons.lang.StringUtils; 035 036import org.ametys.cms.transformation.xslt.ResolveURIComponent; 037import org.ametys.core.observation.Event; 038import org.ametys.core.observation.ObservationManager; 039import org.ametys.core.right.RightManager; 040import org.ametys.core.user.CurrentUserProvider; 041import org.ametys.core.user.UserManager; 042import org.ametys.core.util.I18nUtils; 043import org.ametys.plugins.core.user.UserHelper; 044import org.ametys.plugins.explorer.ExplorerNode; 045import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 046import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory; 047import org.ametys.plugins.repository.AmetysObject; 048import org.ametys.plugins.repository.AmetysObjectIterable; 049import org.ametys.plugins.repository.AmetysObjectResolver; 050import org.ametys.plugins.repository.AmetysRepositoryException; 051import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 052import org.ametys.plugins.repository.activities.Activity; 053import org.ametys.plugins.repository.activities.ActivityHelper; 054import org.ametys.plugins.repository.activities.ActivityTypeExpression; 055import org.ametys.plugins.repository.query.expression.AndExpression; 056import org.ametys.plugins.repository.query.expression.Expression; 057import org.ametys.plugins.repository.query.expression.Expression.Operator; 058import org.ametys.plugins.repository.query.expression.OrExpression; 059import org.ametys.plugins.repository.query.expression.StringExpression; 060import org.ametys.plugins.workspaces.activities.AbstractWorkspacesActivityType; 061import org.ametys.plugins.workspaces.activities.activitystream.ActivityStreamClientInteraction; 062import org.ametys.plugins.workspaces.project.ProjectConstants; 063import org.ametys.plugins.workspaces.project.ProjectManager; 064import org.ametys.plugins.workspaces.project.modules.WorkspaceModule; 065import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint; 066import org.ametys.plugins.workspaces.project.objects.Project; 067import org.ametys.plugins.workspaces.project.rights.ProjectRightHelper; 068import org.ametys.plugins.workspaces.util.StatisticColumn; 069import org.ametys.plugins.workspaces.util.StatisticsColumnType; 070import org.ametys.runtime.i18n.I18nizableText; 071import org.ametys.runtime.model.ElementDefinition; 072import org.ametys.runtime.plugin.component.AbstractLogEnabled; 073import org.ametys.runtime.plugin.component.PluginAware; 074import org.ametys.web.ObservationConstants; 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 List<String> modulePageIds = _getModulePages(project) 328 .stream() 329 .filter(p -> !"index".equals(p.getPathInSitemap()) && (visible && !p.isVisible() || !visible && p.isVisible())) 330 .map(Page::getId) 331 .collect(Collectors.toList()); 332 333 _pageDAO.setVisibility(modulePageIds, visible); 334 } 335 336 /** 337 * Delete the module pages and their related contents 338 * @param project The project 339 */ 340 protected void _deletePages(Project project) 341 { 342 List<Page> modulePages = _getModulePages(project); 343 344 for (Page page : modulePages) 345 { 346 _pageDAO.deletePage((ModifiablePage) page, true); 347 } 348 } 349 350 /** 351 * Get the module pages 352 * @param project the project 353 * @return the module pages 354 */ 355 protected List<Page> _getModulePages(Project project) 356 { 357 String modulePageName = getModulePageName(); 358 List<Page> pages = new ArrayList<>(); 359 Site site = project.getSite(); 360 if (site != null) 361 { 362 for (Sitemap sitemap : site.getSitemaps()) 363 { 364 if (sitemap.hasChild(modulePageName)) 365 { 366 pages.add(sitemap.getChild(modulePageName)); 367 } 368 } 369 } 370 371 return pages; 372 } 373 374 /** 375 * Delete all activities related to this module 376 * @param project The project 377 */ 378 protected void _deleteActivities(Project project) 379 { 380 Expression projectExpression = new StringExpression(AbstractWorkspacesActivityType.PROJECT_NAME, Operator.EQ, project.getName()); 381 List<Expression> eventTypeExpressions = new ArrayList<>(); 382 for (String eventType: getAllowedEventTypes()) 383 { 384 eventTypeExpressions.add(new ActivityTypeExpression(Operator.EQ, eventType)); 385 } 386 Expression moduleActivityExpression = new AndExpression(projectExpression, new OrExpression((Expression[]) eventTypeExpressions.toArray())); 387 String query = ActivityHelper.getActivityXPathQuery(moduleActivityExpression); 388 AmetysObjectIterable<Activity> activities = _resolver.query(query); 389 for (Activity activity : activities) 390 { 391 activity.remove(); 392 } 393 } 394 395 /** 396 * Get the default value of the XSLT parameter of the given service. 397 * @param serviceId the service ID. 398 * @return the default XSLT parameter value. 399 */ 400 protected String _getDefaultXslt(String serviceId) 401 { 402 Service service = _serviceEP.hasExtension(serviceId) ? _serviceEP.getExtension(serviceId) : null; 403 if (service != null) 404 { 405 @SuppressWarnings("unchecked") 406 ElementDefinition<String> xsltParameterDefinition = (ElementDefinition<String>) service.getParameters().get("xslt"); 407 408 if (xsltParameterDefinition != null) 409 { 410 return xsltParameterDefinition.getDefaultValue(); 411 } 412 } 413 414 return StringUtils.EMPTY; 415 } 416 417 /** 418 * Returns the module page's name 419 * @return The module page's name 420 */ 421 protected abstract String getModulePageName(); 422 423 /** 424 * Returns the module page's title as i18n 425 * @return The module page's title 426 */ 427 protected abstract I18nizableText getModulePageTitle(); 428 429 /** 430 * Returns the template to use for module's page. Can be null if the page should be a node page 431 * @return The template 432 */ 433 protected String getModulePageTemplate() 434 { 435 return ProjectConstants.PROJECT_TEMPLATE; 436 } 437 438 /** 439 * Initialize the module page 440 * @param modulePage The module page 441 */ 442 protected abstract void initializeModulePage(ModifiablePage modulePage); 443 444 /** 445 * Internal process when module is deactivated 446 * @param project The project 447 */ 448 protected void _internalDeactivateModule(Project project) 449 { 450 // Empty 451 } 452 453 /** 454 * Internal process to delete data 455 * @param project The project 456 */ 457 protected void _internalDeleteData(Project project) 458 { 459 // Empty 460 } 461 462 /** 463 * Internal process when module is activated 464 * @param project The project 465 * @param additionalValues A list of optional additional values. Accepted values are : description, mailingList, inscriptionStatus, defaultProfile, tags, categoryTags, keywords and language 466 */ 467 protected void _internalActivateModule(Project project, Map<String, Object> additionalValues) 468 { 469 // Empty 470 } 471 472 /** 473 * Utility method to get or create an ametys object 474 * @param <A> A sub class of AmetysObject 475 * @param parent The parent object 476 * @param name The ametys object name 477 * @param type The ametys object type 478 * @param create True to create the object if it does not exist 479 * @return ametys object 480 * @throws AmetysRepositoryException if an repository error occurs 481 */ 482 protected <A extends AmetysObject> A _getAmetysObject(ModifiableTraversableAmetysObject parent, String name, String type, boolean create) throws AmetysRepositoryException 483 { 484 A object = null; 485 486 if (parent.hasChild(name)) 487 { 488 object = parent.getChild(name); 489 } 490 else if (create) 491 { 492 object = parent.createChild(name, type); 493 parent.saveChanges(); 494 } 495 496 return object; 497 } 498 499 @Override 500 public Map<String, Object> getStatistics(Project project) 501 { 502 Map<String, Object> statistics = new HashMap<>(); 503 504 if (ArrayUtils.contains(project.getModules(), getId())) 505 { 506 statistics.put(_getModuleLastActivityKey(), _getModuleLastActivity(project)); 507 statistics.put(_getModuleAtivateKey(), true); 508 statistics.put(getModuleSizeKey(), _getModuleSize(project)); 509 statistics.putAll(_getInternalStatistics(project, true)); 510 } 511 else 512 { 513 statistics.put(_getModuleAtivateKey(), false); 514 statistics.putAll(_getInternalStatistics(project, false)); 515 // Use -2 as default empty value, so the sort in columns can work. It will be replaced by empty value in the renderer. 516 statistics.put(getModuleSizeKey(), __SIZE_INACTIVE); 517 } 518 519 return statistics; 520 } 521 522 /** 523 * Get the internal statistics of the module 524 * @param project The project 525 * @param isActive true if module is active 526 * @return a map of internal statistics 527 */ 528 protected Map<String, Object> _getInternalStatistics(Project project, boolean isActive) 529 { 530 return Map.of(); 531 } 532 533 @Override 534 public List<StatisticColumn> getStatisticModel() 535 { 536 List<StatisticColumn> statisticHeaders = new ArrayList<>(); 537 538 if (_showActivatedStatus()) 539 { 540 statisticHeaders.add(new StatisticColumn(_getModuleAtivateKey(), getModuleTitle()) 541 .withGroup(GROUP_HEADER_ACTIVATED_ID) 542 .withType(StatisticsColumnType.BOOLEAN)); 543 } 544 if (_showLastActivity()) 545 { 546 statisticHeaders.add(new StatisticColumn(_getModuleLastActivityKey(), getModuleTitle()) 547 .withGroup(GROUP_HEADER_LAST_ACTIVITY_ID) 548 .withType(StatisticsColumnType.DATE)); 549 } 550 551 if (_showModuleSize()) 552 { 553 statisticHeaders.add(new StatisticColumn(getModuleSizeKey(), getModuleTitle()) 554 .withType(StatisticsColumnType.LONG) 555 .withGroup(GROUP_HEADER_SIZE_ID) 556 .withRenderer("Ametys.plugins.workspaces.project.tool.ProjectsGridHelper.renderSize") 557 .isHidden(true)); 558 } 559 560 statisticHeaders.addAll(_getInternalStatisticModel()); 561 562 return statisticHeaders; 563 } 564 565 /** 566 * Get the headers of statistics 567 * @return a list of statistics headers 568 */ 569 protected List<StatisticColumn> _getInternalStatisticModel() 570 { 571 return List.of(); 572 } 573 574 @Override 575 public String getModuleSizeKey() 576 { 577 return getModuleName() + "$size"; 578 } 579 580 private String _getModuleAtivateKey() 581 { 582 return getModuleName() + "$activated"; 583 } 584 585 private String _getModuleLastActivityKey() 586 { 587 return getModuleName() + "$lastActivity"; 588 } 589 590 /** 591 * Get the size of module in bytes 592 * @param project The project 593 * @return the size of module in bytes 594 */ 595 protected long _getModuleSize(Project project) 596 { 597 return 0; 598 } 599 600 /** 601 * Check if activated status should be shown or not 602 * @return true if activated status should be shown 603 */ 604 protected boolean _showActivatedStatus() 605 { 606 return true; 607 } 608 609 /** 610 * Check if module size should be shown or not 611 * @return true if module size should be shown 612 */ 613 protected boolean _showModuleSize() 614 { 615 return false; 616 } 617 618 public Set<String> getAllEventTypes() 619 { 620 return getAllowedEventTypes(); 621 } 622 623 /** 624 * Check if the last activity should be shown or not 625 * @return true if last activity should be shown 626 */ 627 protected boolean _showLastActivity() 628 { 629 return getAllEventTypes().size() != 0; 630 } 631 632 /** 633 * Get the size of module in bytes 634 * @param project The project 635 * @return the size of module in bytes 636 */ 637 protected ZonedDateTime _getModuleLastActivity(Project project) 638 { 639 return _activityStream.getDateOfLastActivityByActivityType(project.getName(), getAllowedEventTypes()); 640 } 641 642 public ModifiableResourceCollection getModuleRoot(Project project, boolean create) 643 { 644 try 645 { 646 ExplorerNode projectRootNode = project.getExplorerRootNode(); 647 648 if (projectRootNode instanceof ModifiableResourceCollection mProjectRootNode) 649 { 650 return _getAmetysObject(mProjectRootNode, getModuleName(), JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create); 651 } 652 else 653 { 654 throw new IllegalArgumentException("Root module '" + projectRootNode.getPath() + "' is not modifiable"); 655 } 656 } 657 catch (AmetysRepositoryException e) 658 { 659 throw new AmetysRepositoryException("Error getting the " + getModuleName() + " root node.", e); 660 } 661 } 662}