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