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        @SuppressWarnings("unchecked")
577        ElementDefinition<String> xsltParam = (ElementDefinition<String>) service.getModelItem("xslt");
578        return xsltParam != null ? xsltParam.getDefaultValue() : StringUtils.EMPTY;
579    }
580    
581    /**
582     * Translate the key in the plugin's catalogue.
583     * @param key the i18n key.
584     * @param language the language.
585     * @return the translated value.
586     */
587    protected String translate(String key, String language)
588    {
589        return _i18nUtils.translate(new I18nizableText(_i18nCatalogue, key), language);
590    }
591    
592}