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}