001/*
002 *  Copyright 2020 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.wall;
017
018import java.util.ArrayList;
019import java.util.List;
020import java.util.Set;
021import java.util.stream.Collectors;
022
023import org.apache.avalon.framework.service.ServiceException;
024import org.apache.avalon.framework.service.ServiceManager;
025import org.apache.commons.lang.IllegalClassException;
026
027import org.ametys.cms.repository.ContentQueryHelper;
028import org.ametys.cms.repository.ContentTypeExpression;
029import org.ametys.plugins.explorer.ExplorerNode;
030import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
031import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory;
032import org.ametys.plugins.repository.AmetysObject;
033import org.ametys.plugins.repository.AmetysRepositoryException;
034import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
035import org.ametys.plugins.repository.query.expression.AndExpression;
036import org.ametys.plugins.repository.query.expression.Expression;
037import org.ametys.plugins.repository.query.expression.Expression.Operator;
038import org.ametys.plugins.repository.query.expression.OrExpression;
039import org.ametys.plugins.repository.query.expression.StringExpression;
040import org.ametys.plugins.workspaces.AbstractWorkspaceModule;
041import org.ametys.plugins.workspaces.ObservationConstants;
042import org.ametys.plugins.workspaces.WorkspacesConstants;
043import org.ametys.plugins.workspaces.project.objects.Project;
044import org.ametys.runtime.i18n.I18nizableText;
045import org.ametys.web.repository.content.WebContentDAO;
046import org.ametys.web.repository.page.ModifiablePage;
047import org.ametys.web.repository.page.ModifiableZone;
048import org.ametys.web.repository.page.ModifiableZoneItem;
049import org.ametys.web.repository.page.Page;
050import org.ametys.web.repository.page.ZoneDAO;
051import org.ametys.web.repository.page.ZoneItem;
052import org.ametys.web.repository.page.ZoneItem.ZoneType;
053import org.ametys.web.repository.site.Site;
054import org.ametys.web.repository.sitemap.Sitemap;
055
056import com.google.common.collect.ImmutableSet;
057
058/**
059 * Workspace module for wall content
060 */
061public class WallContentModule extends AbstractWorkspaceModule
062{
063    /** The id of calendar module */
064    public static final String WALLCONTENT_MODULE_ID = WallContentModule.class.getName();
065    
066    /** The id of wall content service */
067    public static final String WALLCONTENT_SERVICE_ID = "org.ametys.web.service.SearchService";
068    
069    /** Search service content types */
070    protected static final String[] SEARCH_SERVICE_CONTENT_TYPES = new String[] {WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID};
071    
072    /** Search service returnables */
073    protected static final String[] SEARCH_SERVICE_RETURNABLES = new String[] {"org.ametys.web.frontoffice.search.metamodel.impl.ContentReturnable"};
074    
075    /** Search service sorts */
076    protected static final String[] SEARCH_SERVICE_SORTS = new String[] {"{\"name\":\"ContentReturnable$ContentSearchable$indexingField$org.ametys.plugins.workspaces.Content.wallContent$pinned\",\"sort\":\"DESC\"}", "{\"name\":\"ContentReturnable$ContentSearchable$systemProperty$creationDate\",\"sort\":\"DESC\"}"};
077    
078    /** Search service contexts */
079    protected static final String[] SEARCH_SERVICE_CONTEXTS = new String[] {"{\"sites\":\"{\\\"context\\\":\\\"CURRENT_SITE\\\",\\\"sites\\\":[]}\",\"search-sitemap-context\":\"{\\\"context\\\":\\\"CURRENT_SITE\\\",\\\"page\\\":null}\",\"context-lang\":\"CURRENT\",\"tags\":[]}"};
080    
081    /** Search service xslt */
082    protected static final String SEARCH_SERVICE_XSLT = "pages/services/search/wall-content.xsl";
083    
084    /** Workspaces wallcontent node name */
085    private static final String __WORKSPACES_WALLCONTENT_NODE_NAME = "wallcontent";
086
087    /** The zone DAO */
088    protected ZoneDAO _zoneDAO;
089    /** The content DAO */
090    protected WebContentDAO _contentDAO;
091    
092    @Override
093    public void service(ServiceManager manager) throws ServiceException
094    {
095        super.service(manager);
096        _zoneDAO = (ZoneDAO) manager.lookup(ZoneDAO.ROLE);
097        _contentDAO = (WebContentDAO) manager.lookup(WebContentDAO.ROLE);
098    }
099    
100    
101    public String getId()
102    {
103        return WALLCONTENT_MODULE_ID;
104    }
105
106    public String getModuleName()
107    {
108        return __WORKSPACES_WALLCONTENT_NODE_NAME;
109    }
110    
111    public int getOrder()
112    {
113        return ORDER_WALLCONTENT;
114    }
115
116    public Set<String> getAllowedEventTypes()
117    {
118        return ImmutableSet.of(ObservationConstants.EVENT_WALLCONTENT_ADDED);
119    }
120
121    public ModifiableResourceCollection getModuleRoot(Project project, boolean create)
122    {
123        // No root for this module, but root is still needed to handle rights on it
124        try
125        {
126            ExplorerNode projectRootNode = project.getExplorerRootNode();
127            
128            if (projectRootNode instanceof ModifiableResourceCollection)
129            {
130                ModifiableResourceCollection projectRootNodeRc = (ModifiableResourceCollection) projectRootNode;
131                return _getAmetysObject(projectRootNodeRc, __WORKSPACES_WALLCONTENT_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create);
132            }
133            else
134            {
135                throw new IllegalClassException(ModifiableResourceCollection.class, projectRootNode.getClass());
136            }
137        }
138        catch (AmetysRepositoryException e)
139        {
140            throw new AmetysRepositoryException("Error getting the wallcontent root node.", e);
141        }
142    }
143
144    @Override
145    protected String getModulePageName()
146    {
147        return "index";
148    }
149
150    public I18nizableText getModuleTitle()
151    {
152        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_WALLCONTENT_LABEL");
153    }
154    public I18nizableText getModuleDescription()
155    {
156        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_WALLCONTENT_DESCRIPTION");
157    }
158    @Override
159    protected I18nizableText getModulePageTitle()
160    {
161        // The module page is the index page, which already exists
162        return null;
163    }
164
165    @Override
166    protected void initializeModulePage(ModifiablePage modulePage)
167    {
168        ModifiableZone defaultZone;
169        if (modulePage.hasZone("default"))
170        {
171            defaultZone = modulePage.getZone("default");
172        }
173        else
174        {
175            defaultZone = modulePage.createZone("default");            
176        }
177        
178        boolean hasService = defaultZone.getZoneItems().stream().anyMatch(zi -> WALLCONTENT_SERVICE_ID.equals(zi.getServiceId()));
179        
180        if (!hasService)
181        {
182            ModifiableZoneItem defaultZoneItem = defaultZone.addZoneItem();
183            defaultZoneItem.setType(ZoneType.SERVICE);
184            defaultZoneItem.setServiceId(WALLCONTENT_SERVICE_ID);
185            
186            ModifiableModelAwareDataHolder serviceDataHolder = defaultZoneItem.getServiceParameters();
187            serviceDataHolder.setValue("header", null);
188            serviceDataHolder.setValue("contentTypes", SEARCH_SERVICE_CONTENT_TYPES);
189            serviceDataHolder.setValue("returnables", SEARCH_SERVICE_RETURNABLES);
190            serviceDataHolder.setValue("initialSorts", SEARCH_SERVICE_SORTS);
191            serviceDataHolder.setValue("contexts", SEARCH_SERVICE_CONTEXTS);
192            serviceDataHolder.setValue("resultsPerPage", 15);
193            serviceDataHolder.setValue("rightCheckingMode", "none");
194            serviceDataHolder.setValue("resultPlace", "ABOVE_CRITERIA");
195            serviceDataHolder.setValue("launchSearchAtStartup", true);
196            serviceDataHolder.setValue("rss", false);
197            serviceDataHolder.setValue("contentView", "main");
198            serviceDataHolder.setValue("xslt", SEARCH_SERVICE_XSLT);
199        }
200    }
201
202    /*
203     * Override because the page should already exist, the parent returns null in this case
204     */
205    @Override
206    protected ModifiablePage _createModulePage(Project project, Sitemap sitemap, String name, I18nizableText pageTitle, String skinTemplate)
207    {
208        if (sitemap.hasChild(name))
209        {
210            return sitemap.getChild(name);
211        }
212        else
213        {
214            return super._createModulePage(project, sitemap, name, pageTitle, skinTemplate);
215        }
216    }
217    
218    @Override
219    protected void _deletePages(Project project)
220    {
221        // Nothing. Index page should not be deleted.
222    }
223    
224    @Override
225    protected void _internalDeactivateModule(Project project)
226    {
227        // Remove wall service
228        _removeWallService(project);
229    }
230    
231    @Override
232    protected void _internalDeleteData(Project project)
233    {
234        // Remove wall service
235        _removeWallService(project);
236        
237        // Delete wall contents
238        _deleteWallContents(project);
239    }
240    
241    private void _deleteWallContents(Project project)
242    {
243        Expression cTypeExpr = new ContentTypeExpression(Operator.EQ, WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID);
244        
245        List<Expression> siteExpr = new ArrayList<>();
246        for (Site site : project.getSites())
247        {
248            siteExpr.add(new StringExpression("site", Operator.EQ, site.getName()));
249        }
250        
251        Expression expr = new AndExpression(cTypeExpr, new OrExpression(siteExpr.toArray(new Expression[siteExpr.size()])));
252        
253        String xPathQuery = ContentQueryHelper.getContentXPathQuery(expr);
254        
255        List<String> contentIds = _resolver.query(xPathQuery)
256            .stream()
257            .map(AmetysObject::getId)
258            .collect(Collectors.toList());
259        
260        _contentDAO.deleteContents(contentIds, true);
261    }
262    
263    private void _removeWallService(Project project)
264    {
265        List<Page> modulePages = _getModulePages(project);
266        for (Page page : modulePages)
267        {
268            if (page.hasZone("default"))
269            {
270                _projectManager.untagProjectPage((ModifiablePage) page, getModuleRoot(project, false));
271                
272                ModifiableZone defaultZone = ((ModifiablePage) page).getZone("default");
273                
274                Set<String> zoneItemIds = defaultZone.getZoneItems()
275                        .stream()
276                        .filter(zi -> WALLCONTENT_SERVICE_ID.equals(zi.getServiceId()))
277                        .map(ZoneItem::getId)
278                        .collect(Collectors.toSet());
279
280                for (String zoneItemId : zoneItemIds)
281                {
282                    _zoneDAO.removeZoneItem(zoneItemId);
283                }
284            }
285        }
286    }
287}