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