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}