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