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