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) 172 { 173 // create the resources root node 174 getModuleRoot(project, true); 175 _internalActivateModule(project); 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 SkinTemplate template = skin.getTemplate(skinTemplate); 251 if (template != null) 252 { 253 // Set the type and template. 254 page.setType(PageType.CONTAINER); 255 page.setTemplate(skinTemplate); 256 } 257 else 258 { 259 getLogger().error(String.format( 260 "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.", 261 site.getName(), site.getSkinId(), skinTemplate, page.getName())); 262 } 263 264 sitemap.saveChanges(); 265 266 // Move module page to ensure pages order 267 for (WorkspaceModule otherModule : _modulesEP.getModules()) 268 { 269 if (otherModule.compareTo(this) > 0) 270 { 271 Set<Page> modulePages = _projectManager.getModulePages(project, otherModule); 272 if (!modulePages.isEmpty()) 273 { 274 ((MoveablePage) page).orderBefore(modulePages.iterator().next()); 275 break; 276 } 277 } 278 } 279 280 sitemap.saveChanges(); 281 282 return page; 283 } 284 else 285 { 286 return null; 287 } 288 } 289 290 /** 291 * Change the visibility of module pages if needed 292 * @param project The project 293 * @param visible visible <code>true</code> to set pages as visible, <code>false</code> otherwise 294 */ 295 protected void _setPagesVisibility(Project project, boolean visible) 296 { 297 List<String> modulePageIds = _getModulePages(project) 298 .stream() 299 .filter(p -> !"index".equals(p.getPathInSitemap()) && (visible && !p.isVisible() || !visible && p.isVisible())) 300 .map(Page::getId) 301 .collect(Collectors.toList()); 302 303 _pageDAO.setVisibility(modulePageIds, visible); 304 } 305 306 /** 307 * Delete the module pages and their related contents 308 * @param project The project 309 */ 310 protected void _deletePages(Project project) 311 { 312 List<Page> modulePages = _getModulePages(project); 313 314 for (Page page : modulePages) 315 { 316 _pageDAO.deletePage((ModifiablePage) page, true); 317 } 318 } 319 320 /** 321 * Get the module pages 322 * @param project the project 323 * @return the module pages 324 */ 325 protected List<Page> _getModulePages(Project project) 326 { 327 String modulePageName = getModulePageName(); 328 List<Page> pages = new ArrayList<>(); 329 330 for (Site site : project.getSites()) 331 { 332 for (Sitemap sitemap : site.getSitemaps()) 333 { 334 if (sitemap.hasChild(modulePageName)) 335 { 336 pages.add(sitemap.getChild(modulePageName)); 337 } 338 } 339 } 340 341 return pages; 342 } 343 344 /** 345 * Delete all events related to this module 346 * @param project The project 347 */ 348 protected void _deleteEvents(Project project) 349 { 350 try 351 { 352 NodeIterator events = JCREventHelper.getEvents(project, getAllowedEventTypes().toArray(new String[]{})); 353 while (events.hasNext()) 354 { 355 Node event = (Node) events.next(); 356 event.remove(); 357 } 358 } 359 catch (RepositoryException e) 360 { 361 getLogger().warn("Unable to delete project '" + project.getName() + "' events for module '" + this.getId() + "'", e); 362 } 363 } 364 365 /** 366 * Get the default value of the XSLT parameter of the given service. 367 * @param serviceId the service ID. 368 * @return the default XSLT parameter value. 369 */ 370 protected String _getDefaultXslt(String serviceId) 371 { 372 Service service = _serviceEP.hasExtension(serviceId) ? _serviceEP.getExtension(serviceId) : null; 373 if (service != null) 374 { 375 @SuppressWarnings("unchecked") 376 ElementDefinition<String> xsltParameterDefinition = (ElementDefinition<String>) service.getParameters().get("xslt"); 377 378 if (xsltParameterDefinition != null) 379 { 380 return xsltParameterDefinition.getDefaultValue(); 381 } 382 } 383 384 return StringUtils.EMPTY; 385 } 386 387 /** 388 * Returns the module page's name 389 * @return The module page's name 390 */ 391 protected abstract String getModulePageName(); 392 393 /** 394 * Returns the module page's title as i18n 395 * @return The module page's title 396 */ 397 protected abstract I18nizableText getModulePageTitle(); 398 399 /** 400 * Returns the template to use for module's page 401 * @return The template 402 */ 403 protected String getModulePageTemplate() 404 { 405 return ProjectConstants.PROJECT_TEMPLATE; 406 } 407 408 /** 409 * Initialize the module page 410 * @param modulePage The module page 411 */ 412 protected abstract void initializeModulePage(ModifiablePage modulePage); 413 414 /** 415 * Internal process when module is deactivated 416 * @param project The project 417 */ 418 protected void _internalDeactivateModule(Project project) 419 { 420 // Empty 421 } 422 423 /** 424 * Internal process to delete data 425 * @param project The project 426 */ 427 protected void _internalDeleteData(Project project) 428 { 429 // Empty 430 } 431 432 /** 433 * Internal process when module is activated 434 * @param project The project 435 */ 436 protected void _internalActivateModule(Project project) 437 { 438 // Empty 439 } 440 441 /** 442 * Utility method to get or create an ametys object 443 * @param <A> A sub class of AmetysObject 444 * @param parent The parent object 445 * @param name The ametys object name 446 * @param type The ametys object type 447 * @param create True to create the object if it does not exist 448 * @return ametys object 449 * @throws AmetysRepositoryException if an repository error occurs 450 */ 451 protected <A extends AmetysObject> A _getAmetysObject(ModifiableTraversableAmetysObject parent, String name, String type, boolean create) throws AmetysRepositoryException 452 { 453 A object = null; 454 455 if (parent.hasChild(name)) 456 { 457 object = parent.getChild(name); 458 } 459 else if (create) 460 { 461 object = parent.createChild(name, type); 462 parent.saveChanges(); 463 } 464 465 return object; 466 } 467}