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.util.ArrayList; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Map; 022import java.util.Optional; 023import java.util.Set; 024import java.util.stream.Collectors; 025 026import javax.jcr.Node; 027import javax.jcr.NodeIterator; 028import javax.jcr.RepositoryException; 029 030import org.apache.avalon.framework.context.Context; 031import org.apache.avalon.framework.context.ContextException; 032import org.apache.avalon.framework.context.Contextualizable; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.avalon.framework.service.Serviceable; 036import org.apache.commons.lang.StringUtils; 037 038import org.ametys.cms.transformation.xslt.ResolveURIComponent; 039import org.ametys.core.observation.Event; 040import org.ametys.core.observation.ObservationManager; 041import org.ametys.core.right.RightManager; 042import org.ametys.core.user.CurrentUserProvider; 043import org.ametys.core.user.UserManager; 044import org.ametys.core.util.I18nUtils; 045import org.ametys.plugins.core.user.UserHelper; 046import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 047import org.ametys.plugins.repository.AmetysObject; 048import org.ametys.plugins.repository.AmetysObjectResolver; 049import org.ametys.plugins.repository.AmetysRepositoryException; 050import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 051import org.ametys.plugins.repository.events.JCREventHelper; 052import org.ametys.plugins.workspaces.project.ProjectConstants; 053import org.ametys.plugins.workspaces.project.ProjectManager; 054import org.ametys.plugins.workspaces.project.modules.WorkspaceModule; 055import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint; 056import org.ametys.plugins.workspaces.project.objects.Project; 057import org.ametys.plugins.workspaces.project.rights.ProjectRightHelper; 058import org.ametys.runtime.i18n.I18nizableText; 059import org.ametys.runtime.model.ElementDefinition; 060import org.ametys.runtime.plugin.component.AbstractLogEnabled; 061import org.ametys.runtime.plugin.component.PluginAware; 062import org.ametys.web.ObservationConstants; 063import org.ametys.web.repository.page.ModifiablePage; 064import org.ametys.web.repository.page.MoveablePage; 065import org.ametys.web.repository.page.Page; 066import org.ametys.web.repository.page.Page.PageType; 067import org.ametys.web.repository.page.PageDAO; 068import org.ametys.web.repository.site.Site; 069import org.ametys.web.repository.sitemap.Sitemap; 070import org.ametys.web.service.Service; 071import org.ametys.web.service.ServiceExtensionPoint; 072import org.ametys.web.skin.Skin; 073import org.ametys.web.skin.SkinTemplate; 074import org.ametys.web.skin.SkinsManager; 075 076/** 077 * Abstract class for {@link WorkspaceModule} implementation 078 * 079 */ 080public abstract class AbstractWorkspaceModule extends AbstractLogEnabled implements WorkspaceModule, Serviceable, Contextualizable, PluginAware 081{ 082 /** Project manager */ 083 protected ProjectManager _projectManager; 084 /** Project right helper */ 085 protected ProjectRightHelper _projectRightHelper; 086 /** User manager */ 087 protected UserManager _userManager; 088 /** Ametys resolver */ 089 protected AmetysObjectResolver _resolver; 090 /** The rights manager */ 091 protected RightManager _rightManager; 092 /** Observer manager. */ 093 protected ObservationManager _observationManager; 094 /** The current user provider. */ 095 protected CurrentUserProvider _currentUserProvider; 096 /** The users manager */ 097 protected UserHelper _userHelper; 098 /** The i18n utils. */ 099 protected I18nUtils _i18nUtils; 100 /** The skins manager. */ 101 protected SkinsManager _skinsManager; 102 /** The page DAO */ 103 protected PageDAO _pageDAO; 104 /** The avalon context */ 105 protected Context _context; 106 /** The plugin name */ 107 protected String _pluginName; 108 /** The services handler */ 109 protected ServiceExtensionPoint _serviceEP; 110 /** The modules extension point */ 111 protected WorkspaceModuleExtensionPoint _modulesEP; 112 113 @Override 114 public void service(ServiceManager manager) throws ServiceException 115 { 116 _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE); 117 _projectRightHelper = (ProjectRightHelper) manager.lookup(ProjectRightHelper.ROLE); 118 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 119 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 120 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 121 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 122 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 123 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 124 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 125 _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE); 126 _pageDAO = (PageDAO) manager.lookup(PageDAO.ROLE); 127 _serviceEP = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE); 128 _modulesEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE); 129 } 130 131 @Override 132 public void contextualize(Context context) throws ContextException 133 { 134 _context = context; 135 } 136 137 public void setPluginInfo(String pluginName, String featureName, String id) 138 { 139 _pluginName = pluginName; 140 } 141 142 @Override 143 public void deleteData(Project project) 144 { 145 // Delete module pages 146 _deletePages(project); 147 148 _internalDeleteData(project); 149 150 // Delete root 151 ModifiableResourceCollection moduleRoot = getModuleRoot(project, false); 152 if (moduleRoot != null) 153 { 154 moduleRoot.remove(); 155 } 156 157 // Delete events 158 _deleteEvents(project); 159 } 160 161 @Override 162 public void deactivateModule(Project project) 163 { 164 // Hide module pages 165 _setPagesVisibility(project, false); 166 167 _internalDeactivateModule(project); 168 } 169 170 @Override 171 public void activateModule(Project project, Map<String, Object> additionalValues) 172 { 173 // create the resources root node 174 getModuleRoot(project, true); 175 _internalActivateModule(project, additionalValues); 176 177 for (Site site : project.getSites()) 178 { 179 for (Sitemap sitemap : site.getSitemaps()) 180 { 181 initializeSitemap(project, sitemap); 182 } 183 } 184 185 _setPagesVisibility(project, true); 186 } 187 188 @Override 189 public void initializeSitemap(Project project, Sitemap sitemap) 190 { 191 ModifiablePage page = _createModulePage(project, sitemap, getModulePageName(), getModulePageTitle(), getModulePageTemplate()); 192 193 if (page != null) 194 { 195 page.tag("SECTION"); 196 _projectManager.tagProjectPage(page, getModuleRoot(project, true)); 197 198 initializeModulePage(page); 199 200 page.saveChanges(); 201 202 Map<String, Object> eventParams = new HashMap<>(); 203 eventParams.put(ObservationConstants.ARGS_PAGE, page); 204 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_ADDED, _currentUserProvider.getUser(), eventParams)); 205 } 206 } 207 208 @Override 209 public String getModuleUrl(Project project) 210 { 211 Optional<String> url = _projectManager.getModulePages(project, this).stream() 212 .findFirst() 213 .map(page -> ResolveURIComponent.resolve("page", page.getId())); 214 215 if (url.isPresent()) 216 { 217 return url.get(); 218 } 219 else 220 { 221 // No page found 222 return null; 223 } 224 } 225 226 /** 227 * Create a new page if not already exists 228 * @param project The module project 229 * @param sitemap The sitemap where the page will be created 230 * @param name The page's name 231 * @param pageTitle The page's title as i18nizable text 232 * @param skinTemplate The template from the skin to apply on the page 233 * @return the created page or <code>null</code> if page already exists 234 */ 235 protected ModifiablePage _createModulePage(Project project, Sitemap sitemap, String name, I18nizableText pageTitle, String skinTemplate) 236 { 237 if (!sitemap.hasChild(name)) 238 { 239 ModifiablePage page = sitemap.createChild(name, "ametys:defaultPage"); 240 241 // Title should not be missing, but just in case if the i18n message or the whole catalog does not exists in the requested language 242 // to prevent a non-user-friendly error and still generate the project workspace. 243 page.setTitle(StringUtils.defaultIfEmpty(_i18nUtils.translate(pageTitle, sitemap.getName()), "Missing title")); 244 page.setType(PageType.NODE); 245 page.setSiteName(sitemap.getSiteName()); 246 page.setSitemapName(sitemap.getName()); 247 248 Site site = page.getSite(); 249 Skin skin = _skinsManager.getSkin(site.getSkinId()); 250 251 if (skinTemplate != null) 252 { 253 SkinTemplate template = skin.getTemplate(skinTemplate); 254 if (template != null) 255 { 256 // Set the type and template. 257 page.setType(PageType.CONTAINER); 258 page.setTemplate(skinTemplate); 259 } 260 else 261 { 262 getLogger().error(String.format( 263 "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.", 264 site.getName(), site.getSkinId(), skinTemplate, page.getName())); 265 } 266 } 267 268 sitemap.saveChanges(); 269 270 // Move module page to ensure pages order 271 for (WorkspaceModule otherModule : _modulesEP.getModules()) 272 { 273 if (otherModule.compareTo(this) > 0) 274 { 275 Set<Page> modulePages = _projectManager.getModulePages(project, otherModule); 276 if (!modulePages.isEmpty()) 277 { 278 ((MoveablePage) page).orderBefore(modulePages.iterator().next()); 279 break; 280 } 281 } 282 } 283 284 sitemap.saveChanges(); 285 286 return page; 287 } 288 else 289 { 290 return null; 291 } 292 } 293 294 /** 295 * Change the visibility of module pages if needed 296 * @param project The project 297 * @param visible visible <code>true</code> to set pages as visible, <code>false</code> otherwise 298 */ 299 protected void _setPagesVisibility(Project project, boolean visible) 300 { 301 List<String> modulePageIds = _getModulePages(project) 302 .stream() 303 .filter(p -> !"index".equals(p.getPathInSitemap()) && (visible && !p.isVisible() || !visible && p.isVisible())) 304 .map(Page::getId) 305 .collect(Collectors.toList()); 306 307 _pageDAO.setVisibility(modulePageIds, visible); 308 } 309 310 /** 311 * Delete the module pages and their related contents 312 * @param project The project 313 */ 314 protected void _deletePages(Project project) 315 { 316 List<Page> modulePages = _getModulePages(project); 317 318 for (Page page : modulePages) 319 { 320 _pageDAO.deletePage((ModifiablePage) page, true); 321 } 322 } 323 324 /** 325 * Get the module pages 326 * @param project the project 327 * @return the module pages 328 */ 329 protected List<Page> _getModulePages(Project project) 330 { 331 String modulePageName = getModulePageName(); 332 List<Page> pages = new ArrayList<>(); 333 334 for (Site site : project.getSites()) 335 { 336 for (Sitemap sitemap : site.getSitemaps()) 337 { 338 if (sitemap.hasChild(modulePageName)) 339 { 340 pages.add(sitemap.getChild(modulePageName)); 341 } 342 } 343 } 344 345 return pages; 346 } 347 348 /** 349 * Delete all events related to this module 350 * @param project The project 351 */ 352 protected void _deleteEvents(Project project) 353 { 354 try 355 { 356 NodeIterator events = JCREventHelper.getEvents(project, getAllowedEventTypes().toArray(new String[]{})); 357 while (events.hasNext()) 358 { 359 Node event = (Node) events.next(); 360 event.remove(); 361 } 362 } 363 catch (RepositoryException e) 364 { 365 getLogger().warn("Unable to delete project '" + project.getName() + "' events for module '" + this.getId() + "'", e); 366 } 367 } 368 369 /** 370 * Get the default value of the XSLT parameter of the given service. 371 * @param serviceId the service ID. 372 * @return the default XSLT parameter value. 373 */ 374 protected String _getDefaultXslt(String serviceId) 375 { 376 Service service = _serviceEP.hasExtension(serviceId) ? _serviceEP.getExtension(serviceId) : null; 377 if (service != null) 378 { 379 @SuppressWarnings("unchecked") 380 ElementDefinition<String> xsltParameterDefinition = (ElementDefinition<String>) service.getParameters().get("xslt"); 381 382 if (xsltParameterDefinition != null) 383 { 384 return xsltParameterDefinition.getDefaultValue(); 385 } 386 } 387 388 return StringUtils.EMPTY; 389 } 390 391 /** 392 * Returns the module page's name 393 * @return The module page's name 394 */ 395 protected abstract String getModulePageName(); 396 397 /** 398 * Returns the module page's title as i18n 399 * @return The module page's title 400 */ 401 protected abstract I18nizableText getModulePageTitle(); 402 403 /** 404 * Returns the template to use for module's page. Can be null if the page should be a node page 405 * @return The template 406 */ 407 protected String getModulePageTemplate() 408 { 409 return ProjectConstants.PROJECT_TEMPLATE; 410 } 411 412 /** 413 * Initialize the module page 414 * @param modulePage The module page 415 */ 416 protected abstract void initializeModulePage(ModifiablePage modulePage); 417 418 /** 419 * Internal process when module is deactivated 420 * @param project The project 421 */ 422 protected void _internalDeactivateModule(Project project) 423 { 424 // Empty 425 } 426 427 /** 428 * Internal process to delete data 429 * @param project The project 430 */ 431 protected void _internalDeleteData(Project project) 432 { 433 // Empty 434 } 435 436 /** 437 * Internal process when module is activated 438 * @param project The project 439 * @param additionalValues A list of optional additional values. Accepted values are : description, mailingList, inscriptionStatus, defaultProfile, tags, categoryTags, keywords and language 440 */ 441 protected void _internalActivateModule(Project project, Map<String, Object> additionalValues) 442 { 443 // Empty 444 } 445 446 /** 447 * Utility method to get or create an ametys object 448 * @param <A> A sub class of AmetysObject 449 * @param parent The parent object 450 * @param name The ametys object name 451 * @param type The ametys object type 452 * @param create True to create the object if it does not exist 453 * @return ametys object 454 * @throws AmetysRepositoryException if an repository error occurs 455 */ 456 protected <A extends AmetysObject> A _getAmetysObject(ModifiableTraversableAmetysObject parent, String name, String type, boolean create) throws AmetysRepositoryException 457 { 458 A object = null; 459 460 if (parent.hasChild(name)) 461 { 462 object = parent.getChild(name); 463 } 464 else if (create) 465 { 466 object = parent.createChild(name, type); 467 parent.saveChanges(); 468 } 469 470 return object; 471 } 472}