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