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