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