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.HashMap;
019import java.util.List;
020import java.util.Map;
021import java.util.Objects;
022import java.util.Set;
023import java.util.stream.Collectors;
024
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.commons.lang.IllegalClassException;
028
029import org.ametys.cms.data.Binary;
030import org.ametys.cms.repository.Content;
031import org.ametys.cms.repository.ContentQueryHelper;
032import org.ametys.cms.repository.ContentTypeExpression;
033import org.ametys.cms.search.content.ContentSearcherFactory;
034import org.ametys.plugins.explorer.ExplorerNode;
035import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
036import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory;
037import org.ametys.plugins.repository.AmetysObject;
038import org.ametys.plugins.repository.AmetysObjectIterable;
039import org.ametys.plugins.repository.AmetysRepositoryException;
040import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
041import org.ametys.plugins.repository.query.expression.AndExpression;
042import org.ametys.plugins.repository.query.expression.Expression;
043import org.ametys.plugins.repository.query.expression.Expression.Operator;
044import org.ametys.plugins.repository.query.expression.StringExpression;
045import org.ametys.plugins.workspaces.AbstractWorkspaceModule;
046import org.ametys.plugins.workspaces.ObservationConstants;
047import org.ametys.plugins.workspaces.WorkspacesConstants;
048import org.ametys.plugins.workspaces.project.objects.Project;
049import org.ametys.plugins.workspaces.util.StatisticColumn;
050import org.ametys.plugins.workspaces.util.StatisticsColumnType;
051import org.ametys.runtime.i18n.I18nizableText;
052import org.ametys.web.repository.content.WebContentDAO;
053import org.ametys.web.repository.page.ModifiablePage;
054import org.ametys.web.repository.page.ModifiableZone;
055import org.ametys.web.repository.page.ModifiableZoneItem;
056import org.ametys.web.repository.page.Page;
057import org.ametys.web.repository.page.ZoneDAO;
058import org.ametys.web.repository.page.ZoneItem;
059import org.ametys.web.repository.page.ZoneItem.ZoneType;
060import org.ametys.web.repository.sitemap.Sitemap;
061import org.ametys.web.search.query.SiteQuery;
062
063import com.google.common.collect.ImmutableSet;
064
065/**
066 * Workspace module for wall content
067 */
068public class WallContentModule extends AbstractWorkspaceModule
069{
070    /** The id of calendar module */
071    public static final String WALLCONTENT_MODULE_ID = WallContentModule.class.getName();
072    
073    /** The id of wall content service */
074    public static final String WALLCONTENT_SERVICE_ID = "org.ametys.web.service.SearchService";
075    
076    /** Search service content types */
077    protected static final String[] SEARCH_SERVICE_CONTENT_TYPES = new String[] {WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID};
078    
079    /** Search service returnables */
080    protected static final String[] SEARCH_SERVICE_RETURNABLES = new String[] {"org.ametys.web.frontoffice.search.metamodel.impl.ContentReturnable"};
081    
082    /** Search service sorts */
083    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\"}"};
084    
085    /** Search service contexts */
086    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\":[]}"};
087    
088    /** Search service xslt */
089    protected static final String SEARCH_SERVICE_XSLT = "pages/services/search/wall-content.xsl";
090    
091    /** Workspaces wallcontent node name */
092    private static final String __WORKSPACES_WALLCONTENT_NODE_NAME = "wallcontent";
093
094    private static final String __WALLCONTENT_NUMBER_HEADER_ID = __WORKSPACES_WALLCONTENT_NODE_NAME + "$wall_content_number";
095    
096    /** The zone DAO */
097    protected ZoneDAO _zoneDAO;
098    /** The content DAO */
099    protected WebContentDAO _contentDAO;
100    /** The content searcher factory */
101    protected ContentSearcherFactory _contentSearcherFactory;
102    
103    @Override
104    public void service(ServiceManager manager) throws ServiceException
105    {
106        super.service(manager);
107        _zoneDAO = (ZoneDAO) manager.lookup(ZoneDAO.ROLE);
108        _contentDAO = (WebContentDAO) manager.lookup(WebContentDAO.ROLE);
109        _contentSearcherFactory = (ContentSearcherFactory) manager.lookup(ContentSearcherFactory.ROLE);
110    }
111    
112    public String getId()
113    {
114        return WALLCONTENT_MODULE_ID;
115    }
116
117    public String getModuleName()
118    {
119        return __WORKSPACES_WALLCONTENT_NODE_NAME;
120    }
121    
122    public int getOrder()
123    {
124        return ORDER_WALLCONTENT;
125    }
126
127    public Set<String> getAllowedEventTypes()
128    {
129        return ImmutableSet.of(ObservationConstants.EVENT_WALLCONTENT_ADDED, org.ametys.cms.ObservationConstants.EVENT_CONTENT_COMMENT_VALIDATED);
130    }
131
132    public ModifiableResourceCollection getModuleRoot(Project project, boolean create)
133    {
134        // No root for this module, but root is still needed to handle rights on it
135        try
136        {
137            ExplorerNode projectRootNode = project.getExplorerRootNode();
138            
139            if (projectRootNode instanceof ModifiableResourceCollection)
140            {
141                ModifiableResourceCollection projectRootNodeRc = (ModifiableResourceCollection) projectRootNode;
142                return _getAmetysObject(projectRootNodeRc, __WORKSPACES_WALLCONTENT_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create);
143            }
144            else
145            {
146                throw new IllegalClassException(ModifiableResourceCollection.class, projectRootNode.getClass());
147            }
148        }
149        catch (AmetysRepositoryException e)
150        {
151            throw new AmetysRepositoryException("Error getting the wallcontent root node.", e);
152        }
153    }
154
155    @Override
156    protected String getModulePageName()
157    {
158        return "index";
159    }
160
161    public I18nizableText getModuleTitle()
162    {
163        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_WALLCONTENT_LABEL");
164    }
165    public I18nizableText getModuleDescription()
166    {
167        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_WALLCONTENT_DESCRIPTION");
168    }
169    @Override
170    protected I18nizableText getModulePageTitle()
171    {
172        // The module page is the index page, which already exists
173        return null;
174    }
175
176    @Override
177    protected void initializeModulePage(ModifiablePage modulePage)
178    {
179        ModifiableZone defaultZone;
180        if (modulePage.hasZone("default"))
181        {
182            defaultZone = modulePage.getZone("default");
183        }
184        else
185        {
186            defaultZone = modulePage.createZone("default");            
187        }
188        
189        boolean hasService = defaultZone.getZoneItems().stream().anyMatch(zi -> WALLCONTENT_SERVICE_ID.equals(zi.getServiceId()));
190        
191        if (!hasService)
192        {
193            ModifiableZoneItem defaultZoneItem = defaultZone.addZoneItem();
194            defaultZoneItem.setType(ZoneType.SERVICE);
195            defaultZoneItem.setServiceId(WALLCONTENT_SERVICE_ID);
196            
197            ModifiableModelAwareDataHolder serviceDataHolder = defaultZoneItem.getServiceParameters();
198            serviceDataHolder.setValue("header", null);
199            serviceDataHolder.setValue("contentTypes", SEARCH_SERVICE_CONTENT_TYPES);
200            serviceDataHolder.setValue("returnables", SEARCH_SERVICE_RETURNABLES);
201            serviceDataHolder.setValue("initialSorts", SEARCH_SERVICE_SORTS);
202            serviceDataHolder.setValue("contexts", SEARCH_SERVICE_CONTEXTS);
203            serviceDataHolder.setValue("resultsPerPage", 15);
204            serviceDataHolder.setValue("rightCheckingMode", "none");
205            serviceDataHolder.setValue("resultPlace", "ABOVE_CRITERIA");
206            serviceDataHolder.setValue("launchSearchAtStartup", true);
207            serviceDataHolder.setValue("rss", false);
208            serviceDataHolder.setValue("contentView", "main");
209            serviceDataHolder.setValue("xslt", SEARCH_SERVICE_XSLT);
210        }
211    }
212
213    /*
214     * Override because the page should already exist, the parent returns null in this case
215     */
216    @Override
217    protected ModifiablePage _createModulePage(Project project, Sitemap sitemap, String name, I18nizableText pageTitle, String skinTemplate)
218    {
219        if (sitemap.hasChild(name))
220        {
221            return sitemap.getChild(name);
222        }
223        else
224        {
225            return super._createModulePage(project, sitemap, name, pageTitle, skinTemplate);
226        }
227    }
228    
229    @Override
230    protected void _deletePages(Project project)
231    {
232        // Nothing. Index page should not be deleted.
233    }
234    
235    @Override
236    protected void _internalDeactivateModule(Project project)
237    {
238        // Remove wall service
239        _removeWallService(project);
240    }
241    
242    @Override
243    protected void _internalDeleteData(Project project)
244    {
245        // Remove wall service
246        _removeWallService(project);
247        
248        // Delete wall contents
249        _deleteWallContents(project);
250    }
251    
252    private void _deleteWallContents(Project project)
253    {
254        Expression cTypeExpr = new ContentTypeExpression(Operator.EQ, WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID);
255        
256        Expression siteExpr = new StringExpression("site", Operator.EQ, project.getSite().getName());
257        
258        Expression expr = new AndExpression(cTypeExpr, siteExpr);
259        
260        String xPathQuery = ContentQueryHelper.getContentXPathQuery(expr);
261        
262        List<String> contentIds = _resolver.query(xPathQuery)
263            .stream()
264            .map(AmetysObject::getId)
265            .collect(Collectors.toList());
266        
267        _contentDAO.deleteContents(contentIds, true);
268    }
269    
270    private void _removeWallService(Project project)
271    {
272        List<Page> modulePages = _getModulePages(project);
273        for (Page page : modulePages)
274        {
275            if (page.hasZone("default"))
276            {
277                _projectManager.untagProjectPage((ModifiablePage) page, getModuleRoot(project, false));
278                
279                ModifiableZone defaultZone = ((ModifiablePage) page).getZone("default");
280                
281                Set<String> zoneItemIds = defaultZone.getZoneItems()
282                        .stream()
283                        .filter(zi -> WALLCONTENT_SERVICE_ID.equals(zi.getServiceId()))
284                        .map(ZoneItem::getId)
285                        .collect(Collectors.toSet());
286
287                for (String zoneItemId : zoneItemIds)
288                {
289                    _zoneDAO.removeZoneItem(zoneItemId);
290                }
291            }
292        }
293    }
294    
295    @Override
296    public Map<String, Object> _getInternalStatistics(Project project, boolean isActive)
297    { 
298        if (isActive)
299        {
300            Map<String, Object> statistics = new HashMap<>();
301            try
302            {
303                AmetysObjectIterable<Content> results = _contentSearcherFactory.create(WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID)
304                        .search(new SiteQuery(project.getName()));
305                statistics.put(__WALLCONTENT_NUMBER_HEADER_ID, results.getSize());
306            }
307            catch (Exception e)
308            {
309                getLogger().error("Error searching wall content in project " + project.getId(), e);
310            }
311            return statistics;
312        }
313        else
314        {
315            return Map.of(__WALLCONTENT_NUMBER_HEADER_ID, __SIZE_INACTIVE);
316        }
317    }
318
319    @Override
320    public List<StatisticColumn> _getInternalStatisticModel()
321    {
322        return List.of(new StatisticColumn(__WALLCONTENT_NUMBER_HEADER_ID, new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_WALL_CONTENT_NUMBER"))
323                .withRenderer("Ametys.plugins.workspaces.project.tool.ProjectsGridHelper.renderElements")
324                .withType(StatisticsColumnType.LONG)
325                .withGroup(GROUP_HEADER_ELEMENTS_ID));
326    }
327
328    @Override
329    protected long _getModuleSize(Project project)
330    {
331        try
332        {
333            AmetysObjectIterable<Content> resultsIterable = _contentSearcherFactory.create(WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID)
334                    .search(new SiteQuery(project.getName()));
335
336            return resultsIterable.stream()
337                .map(content -> content.getValue("illustration/image"))
338                .filter(Objects::nonNull)
339                .filter(Binary.class::isInstance)
340                .map(Binary.class::cast)
341                .mapToLong(Binary::getLength)
342                .sum();
343        }
344        catch (Exception e)
345        {
346            getLogger().error("Error searching wall content images in project " + project.getId(), e);
347            return -1;
348        }
349    }
350
351    @Override
352    protected boolean _showModuleSize()
353    {
354        return true;
355    }
356}