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