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.HashMap;
019import java.util.Map;
020import java.util.Optional;
021
022import javax.jcr.Node;
023import javax.jcr.NodeIterator;
024import javax.jcr.RepositoryException;
025
026import org.apache.avalon.framework.CascadingRuntimeException;
027import org.apache.avalon.framework.context.Context;
028import org.apache.avalon.framework.context.ContextException;
029import org.apache.avalon.framework.context.Contextualizable;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.cocoon.components.ContextHelper;
034import org.apache.cocoon.environment.Request;
035import org.apache.commons.lang.StringUtils;
036import org.apache.commons.lang3.tuple.Triple;
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.AmetysObjectIterable;
049import org.ametys.plugins.repository.AmetysObjectResolver;
050import org.ametys.plugins.repository.AmetysRepositoryException;
051import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
052import org.ametys.plugins.repository.events.JCREventHelper;
053import org.ametys.plugins.workspaces.project.ProjectConstants;
054import org.ametys.plugins.workspaces.project.ProjectManager;
055import org.ametys.plugins.workspaces.project.modules.WorkspaceModule;
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.Page;
065import org.ametys.web.repository.page.Page.PageType;
066import org.ametys.web.repository.page.PageDAO;
067import org.ametys.web.repository.site.Site;
068import org.ametys.web.repository.sitemap.Sitemap;
069import org.ametys.web.service.Service;
070import org.ametys.web.service.ServiceExtensionPoint;
071import org.ametys.web.skin.Skin;
072import org.ametys.web.skin.SkinTemplate;
073import org.ametys.web.skin.SkinsManager;
074
075/**
076 * Abstract class for {@link WorkspaceModule} implementation
077 *
078 */
079public abstract class AbstractWorkspaceModule extends AbstractLogEnabled implements WorkspaceModule, Serviceable, Contextualizable, PluginAware
080{
081    private static final String __URL_CACHE_REQUEST_ATTRIBUTE_NAME = AbstractWorkspaceModule.class.getName() + "$urlCache";
082    
083    /** Project manager */
084    protected ProjectManager _projectManager;
085    /** Project right helper */
086    protected ProjectRightHelper _projectRightHelper;
087    /** User manager */
088    protected UserManager _userManager;
089    /** Ametys resolver */
090    protected AmetysObjectResolver _resolver;
091    /** The rights manager */
092    protected RightManager _rightManager;
093    /** Observer manager. */
094    protected ObservationManager _observationManager;
095    /** The current user provider. */
096    protected CurrentUserProvider _currentUserProvider;
097    /** The users manager */
098    protected UserHelper _userHelper;
099    /** The i18n utils. */
100    protected I18nUtils _i18nUtils;
101    /** The skins manager. */
102    protected SkinsManager _skinsManager;
103    /** The page DAO */
104    protected PageDAO _pageDAO;
105    /** The avalon context */
106    protected Context _context;
107    /** The plugin name */
108    protected String _pluginName;
109    /** The services handler */
110    protected ServiceExtensionPoint _serviceEP;
111    
112    @Override
113    public void service(ServiceManager manager) throws ServiceException
114    {
115        _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE);
116        _projectRightHelper = (ProjectRightHelper) manager.lookup(ProjectRightHelper.ROLE);
117        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
118        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
119        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
120        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
121        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
122        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
123        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
124        _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE);
125        _pageDAO = (PageDAO) manager.lookup(PageDAO.ROLE);
126        _serviceEP = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE);
127    }
128
129    @Override
130    public void contextualize(Context context) throws ContextException
131    {
132        _context = context;
133    }
134    
135    public void setPluginInfo(String pluginName, String featureName, String id)
136    {
137        _pluginName = pluginName;
138    }
139    
140    @Override
141    public void deactivateModule(Project project)
142    {
143        _deletePages(project, getModulePageName());
144        _internalDeactivateModule(project);
145        
146        // Delete root
147        ModifiableResourceCollection moduleRoot = getModuleRoot(project, false);
148        if (moduleRoot != null)
149        {
150            moduleRoot.remove();
151        }
152        
153        // Delete events
154        try
155        {
156            NodeIterator events = JCREventHelper.getEvents(project, getAllowedEventTypes().toArray(new String[]{}));
157            while (events.hasNext())
158            {
159                Node event = (Node) events.next();
160                event.remove();
161            }
162        }
163        catch (RepositoryException e)
164        {
165            getLogger().warn("Unable to delete project '" + project.getName() + "' events for module '" + this.getId() + "'", e);
166        }
167    }
168    
169    @Override
170    public void activateModule(Project project)
171    {
172        for (Site site : project.getSites())
173        {
174            for (Sitemap sitemap : site.getSitemaps())
175            {
176                initializeSitemap(sitemap);
177            }
178        }
179
180        _internalActivateModule(project);
181        
182        // create the resources root node
183        getModuleRoot(project, true);
184    }
185    
186    @Override
187    public void initializeSitemap(Sitemap sitemap)
188    {
189        ModifiablePage page = _createModulePage(sitemap, getModulePageName(), getModulePageTitle(), getModulePageTemplate());
190        
191        if (page != null)
192        {
193            page.tag("SECTION");
194            page.tag(getModuleTagName());
195            
196            initializeModulePage(page);
197            
198            page.saveChanges();
199            
200            Map<String, Object> eventParams = new HashMap<>();
201            eventParams.put(ObservationConstants.ARGS_PAGE, page);
202            _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_ADDED, _currentUserProvider.getUser(), eventParams));
203        }
204    }
205    
206    @Override
207    public AmetysObjectIterable<Page> getModulePages(Project project, String language)
208    {
209        return _projectManager.getProjectPages(project, getModuleTagName(), language);
210    }
211    
212    @Override
213    public String getModuleUrl(Project project, String language)
214    {
215        // <ProjectName, TagName, Language>
216        Map<Triple<String, String, String>, String> cache = _getModuleUrlsCache();
217        if (cache != null)
218        {
219            return cache.computeIfAbsent(Triple.of(project.getName(), getModuleTagName(), language), url -> _computedModuleUrl(project, language));
220        }
221        else
222        {
223            return _computedModuleUrl(project, language);
224        }
225    }
226    
227    String _computedModuleUrl(Project project, String language)
228    {
229        Optional<String> url = getModulePages(project, language).stream()
230            .findFirst()
231            .map(page -> ResolveURIComponent.resolve("page", page.getId()));
232        
233        if (url.isPresent())
234        {
235            return url.get();
236        }
237        else
238        {
239            // No page found
240            return null;
241        }
242    }
243    
244
245    Map<Triple<String, String, String>, String> _getModuleUrlsCache()
246    {
247        Request request;
248        try
249        {
250            request = ContextHelper.getRequest(_context);
251        }
252        catch (CascadingRuntimeException e)
253        {
254            return null;
255        }
256        
257        if (request == null)
258        {
259            return null;
260        }
261        
262        @SuppressWarnings("unchecked")
263        Map<Triple<String, String, String>, String> cache = (Map<Triple<String, String, String>, String>) request.getAttribute(__URL_CACHE_REQUEST_ATTRIBUTE_NAME);
264        if (cache == null)
265        {
266            cache = new HashMap<>();
267            request.setAttribute(__URL_CACHE_REQUEST_ATTRIBUTE_NAME, cache);
268        }
269        
270        return cache;
271    }
272    
273    
274    /**
275     * Create a new page if not already exists
276     * @param sitemap The sitemap where the page will be created
277     * @param name The page's name
278     * @param pageTitle The page's title as i18nizable text
279     * @param skinTemplate The template from the skin to apply on the page
280     * @return the created page or <code>null</code> if page already exists
281     */
282    protected ModifiablePage _createModulePage(Sitemap sitemap, String name, I18nizableText pageTitle, String skinTemplate)
283    {
284        if (!sitemap.hasChild(name))
285        {
286            ModifiablePage page = sitemap.createChild(name, "ametys:defaultPage");
287            
288            // Title should not be missing, but just in case if the i18n message or the whole catalog does not exists in the requested language
289            // to prevent a non-user-friendly error and still generate the project workspace.
290            page.setTitle(StringUtils.defaultIfEmpty(_i18nUtils.translate(pageTitle, sitemap.getName()), "Missing title"));
291            page.setType(PageType.NODE);
292            page.setSiteName(sitemap.getSiteName());
293            page.setSitemapName(sitemap.getName());
294            
295            Site site = page.getSite();
296            Skin skin = _skinsManager.getSkin(site.getSkinId());
297            SkinTemplate template = skin.getTemplate(skinTemplate);
298            if (template != null)
299            {
300                // Set the type and template.
301                page.setType(PageType.CONTAINER);
302                page.setTemplate(skinTemplate);
303            }
304            else
305            {
306                getLogger().error(String.format(
307                        "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.",
308                        site.getName(), site.getSkinId(), skinTemplate, page.getName()));
309            }
310            
311            sitemap.saveChanges();
312            
313            return page;
314        }
315        else
316        {
317            return null;
318        }
319    }
320    
321    /**
322     * Delete all pages of the project sites matching the page name
323     * @param project The project
324     * @param pageName The page name
325     */
326    protected void _deletePages(Project project, String pageName)
327    {
328        for (Site site : project.getSites())
329        {
330            for (Sitemap sitemap : site.getSitemaps())
331            {
332                if (sitemap.hasChild(pageName))
333                {
334                    ModifiablePage page = sitemap.getChild(pageName);
335                    _pageDAO.deletePage(page, true);
336                }
337            }
338            
339            site.saveChanges();
340        }
341    }
342    
343    /**
344     * Get the default value of the XSLT parameter of the given service.
345     * @param serviceId the service ID.
346     * @return the default XSLT parameter value.
347     */
348    protected String _getDefaultXslt(String serviceId)
349    {
350        Service service = _serviceEP.hasExtension(serviceId) ? _serviceEP.getExtension(serviceId) : null;
351        if (service != null)
352        {
353            @SuppressWarnings("unchecked")
354            ElementDefinition<String> xsltParameterDefinition = (ElementDefinition<String>) service.getParameters().get("xslt");
355            
356            if (xsltParameterDefinition != null)
357            {
358                return xsltParameterDefinition.getDefaultValue();
359            }
360        }
361        
362        return StringUtils.EMPTY;
363    }
364    
365    /**
366     * Returns the tag of apply to module page
367     * @return The tag for module page
368     */
369    protected abstract String getModuleTagName();
370    
371    /**
372     * Returns the module page's name
373     * @return The module page's name
374     */
375    protected abstract String getModulePageName();
376    
377    /**
378     * Returns the module page's title as i18n
379     * @return The module page's title
380     */
381    protected abstract I18nizableText getModulePageTitle();
382    
383    /**
384     * Returns the template to use for module's page
385     * @return The template
386     */
387    protected String getModulePageTemplate() 
388    {
389        return ProjectConstants.PROJECT_TEMPLATE;
390    }
391    
392    /**
393     * Initialize the module page
394     * @param modulePage The module page
395     */
396    protected abstract void initializeModulePage(ModifiablePage modulePage);
397    
398    /**
399     * Internal process when module is deactivated
400     * @param project The project
401     */
402    protected void _internalDeactivateModule(Project project) 
403    {
404        // Empty
405    }
406    
407    /**
408     * Internal process when module is activated
409     * @param project The project
410     */
411    protected void _internalActivateModule(Project project)
412    {
413        // Empty
414    }
415    
416    /**
417     * Utility method to get or create an ametys object
418     * @param <A> A sub class of AmetysObject
419     * @param parent The parent object
420     * @param name The ametys object name
421     * @param type The ametys object type
422     * @param create True to create the object if it does not exist
423     * @return ametys object
424     * @throws AmetysRepositoryException if an repository error occurs
425     */
426    protected <A extends AmetysObject> A _getAmetysObject(ModifiableTraversableAmetysObject parent, String name, String type, boolean create) throws AmetysRepositoryException
427    {
428        A object = null;
429        
430        if (parent.hasChild(name))
431        {
432            object = parent.getChild(name);
433        }
434        else if (create)
435        {
436            object = parent.createChild(name, type);
437            parent.saveChanges();
438        }
439        
440        return object;
441    }
442}