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}