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