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