001/* 002 * Copyright 2011 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.blog; 017 018import java.util.HashMap; 019import java.util.Map; 020 021import javax.jcr.Node; 022import javax.jcr.RepositoryException; 023 024import org.apache.avalon.framework.logger.AbstractLogEnabled; 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.service.ServiceManager; 027import org.apache.avalon.framework.service.Serviceable; 028import org.apache.commons.lang3.StringUtils; 029 030import org.ametys.cms.FilterNameHelper; 031import org.ametys.cms.repository.Content; 032import org.ametys.cms.repository.DefaultContent; 033import org.ametys.cms.repository.ModifiableContent; 034import org.ametys.cms.workflow.CreateContentFunction; 035import org.ametys.core.observation.Event; 036import org.ametys.core.observation.ObservationManager; 037import org.ametys.core.observation.Observer; 038import org.ametys.core.user.UserIdentity; 039import org.ametys.core.util.I18nUtils; 040import org.ametys.plugins.blog.repository.BlogRootPageFactory; 041import org.ametys.plugins.blog.repository.VirtualPostsPage; 042import org.ametys.plugins.repository.AmetysObjectResolver; 043import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder; 044import org.ametys.plugins.workflow.AbstractWorkflowComponent; 045import org.ametys.plugins.workflow.support.WorkflowProvider; 046import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 047import org.ametys.runtime.i18n.I18nizableText; 048import org.ametys.runtime.model.ElementDefinition; 049import org.ametys.runtime.plugin.component.PluginAware; 050import org.ametys.web.ObservationConstants; 051import org.ametys.web.repository.content.shared.SharedContentManager; 052import org.ametys.web.repository.page.ModifiablePage; 053import org.ametys.web.repository.page.ModifiableZone; 054import org.ametys.web.repository.page.ModifiableZoneItem; 055import org.ametys.web.repository.page.Page.PageType; 056import org.ametys.web.repository.page.ZoneItem.ZoneType; 057import org.ametys.web.repository.site.Site; 058import org.ametys.web.repository.sitemap.Sitemap; 059import org.ametys.web.service.Service; 060import org.ametys.web.service.ServiceExtensionPoint; 061import org.ametys.web.skin.Skin; 062import org.ametys.web.skin.SkinTemplate; 063import org.ametys.web.skin.SkinTemplateZone; 064import org.ametys.web.skin.SkinsManager; 065 066import com.opensymphony.workflow.WorkflowException; 067 068/** 069 * Initializes all blog pages when a site is created. 070 */ 071public class InitializeBlogSiteObserver extends AbstractLogEnabled implements Observer, Serviceable, PluginAware 072{ 073 /** The ametys object resolver. */ 074 protected AmetysObjectResolver _ametysResolver; 075 076 /** The skins manager. */ 077 protected SkinsManager _skinsManager; 078 079 /** The service extension point. */ 080 protected ServiceExtensionPoint _serviceEP; 081 082 /** The i18n utils. */ 083 protected I18nUtils _i18nUtils; 084 085 /** The observation manager. */ 086 protected ObservationManager _observationManager; 087 088 /** The workflow provider */ 089 protected WorkflowProvider _workflowProvider; 090 091 /** The shared content manager. */ 092 protected SharedContentManager _sharedContentManager; 093 094 /** The plugin name. */ 095 protected String _pluginName; 096 097 /** The i18n catalogue. */ 098 protected String _i18nCatalogue; 099 100 @Override 101 public void setPluginInfo(String pluginName, String featureName, String id) 102 { 103 _pluginName = pluginName; 104 _i18nCatalogue = "plugin." + pluginName; 105 } 106 107 @Override 108 public void service(ServiceManager manager) throws ServiceException 109 { 110 _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 111 _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE); 112 _serviceEP = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE); 113 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 114 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 115 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 116 _sharedContentManager = (SharedContentManager) manager.lookup(SharedContentManager.ROLE); 117 } 118 119 @Override 120 public boolean supports(Event event) 121 { 122 return event.getId().equals(ObservationConstants.EVENT_SITE_ADDED) 123 || event.getId().equals(ObservationConstants.EVENT_SITEMAP_ADDED); 124 } 125 126 @Override 127 public int getPriority(Event event) 128 { 129 // Not too big but not too small either. 130 return MAX_PRIORITY + 842708; 131 } 132 133 @Override 134 public void observe(Event event, Map<String, Object> transientVars) throws Exception 135 { 136 if (event.getId().equals(ObservationConstants.EVENT_SITE_ADDED)) 137 { 138 Site site = (Site) event.getArguments().get(ObservationConstants.ARGS_SITE); 139 if (site != null && BlogConstants.BLOG_SITE_TYPE.equals(site.getType())) 140 { 141 initializeSite(site); 142 } 143 } 144 else if (event.getId().equals(ObservationConstants.EVENT_SITEMAP_ADDED)) 145 { 146 // If a sitemap is created in a blog site, initialize it. 147 Sitemap sitemap = (Sitemap) event.getArguments().get(ObservationConstants.ARGS_SITEMAP); 148 if (sitemap != null) 149 { 150 if (BlogConstants.BLOG_SITE_TYPE.equals(sitemap.getSite().getType())) 151 { 152 initializeSitemap(sitemap); 153 } 154 } 155 } 156 } 157 158 /** 159 * Initialize the given site. 160 * @param site the Site object. 161 */ 162 protected void initializeSite(Site site) 163 { 164 for (Sitemap sitemap : site.getSitemaps()) 165 { 166 initializeSitemap(sitemap); 167 } 168 } 169 170 /** 171 * Initialize the given sitemap. 172 * @param sitemap the Sitemap object. 173 */ 174 protected void initializeSitemap(Sitemap sitemap) 175 { 176 Site site = sitemap.getSite(); 177 String siteName = site.getName(); 178 String sitemapName = sitemap.getName(); 179 180 try 181 { 182 // Create the index page. 183 ModifiablePage indexPage = createPage(sitemap, "index", translate("PLUGINS_BLOG_INDEX_PAGE_TITLE", sitemapName)); 184 185 // Create the profile page. 186 ModifiablePage profilePage = createPage(sitemap, "about", translate("PLUGINS_BLOG_ABOUT_PAGE_TITLE", sitemapName)); 187 188 Content profileContent = initializeProfilePage(profilePage); 189 190 initializeIndexPage(indexPage, profileContent); 191 192 Node sitemapNode = sitemap.getNode(); 193 194 String[] virtualValue = new String[] {BlogRootPageFactory.class.getName()}; 195 196 sitemapNode.setProperty(AmetysObjectResolver.VIRTUAL_PROPERTY, virtualValue); 197 198 // Create the search page. 199 ModifiablePage searchPage = createPage(sitemap, "search", translate("PLUGINS_BLOG_SEARCH_PAGE_TITLE", sitemapName)); 200 initializeSearchPage(searchPage, profileContent); 201 202 sitemap.saveChanges(); 203 204 // Notify of the sitemap change. 205 Map<String, Object> eventParams = new HashMap<>(); 206 eventParams.put(ObservationConstants.ARGS_SITEMAP, sitemap); 207 _observationManager.notify(new Event(ObservationConstants.EVENT_SITEMAP_UPDATED, new UserIdentity("admin", "admin"), eventParams)); 208 } 209 catch (RepositoryException e) 210 { 211 getLogger().error("Error setting the virtual blog root property on the sitemap " + sitemapName + " of site " + siteName); 212 } 213 } 214 215 /** 216 * Create the blog root page. 217 * @param sitemap The sitemap 218 * @param name The page's name 219 * @param title The page's title 220 * @return the root page. 221 */ 222 protected ModifiablePage createPage(Sitemap sitemap, String name, String title) 223 { 224 if (!sitemap.hasChild(name)) 225 { 226 ModifiablePage page = sitemap.createChild(name, "ametys:defaultPage"); 227 228 page.setTitle(title); 229 page.setType(PageType.NODE); 230 page.setSiteName(sitemap.getSiteName()); 231 page.setSitemapName(sitemap.getName()); 232 233 sitemap.saveChanges(); 234 235 return page; 236 } 237 else 238 { 239 return sitemap.getChild(name); 240 } 241 } 242 243 /** 244 * Initialize the blog index page. 245 * @param indexPage the blog index page. 246 * @param profileContent the profile Content. 247 */ 248 protected void initializeIndexPage(ModifiablePage indexPage, Content profileContent) 249 { 250 Site site = indexPage.getSite(); 251 Skin skin = _skinsManager.getSkin(site.getSkinId()); 252 SkinTemplate template = skin.getTemplate(BlogConstants.BLOG_TEMPLATE); 253 254 if (template != null) 255 { 256 // Set the type and template. 257 indexPage.setType(PageType.CONTAINER); 258 indexPage.setTemplate(BlogConstants.BLOG_TEMPLATE); 259 260 // Initialize the zones. 261 Map<String, SkinTemplateZone> templateZones = template.getZones(); 262 if (templateZones.containsKey("default")) 263 { 264 initializeDefaultZone(indexPage); 265 } 266 else 267 { 268 getLogger().error("A 'default' zone is mandatory in the blog template!"); 269 return; 270 } 271 272 if (templateZones.containsKey("aside")) 273 { 274 initializeAsideZone(indexPage); 275 } 276 277 if (templateZones.containsKey("about")) 278 { 279 initializeIndexAboutZone(indexPage, profileContent); 280 } 281 282 indexPage.saveChanges(); 283 284 Map<String, Object> eventParams = new HashMap<>(); 285 eventParams.put(ObservationConstants.ARGS_PAGE, indexPage); 286 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_ADDED, new UserIdentity("admin", "admin"), eventParams)); 287 } 288 else 289 { 290 getLogger().error("The blog site " + site.getName() + " was created with the skin " 291 + site.getSkinId() + " which doesn't possess the mandatory template '" + BlogConstants.BLOG_TEMPLATE 292 + ". The blog's default pages will not be initialized."); 293 } 294 } 295 296 /** 297 * Initialize the search page. 298 * @param page the search page. 299 * @param profileContent the profile Content. 300 */ 301 protected void initializeSearchPage(ModifiablePage page, Content profileContent) 302 { 303 Site site = page.getSite(); 304 Skin skin = _skinsManager.getSkin(site.getSkinId()); 305 SkinTemplate template = skin.getTemplate(BlogConstants.BLOG_TEMPLATE); 306 307 if (template != null) 308 { 309 // Set the type and template. 310 page.setType(PageType.CONTAINER); 311 page.setTemplate(BlogConstants.BLOG_TEMPLATE); 312 313 // Initialize the zones. 314 Map<String, SkinTemplateZone> templateZones = template.getZones(); 315 if (templateZones.containsKey("default")) 316 { 317 initializeSearchDefaultZone(page); 318 } 319 else 320 { 321 getLogger().error("A 'default' zone is mandatory in the blog template!"); 322 } 323 324 if (templateZones.containsKey("aside")) 325 { 326 initializeAsideZone(page); 327 } 328 329 if (templateZones.containsKey("about")) 330 { 331 initializeIndexAboutZone(page, profileContent); 332 } 333 334 page.saveChanges(); 335 336 Map<String, Object> eventParams = new HashMap<>(); 337 eventParams.put(ObservationConstants.ARGS_PAGE, page); 338 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_ADDED, new UserIdentity("admin", "admin"), eventParams)); 339 } 340 else 341 { 342 getLogger().error("The blog site " + site.getName() + " was created with the skin " 343 + site.getSkinId() + " which doesn't possess the mandatory template '" + BlogConstants.BLOG_TEMPLATE 344 + ". The blog's default pages will not be initialized."); 345 } 346 347 } 348 349 /** 350 * Initialize the search page's default zone. 351 * @param page the search page. 352 */ 353 protected void initializeSearchDefaultZone(ModifiablePage page) 354 { 355 ModifiableZone defaultZone = page.createZone("default"); 356 357 ModifiableZoneItem defaultZoneItem = defaultZone.addZoneItem(); 358 defaultZoneItem.setType(ZoneType.SERVICE); 359 defaultZoneItem.setServiceId("org.ametys.web.service.FrontSearchService"); 360 361 String rootId = page.getSitemap().getId(); 362 String searchPageId = "blog-category://" + VirtualPostsPage.NAME + "?rootId=" + rootId; 363 364 ModifiableModelAwareDataHolder serviceMetadata = defaultZoneItem.getServiceParameters(); 365 serviceMetadata.setValue("advanced-search", false); 366 serviceMetadata.setValue("offset", 10L); 367 serviceMetadata.setValue("search-mode", "criteria-and-results"); 368 serviceMetadata.setValue("search-by-content-types", BlogConstants.POST_CONTENT_TYPE); 369 serviceMetadata.setValue("search-by-content-types-choice", "none"); 370 serviceMetadata.setValue("search-by-pages", searchPageId); 371 serviceMetadata.setValue("search-multisite", false); 372 serviceMetadata.setValue("xslt", getDefaultXslt("org.ametys.web.service.FrontSearchService")); 373 } 374 375 /** 376 * Initialize the blog profile page. 377 * @param page the blog profile page. 378 * @return the profile content. 379 */ 380 protected Content initializeProfilePage(ModifiablePage page) 381 { 382 Site site = page.getSite(); 383 Skin skin = _skinsManager.getSkin(site.getSkinId()); 384 SkinTemplate template = skin.getTemplate(BlogConstants.BLOG_TEMPLATE); 385 386 Content profileContent = null; 387 388 if (template != null) 389 { 390 // Set the type and template. 391 page.setType(PageType.CONTAINER); 392 page.setTemplate(BlogConstants.BLOG_TEMPLATE); 393 394 // Initialize the zones. 395 Map<String, SkinTemplateZone> templateZones = template.getZones(); 396 if (templateZones.containsKey("default")) 397 { 398 profileContent = initializeProfileDefaultZone(page); 399 } 400 else 401 { 402 getLogger().error("A 'default' zone is mandatory in the blog template!"); 403 return null; 404 } 405 406 if (templateZones.containsKey("aside")) 407 { 408 initializeAsideZone(page); 409 } 410 411 page.saveChanges(); 412 413 Map<String, Object> eventParams = new HashMap<>(); 414 eventParams.put(ObservationConstants.ARGS_PAGE, page); 415 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_ADDED, new UserIdentity("admin", "admin"), eventParams)); 416 } 417 else 418 { 419 getLogger().error("The blog site " + site.getName() + " was created with the skin " 420 + site.getSkinId() + " which doesn't possess the mandatory template '" + BlogConstants.BLOG_TEMPLATE 421 + ". The blog's default pages will not be initialized."); 422 } 423 424 return profileContent; 425 } 426 427 /** 428 * Initialize the profile page's default zone. 429 * @param page the profile page. 430 * @return the profile content. 431 */ 432 protected Content initializeProfileDefaultZone(ModifiablePage page) 433 { 434 try 435 { 436 String lang = page.getSitemapName(); 437 438 Content profile = createProfileContent(BlogConstants.BLOG_WORKFLOW_NAME, page.getSiteName(), page.getSitemapName(), translate("PLUGINS_BLOG_ABOUT_PAGE_TITLE", lang)); 439 440 ModifiableZone aboutZone = page.createZone("default"); 441 442 ModifiableZoneItem aboutZoneItem = aboutZone.addZoneItem(); 443 aboutZoneItem.setType(ZoneType.CONTENT); 444 445 aboutZoneItem.setContent(profile); 446 447 return profile; 448 } 449 catch (WorkflowException e) 450 { 451 getLogger().error("Error creating profile content", e); 452 return null; 453 } 454 } 455 456 457 /** 458 * Initialize the default zone. 459 * @param page The page 460 */ 461 protected void initializeDefaultZone(ModifiablePage page) 462 { 463 ModifiableZone defaultZone = page.createZone("default"); 464 465 // Archives service. 466 ModifiableZoneItem archivesZoneItem = defaultZone.addZoneItem(); 467 archivesZoneItem.setType(ZoneType.SERVICE); 468 archivesZoneItem.setServiceId(BlogConstants.POSTS_SERVICE_ID); 469 470 ModifiableModelAwareDataHolder parameters = archivesZoneItem.getServiceParameters(); 471 parameters.setValue("header", ""); 472 parameters.setValue("type", "all"); 473 parameters.setValue("metadataSetName", "abstract"); 474 parameters.setValue("xslt", getDefaultXslt(BlogConstants.POSTS_SERVICE_ID)); 475 } 476 477 /** 478 * Initialize the index page's "about" zone. 479 * @param indexPage the index Page. 480 * @param profileContent the profile content. 481 */ 482 protected void initializeIndexAboutZone(ModifiablePage indexPage, Content profileContent) 483 { 484 ModifiableZone aboutZone = indexPage.createZone("about"); 485 486 if (profileContent instanceof DefaultContent) 487 { 488 ModifiableZoneItem aboutZoneItem = aboutZone.addZoneItem(); 489 490 aboutZoneItem.setType(ZoneType.CONTENT); 491 aboutZoneItem.setContent(profileContent); 492 aboutZoneItem.setViewName("abstract"); 493 } 494 } 495 496 /** 497 * Initialize the "aside" zone. 498 * @param page the page on which to initialize the aside zone. 499 */ 500 protected void initializeAsideZone(ModifiablePage page) 501 { 502 String language = page.getSitemapName(); 503 504 ModifiableZone aboutZone = page.createZone("aside"); 505 506 // Archives service. 507 ModifiableZoneItem archivesZoneItem = aboutZone.addZoneItem(); 508 archivesZoneItem.setType(ZoneType.SERVICE); 509 archivesZoneItem.setServiceId(BlogConstants.ARCHIVES_SERVICE_ID); 510 511 ModifiableModelAwareDataHolder parameters = archivesZoneItem.getServiceParameters(); 512 parameters.setValue("service-title", translate("PLUGINS_BLOG_ARCHIVES_ZONEITEM_TITLE", language)); 513 parameters.setValue("xslt", getDefaultXslt(BlogConstants.ARCHIVES_SERVICE_ID)); 514 515 // Tags service. 516 ModifiableZoneItem tagsZoneItem = aboutZone.addZoneItem(); 517 tagsZoneItem.setType(ZoneType.SERVICE); 518 tagsZoneItem.setServiceId(BlogConstants.TAGS_SERVICE_ID); 519 520 parameters = tagsZoneItem.getServiceParameters(); 521 parameters.setValue("service-title", translate("PLUGINS_BLOG_TAGS_ZONEITEM_TITLE", language)); 522 parameters.setValue("xslt", getDefaultXslt(BlogConstants.TAGS_SERVICE_ID)); 523 } 524 525 /** 526 * Create a profile content. 527 * @param workflowName The workflow name 528 * @param siteName the site name. 529 * @param lang the language. 530 * @param title The content's title 531 * @return the created person content. 532 * @throws WorkflowException if an error occurs 533 */ 534 protected Content createProfileContent(String workflowName, String siteName, String lang, String title) throws WorkflowException 535 { 536 String contentName = FilterNameHelper.filterName(title); 537 538 Map<String, Object> params = new HashMap<>(); 539 540 // Workflow result 541 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(); 542 543 Map<String, Object> workflowResult = new HashMap<>(); 544 params.put(AbstractWorkflowComponent.RESULT_MAP_KEY, workflowResult); 545 546 // Workflow parameters. 547 params.put("workflowName", workflowName); 548 params.put("org.ametys.web.repository.site.Site", siteName); 549 params.put(CreateContentFunction.CONTENT_NAME_KEY, contentName); 550 params.put(CreateContentFunction.CONTENT_TITLE_KEY, title); 551 params.put(CreateContentFunction.CONTENT_TYPES_KEY, new String[] {BlogConstants.PROFILE_CONTENT_TYPE}); 552 params.put(CreateContentFunction.CONTENT_LANGUAGE_KEY, lang); 553 554 workflow.initialize(workflowName, 1, params); 555 String contentId = (String) workflowResult.get("contentId"); 556 Content content = _ametysResolver.resolveById(contentId); 557 558 // FIXME API Check if modifiable. 559 ModifiableContent modifiableContent = (ModifiableContent) content; 560 561 modifiableContent.setTitle(title); 562 563 modifiableContent.saveChanges(); 564 565 return content; 566 } 567 568 /** 569 * Get the default value of the XSLT parameter of the given service. 570 * @param serviceId the service ID. 571 * @return the default XSLT parameter value. 572 */ 573 protected String getDefaultXslt(String serviceId) 574 { 575 Service service = _serviceEP.getExtension(serviceId); 576 if (service.hasModelItem("xslt")) 577 { 578 @SuppressWarnings("unchecked") 579 ElementDefinition<String> xsltParam = (ElementDefinition<String>) service.getModelItem("xslt"); 580 return xsltParam.getDefaultValue(); 581 } 582 else 583 { 584 return StringUtils.EMPTY; 585 } 586 } 587 588 /** 589 * Translate the key in the plugin's catalogue. 590 * @param key the i18n key. 591 * @param language the language. 592 * @return the translated value. 593 */ 594 protected String translate(String key, String language) 595 { 596 return _i18nUtils.translate(new I18nizableText(_i18nCatalogue, key), language); 597 } 598 599}