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.news;
017
018import java.util.HashMap;
019import java.util.List;
020import java.util.Map;
021import java.util.Objects;
022import java.util.Set;
023
024import org.apache.avalon.framework.service.ServiceException;
025import org.apache.avalon.framework.service.ServiceManager;
026import org.apache.commons.lang.IllegalClassException;
027import org.apache.commons.lang.StringUtils;
028
029import org.ametys.cms.data.Binary;
030import org.ametys.cms.repository.Content;
031import org.ametys.cms.search.content.ContentSearcherFactory;
032import org.ametys.plugins.explorer.ExplorerNode;
033import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
034import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory;
035import org.ametys.plugins.repository.AmetysObjectIterable;
036import org.ametys.plugins.repository.AmetysRepositoryException;
037import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
038import org.ametys.plugins.workspaces.AbstractWorkspaceModule;
039import org.ametys.plugins.workspaces.ObservationConstants;
040import org.ametys.plugins.workspaces.WorkspacesConstants;
041import org.ametys.plugins.workspaces.project.objects.Project;
042import org.ametys.plugins.workspaces.util.StatisticColumn;
043import org.ametys.plugins.workspaces.util.StatisticsColumnType;
044import org.ametys.runtime.i18n.I18nizableText;
045import org.ametys.web.repository.page.ModifiablePage;
046import org.ametys.web.repository.page.ModifiableZone;
047import org.ametys.web.repository.page.ModifiableZoneItem;
048import org.ametys.web.repository.page.Page.PageType;
049import org.ametys.web.repository.page.ZoneItem.ZoneType;
050import org.ametys.web.repository.sitemap.Sitemap;
051import org.ametys.web.search.query.SiteQuery;
052
053/**
054 * Workspaces module for news
055 */
056public class NewsWorkspaceModule extends AbstractWorkspaceModule
057{
058    /** The id of calendar module */
059    public static final String NEWS_MODULE_ID = NewsWorkspaceModule.class.getName();
060    
061    /** The id of news service */
062    public static final String NEWS_SERVICE_ID = "org.ametys.web.service.SearchService";
063    
064    /** Name of news's template */
065    public static final String NEWS_PAGE_TEMPLATE = "news";
066
067    /** Search service content types */
068    protected static final String[] SEARCH_SERVICE_CONTENT_TYPES = new String[] {WorkspacesConstants.PROJECT_NEWS_CONTENT_TYPE_ID};
069    
070    /** Search service returnables */
071    protected static final String[] SEARCH_SERVICE_RETURNABLES = new String[] {"org.ametys.web.frontoffice.search.metamodel.impl.ContentReturnable"};
072    
073    /** Search service sorts */
074    protected static final String[] SEARCH_SERVICE_SORTS = new String[] {"{\"name\":\"ContentReturnable$ContentSearchable$indexingField$org.ametys.plugins.workspaces.Content.abstractNews$start-date\",\"sort\":\"DESC\"}", "{\"name\":\"ContentReturnable$ContentSearchable$indexingField$org.ametys.plugins.workspaces.Content.abstractNews$end-date\",\"sort\":\"DESC\"}"};
075    
076    /** Search service contexts */
077    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\":[]}"};
078    
079    /** Search service content view */
080    protected static final String SEARCH_SEARCH_CONTENT_VIEW = "portlet";
081    
082    /** Search service xslt */
083    protected static final String SEARCH_SERVICE_XSLT = "pages/services/search/project-news.xsl";
084    
085    /** Workspaces calendars node name */
086    private static final String __WORKSPACES_NEWS_NODE_NAME = "news";
087
088    private static final String __NEWS_NUMBER_HEADER_ID = __WORKSPACES_NEWS_NODE_NAME + "$news_number";
089
090    private ContentSearcherFactory _contentSearcherFactory;
091
092    
093    @Override
094    public void service(ServiceManager smanager) throws ServiceException
095    {
096        super.service(smanager);
097        _contentSearcherFactory = (ContentSearcherFactory) smanager.lookup(ContentSearcherFactory.ROLE);
098    }
099    
100    public String getId()
101    {
102        return NEWS_MODULE_ID;
103    }
104
105    public int getOrder()
106    {
107        return ORDER_NEWS;
108    }
109
110    public String getModuleName()
111    {
112        return __WORKSPACES_NEWS_NODE_NAME;
113    }
114
115    public Set<String> getAllowedEventTypes()
116    {
117        return Set.of(ObservationConstants.EVENT_PROJECT_NEWS_PUBLISHED, org.ametys.cms.ObservationConstants.EVENT_CONTENT_COMMENT_VALIDATED);
118    }
119
120    public ModifiableResourceCollection getModuleRoot(Project project, boolean create)
121    {
122        try
123        {
124            ExplorerNode projectRootNode = project.getExplorerRootNode();
125            
126            if (projectRootNode instanceof ModifiableResourceCollection)
127            {
128                ModifiableResourceCollection projectRootNodeRc = (ModifiableResourceCollection) projectRootNode;
129                return _getAmetysObject(projectRootNodeRc, __WORKSPACES_NEWS_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create);
130            }
131            else
132            {
133                throw new IllegalClassException(ModifiableResourceCollection.class, projectRootNode.getClass());
134            }
135        }
136        catch (AmetysRepositoryException e)
137        {
138            throw new AmetysRepositoryException("Error getting the news root node.", e);
139        }
140    }
141
142    @Override
143    protected String getModulePageName()
144    {
145        return "news";
146    }
147
148    public I18nizableText getModuleTitle()
149    {
150        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_MODULE_NEWS_LABEL");
151    }
152    public I18nizableText getModuleDescription()
153    {
154        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_MODULE_NEWS_DESCRIPTION");
155    }
156
157    @Override
158    protected I18nizableText getModulePageTitle()
159    {
160        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_WORKSPACE_PAGE_NEWS_TITLE");
161    }
162    
163    @Override
164    protected ModifiablePage _createModulePage(Project project, Sitemap sitemap, String name, I18nizableText pageTitle, String skinTemplate)
165    {
166        if (!sitemap.hasChild(name))
167        {
168            ModifiablePage page = sitemap.createChild(name, "ametys:defaultPage");
169            
170            // Title should not be missing, but just in case if the i18n message or the whole catalog does not exists in the requested language
171            // to prevent a non-user-friendly error and still generate the project workspace.
172            page.setTitle(StringUtils.defaultIfEmpty(_i18nUtils.translate(pageTitle, sitemap.getName()), "Missing title"));
173            page.setType(PageType.CONTAINER);
174            page.setTemplate(NEWS_PAGE_TEMPLATE);
175            page.setSiteName(sitemap.getSiteName());
176            page.setSitemapName(sitemap.getName());
177            
178            sitemap.saveChanges();
179            
180            return page;
181        }
182        else
183        {
184            return null;
185        }
186    }
187
188    @Override
189    protected void initializeModulePage(ModifiablePage modulePage)
190    {
191        modulePage.untag("SECTION");
192        
193        ModifiableZone defaultZone;
194        if (modulePage.hasZone("default"))
195        {
196            defaultZone = modulePage.getZone("default");
197        }
198        else
199        {
200            defaultZone = modulePage.createZone("default");            
201        }
202        
203        boolean hasService = defaultZone.getZoneItems().stream().anyMatch(zi -> NEWS_SERVICE_ID.equals(zi.getServiceId()));
204        
205        if (!hasService)
206        {
207            ModifiableZoneItem defaultZoneItem = defaultZone.addZoneItem();
208            defaultZoneItem.setType(ZoneType.SERVICE);
209            defaultZoneItem.setServiceId(NEWS_SERVICE_ID);
210            
211            ModifiableModelAwareDataHolder serviceDataHolder = defaultZoneItem.getServiceParameters();
212            serviceDataHolder.setValue("header", null);
213            serviceDataHolder.setValue("contentTypes", SEARCH_SERVICE_CONTENT_TYPES);
214            serviceDataHolder.setValue("returnables", SEARCH_SERVICE_RETURNABLES);
215            serviceDataHolder.setValue("initialSorts", SEARCH_SERVICE_SORTS);
216            serviceDataHolder.setValue("contexts", SEARCH_SERVICE_CONTEXTS);
217            serviceDataHolder.setValue("resultsPerPage", 12);
218            serviceDataHolder.setValue("rightCheckingMode", "none");
219            serviceDataHolder.setValue("resultPlace", "ABOVE_CRITERIA");
220            serviceDataHolder.setValue("launchSearchAtStartup", true);
221            serviceDataHolder.setValue("rss", false);
222            serviceDataHolder.setValue("contentView", SEARCH_SEARCH_CONTENT_VIEW);
223            serviceDataHolder.setValue("xslt", SEARCH_SERVICE_XSLT);
224        }
225    }
226    
227    @Override
228    protected String getModulePageTemplate() 
229    {
230        return "page";
231    }
232    
233    @Override
234    public Map<String, Object> _getInternalStatistics(Project project, boolean isActive)
235    { 
236        if (isActive)
237        {
238            Map<String, Object> statistics = new HashMap<>();
239            try
240            {
241                AmetysObjectIterable<Content> results = _contentSearcherFactory.create(WorkspacesConstants.PROJECT_NEWS_CONTENT_TYPE_ID)
242                        .search(new SiteQuery(project.getName()));
243                statistics.put(__NEWS_NUMBER_HEADER_ID, results.getSize());
244            }
245            catch (Exception e)
246            {
247                getLogger().error("Error searching news content images in project " + project.getId(), e);
248            }
249            return statistics;
250        }
251        else
252        {
253            return Map.of(__NEWS_NUMBER_HEADER_ID, __SIZE_INACTIVE);
254        }
255    }
256    
257    @Override
258    public List<StatisticColumn> _getInternalStatisticModel()
259    {
260        return List.of(new StatisticColumn(__NEWS_NUMBER_HEADER_ID, new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_NEWS_NUMBER"))
261                .withRenderer("Ametys.plugins.workspaces.project.tool.ProjectsGridHelper.renderElements")
262                .withType(StatisticsColumnType.LONG)
263                .withGroup(GROUP_HEADER_ELEMENTS_ID));
264    }
265
266    @Override
267    protected long _getModuleSize(Project project)
268    {
269        try
270        {
271            AmetysObjectIterable<Content> results = _contentSearcherFactory.create(WorkspacesConstants.PROJECT_NEWS_CONTENT_TYPE_ID)
272                    .search(new SiteQuery(project.getName()));
273
274            return results.stream()
275                .map(content -> content.getValue("illustration/image"))
276                .filter(Objects::nonNull)
277                .filter(Binary.class::isInstance)
278                .map(Binary.class::cast)
279                .mapToLong(Binary::getLength)
280                .sum();
281        }
282        catch (Exception e)
283        {
284            getLogger().error("Error searching news images in project " + project.getId(), e);
285            return -1;
286        }
287    }
288
289    @Override
290    protected boolean _showModuleSize()
291    {
292        return true;
293    }
294
295    @Override
296    protected boolean _showActivatedStatus()
297    {
298        return false;
299    }
300}