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; 027import org.apache.commons.lang.IllegalClassException; 028 029import org.ametys.cms.data.Binary; 030import org.ametys.cms.repository.Content; 031import org.ametys.cms.repository.ContentQueryHelper; 032import org.ametys.cms.repository.ContentTypeExpression; 033import org.ametys.cms.search.content.ContentSearcherFactory; 034import org.ametys.plugins.explorer.ExplorerNode; 035import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 036import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory; 037import org.ametys.plugins.repository.AmetysObject; 038import org.ametys.plugins.repository.AmetysObjectIterable; 039import org.ametys.plugins.repository.AmetysRepositoryException; 040import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder; 041import org.ametys.plugins.repository.query.expression.AndExpression; 042import org.ametys.plugins.repository.query.expression.Expression; 043import org.ametys.plugins.repository.query.expression.Expression.Operator; 044import org.ametys.plugins.repository.query.expression.StringExpression; 045import org.ametys.plugins.workspaces.AbstractWorkspaceModule; 046import org.ametys.plugins.workspaces.ObservationConstants; 047import org.ametys.plugins.workspaces.WorkspacesConstants; 048import org.ametys.plugins.workspaces.project.objects.Project; 049import org.ametys.plugins.workspaces.util.StatisticColumn; 050import org.ametys.plugins.workspaces.util.StatisticsColumnType; 051import org.ametys.runtime.i18n.I18nizableText; 052import org.ametys.web.repository.content.WebContentDAO; 053import org.ametys.web.repository.page.ModifiablePage; 054import org.ametys.web.repository.page.ModifiableZone; 055import org.ametys.web.repository.page.ModifiableZoneItem; 056import org.ametys.web.repository.page.Page; 057import org.ametys.web.repository.page.ZoneDAO; 058import org.ametys.web.repository.page.ZoneItem; 059import org.ametys.web.repository.page.ZoneItem.ZoneType; 060import org.ametys.web.repository.sitemap.Sitemap; 061import org.ametys.web.search.query.SiteQuery; 062 063import com.google.common.collect.ImmutableSet; 064 065/** 066 * Workspace module for wall content 067 */ 068public class WallContentModule extends AbstractWorkspaceModule 069{ 070 /** The id of calendar module */ 071 public static final String WALLCONTENT_MODULE_ID = WallContentModule.class.getName(); 072 073 /** The id of wall content service */ 074 public static final String WALLCONTENT_SERVICE_ID = "org.ametys.web.service.SearchService"; 075 076 /** Search service content types */ 077 protected static final String[] SEARCH_SERVICE_CONTENT_TYPES = new String[] {WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID}; 078 079 /** Search service returnables */ 080 protected static final String[] SEARCH_SERVICE_RETURNABLES = new String[] {"org.ametys.web.frontoffice.search.metamodel.impl.ContentReturnable"}; 081 082 /** Search service sorts */ 083 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\"}"}; 084 085 /** Search service contexts */ 086 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\":[]}"}; 087 088 /** Search service xslt */ 089 protected static final String SEARCH_SERVICE_XSLT = "pages/services/search/wall-content.xsl"; 090 091 /** Workspaces wallcontent node name */ 092 private static final String __WORKSPACES_WALLCONTENT_NODE_NAME = "wallcontent"; 093 094 private static final String __WALLCONTENT_NUMBER_HEADER_ID = __WORKSPACES_WALLCONTENT_NODE_NAME + "$wall_content_number"; 095 096 /** The zone DAO */ 097 protected ZoneDAO _zoneDAO; 098 /** The content DAO */ 099 protected WebContentDAO _contentDAO; 100 /** The content searcher factory */ 101 protected ContentSearcherFactory _contentSearcherFactory; 102 103 @Override 104 public void service(ServiceManager manager) throws ServiceException 105 { 106 super.service(manager); 107 _zoneDAO = (ZoneDAO) manager.lookup(ZoneDAO.ROLE); 108 _contentDAO = (WebContentDAO) manager.lookup(WebContentDAO.ROLE); 109 _contentSearcherFactory = (ContentSearcherFactory) manager.lookup(ContentSearcherFactory.ROLE); 110 } 111 112 public String getId() 113 { 114 return WALLCONTENT_MODULE_ID; 115 } 116 117 public String getModuleName() 118 { 119 return __WORKSPACES_WALLCONTENT_NODE_NAME; 120 } 121 122 public int getOrder() 123 { 124 return ORDER_WALLCONTENT; 125 } 126 127 public Set<String> getAllowedEventTypes() 128 { 129 return ImmutableSet.of(ObservationConstants.EVENT_WALLCONTENT_ADDED, org.ametys.cms.ObservationConstants.EVENT_CONTENT_COMMENT_VALIDATED); 130 } 131 132 public ModifiableResourceCollection getModuleRoot(Project project, boolean create) 133 { 134 // No root for this module, but root is still needed to handle rights on it 135 try 136 { 137 ExplorerNode projectRootNode = project.getExplorerRootNode(); 138 139 if (projectRootNode instanceof ModifiableResourceCollection) 140 { 141 ModifiableResourceCollection projectRootNodeRc = (ModifiableResourceCollection) projectRootNode; 142 return _getAmetysObject(projectRootNodeRc, __WORKSPACES_WALLCONTENT_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create); 143 } 144 else 145 { 146 throw new IllegalClassException(ModifiableResourceCollection.class, projectRootNode.getClass()); 147 } 148 } 149 catch (AmetysRepositoryException e) 150 { 151 throw new AmetysRepositoryException("Error getting the wallcontent root node.", e); 152 } 153 } 154 155 @Override 156 protected String getModulePageName() 157 { 158 return "index"; 159 } 160 161 public I18nizableText getModuleTitle() 162 { 163 return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_WALLCONTENT_LABEL"); 164 } 165 public I18nizableText getModuleDescription() 166 { 167 return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_WALLCONTENT_DESCRIPTION"); 168 } 169 @Override 170 protected I18nizableText getModulePageTitle() 171 { 172 // The module page is the index page, which already exists 173 return null; 174 } 175 176 @Override 177 protected void initializeModulePage(ModifiablePage modulePage) 178 { 179 ModifiableZone defaultZone; 180 if (modulePage.hasZone("default")) 181 { 182 defaultZone = modulePage.getZone("default"); 183 } 184 else 185 { 186 defaultZone = modulePage.createZone("default"); 187 } 188 189 boolean hasService = defaultZone.getZoneItems().stream().anyMatch(zi -> WALLCONTENT_SERVICE_ID.equals(zi.getServiceId())); 190 191 if (!hasService) 192 { 193 ModifiableZoneItem defaultZoneItem = defaultZone.addZoneItem(); 194 defaultZoneItem.setType(ZoneType.SERVICE); 195 defaultZoneItem.setServiceId(WALLCONTENT_SERVICE_ID); 196 197 ModifiableModelAwareDataHolder serviceDataHolder = defaultZoneItem.getServiceParameters(); 198 serviceDataHolder.setValue("header", null); 199 serviceDataHolder.setValue("contentTypes", SEARCH_SERVICE_CONTENT_TYPES); 200 serviceDataHolder.setValue("returnables", SEARCH_SERVICE_RETURNABLES); 201 serviceDataHolder.setValue("initialSorts", SEARCH_SERVICE_SORTS); 202 serviceDataHolder.setValue("contexts", SEARCH_SERVICE_CONTEXTS); 203 serviceDataHolder.setValue("resultsPerPage", 15); 204 serviceDataHolder.setValue("rightCheckingMode", "none"); 205 serviceDataHolder.setValue("resultPlace", "ABOVE_CRITERIA"); 206 serviceDataHolder.setValue("launchSearchAtStartup", true); 207 serviceDataHolder.setValue("rss", false); 208 serviceDataHolder.setValue("contentView", "main"); 209 serviceDataHolder.setValue("xslt", SEARCH_SERVICE_XSLT); 210 } 211 } 212 213 /* 214 * Override because the page should already exist, the parent returns null in this case 215 */ 216 @Override 217 protected ModifiablePage _createModulePage(Project project, Sitemap sitemap, String name, I18nizableText pageTitle, String skinTemplate) 218 { 219 if (sitemap.hasChild(name)) 220 { 221 return sitemap.getChild(name); 222 } 223 else 224 { 225 return super._createModulePage(project, sitemap, name, pageTitle, skinTemplate); 226 } 227 } 228 229 @Override 230 protected void _deletePages(Project project) 231 { 232 // Nothing. Index page should not be deleted. 233 } 234 235 @Override 236 protected void _internalDeactivateModule(Project project) 237 { 238 // Remove wall service 239 _removeWallService(project); 240 } 241 242 @Override 243 protected void _internalDeleteData(Project project) 244 { 245 // Remove wall service 246 _removeWallService(project); 247 248 // Delete wall contents 249 _deleteWallContents(project); 250 } 251 252 private void _deleteWallContents(Project project) 253 { 254 Expression cTypeExpr = new ContentTypeExpression(Operator.EQ, WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID); 255 256 Expression siteExpr = new StringExpression("site", Operator.EQ, project.getSite().getName()); 257 258 Expression expr = new AndExpression(cTypeExpr, siteExpr); 259 260 String xPathQuery = ContentQueryHelper.getContentXPathQuery(expr); 261 262 List<String> contentIds = _resolver.query(xPathQuery) 263 .stream() 264 .map(AmetysObject::getId) 265 .collect(Collectors.toList()); 266 267 _contentDAO.deleteContents(contentIds, true); 268 } 269 270 private void _removeWallService(Project project) 271 { 272 List<Page> modulePages = _getModulePages(project); 273 for (Page page : modulePages) 274 { 275 if (page.hasZone("default")) 276 { 277 _projectManager.untagProjectPage((ModifiablePage) page, getModuleRoot(project, false)); 278 279 ModifiableZone defaultZone = ((ModifiablePage) page).getZone("default"); 280 281 Set<String> zoneItemIds = defaultZone.getZoneItems() 282 .stream() 283 .filter(zi -> WALLCONTENT_SERVICE_ID.equals(zi.getServiceId())) 284 .map(ZoneItem::getId) 285 .collect(Collectors.toSet()); 286 287 for (String zoneItemId : zoneItemIds) 288 { 289 _zoneDAO.removeZoneItem(zoneItemId); 290 } 291 } 292 } 293 } 294 295 @Override 296 public Map<String, Object> _getInternalStatistics(Project project, boolean isActive) 297 { 298 if (isActive) 299 { 300 Map<String, Object> statistics = new HashMap<>(); 301 try 302 { 303 AmetysObjectIterable<Content> results = _contentSearcherFactory.create(WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID) 304 .search(new SiteQuery(project.getName())); 305 statistics.put(__WALLCONTENT_NUMBER_HEADER_ID, results.getSize()); 306 } 307 catch (Exception e) 308 { 309 getLogger().error("Error searching wall content in project " + project.getId(), e); 310 } 311 return statistics; 312 } 313 else 314 { 315 return Map.of(__WALLCONTENT_NUMBER_HEADER_ID, __SIZE_INACTIVE); 316 } 317 } 318 319 @Override 320 public List<StatisticColumn> _getInternalStatisticModel() 321 { 322 return List.of(new StatisticColumn(__WALLCONTENT_NUMBER_HEADER_ID, new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_WALL_CONTENT_NUMBER")) 323 .withRenderer("Ametys.plugins.workspaces.project.tool.ProjectsGridHelper.renderElements") 324 .withType(StatisticsColumnType.LONG) 325 .withGroup(GROUP_HEADER_ELEMENTS_ID)); 326 } 327 328 @Override 329 protected long _getModuleSize(Project project) 330 { 331 try 332 { 333 AmetysObjectIterable<Content> resultsIterable = _contentSearcherFactory.create(WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID) 334 .search(new SiteQuery(project.getName())); 335 336 return resultsIterable.stream() 337 .map(content -> content.getValue("illustration/image")) 338 .filter(Objects::nonNull) 339 .filter(Binary.class::isInstance) 340 .map(Binary.class::cast) 341 .mapToLong(Binary::getLength) 342 .sum(); 343 } 344 catch (Exception e) 345 { 346 getLogger().error("Error searching wall content images in project " + project.getId(), e); 347 return -1; 348 } 349 } 350 351 @Override 352 protected boolean _showModuleSize() 353 { 354 return true; 355 } 356}