001/* 002 * Copyright 2019 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.userdirectory; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Map; 022import java.util.Set; 023import java.util.stream.Collectors; 024 025import org.apache.avalon.framework.component.Component; 026import org.apache.avalon.framework.configuration.Configurable; 027import org.apache.avalon.framework.configuration.Configuration; 028import org.apache.avalon.framework.configuration.ConfigurationException; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.avalon.framework.service.Serviceable; 032import org.slf4j.Logger; 033 034import org.ametys.cms.ObservationConstants; 035import org.ametys.cms.clientsideelement.content.SmartContentClientSideElementHelper; 036import org.ametys.cms.indexing.solr.SolrIndexHelper; 037import org.ametys.cms.repository.Content; 038import org.ametys.core.observation.Event; 039import org.ametys.core.observation.ObservationManager; 040import org.ametys.core.user.CurrentUserProvider; 041import org.ametys.plugins.repository.AmetysObjectResolver; 042import org.ametys.plugins.repository.ModifiableAmetysObject; 043import org.ametys.plugins.repository.RemovableAmetysObject; 044import org.ametys.plugins.repository.lock.LockableAmetysObject; 045 046/** 047 * Delete UD content component 048 */ 049public abstract class AbstractDeleteUDContentComponent implements Component, Serviceable, Configurable 050{ 051 private static final int _REMOVE_REFERENCE_DEFAULT_ACTION_ID = 200; 052 053 /** The Ametys object resolver */ 054 protected AmetysObjectResolver _resolver; 055 056 /** The observation manager */ 057 protected ObservationManager _observationManager; 058 059 /** The current user provider */ 060 protected CurrentUserProvider _currentUserProvider; 061 062 /** Helper for smart content client elements */ 063 protected SmartContentClientSideElementHelper _smartHelper; 064 065 /** The action id to call when references are removed */ 066 protected int _removeReferenceActionId; 067 068 private SolrIndexHelper _solrIndexHelper; 069 070 @Override 071 public void service(ServiceManager smanager) throws ServiceException 072 { 073 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 074 _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE); 075 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 076 _smartHelper = (SmartContentClientSideElementHelper) smanager.lookup(SmartContentClientSideElementHelper.ROLE); 077 _solrIndexHelper = (SolrIndexHelper) smanager.lookup(SolrIndexHelper.ROLE); 078 } 079 080 @Override 081 public void configure(Configuration configuration) throws ConfigurationException 082 { 083 Configuration conf = configuration.getChild("removeReferenceActionId"); 084 _removeReferenceActionId = conf.getValueAsInteger(_REMOVE_REFERENCE_DEFAULT_ACTION_ID); 085 } 086 087 /** 088 * Delete contents and logs results 089 * @param contentsToRemove the list of contents to remove 090 * @param parameters the additional parameters 091 * @param rights the map of rights id with its prefix 092 * @param logger The logger 093 * @return the number of deleted contents 094 */ 095 @SuppressWarnings("unchecked") 096 public int deleteContentsWithLog(List<Content> contentsToRemove, Map<String, Object> parameters, Map<String, String> rights, Logger logger) 097 { 098 int nbDeletedContents = 0; 099 100 List<String> contentIds = contentsToRemove.stream() 101 .map(Content::getId) 102 .collect(Collectors.toList()); 103 104 logger.info("Trying to delete contents. This can take a while..."); 105 Map<String, Object> deleteResults = deleteContents(contentIds, parameters, rights, logger); 106 logger.info("Contents deleting process ended."); 107 108 for (String contentId : contentIds) 109 { 110 Map<String, Object> result = (Map<String, Object>) deleteResults.get(contentId); 111 if (result != null) // if the result is null, the content was already deleted because it's a child of a previous deleted content 112 { 113 List<String> deletedContents = (List<String>) result.get("deleted-contents"); 114 nbDeletedContents += deletedContents.size(); 115 116 List<Content> referencedContents = (List<Content>) result.get("referenced-contents"); 117 if (referencedContents.size() > 0) 118 { 119 logger.info("The following contents cannot be deleted because they are referenced: {}", referencedContents.stream().map(c -> c.getId()).collect(Collectors.toList())); 120 } 121 122 List<Content> lockedContents = (List<Content>) result.get("locked-contents"); 123 if (lockedContents.size() > 0) 124 { 125 logger.info("The following contents cannot be deleted because they are locked: {}", lockedContents.stream().map(c -> c.getId()).collect(Collectors.toList())); 126 } 127 128 List<Content> undeletedContents = (List<Content>) result.get("undeleted-contents"); 129 if (undeletedContents.size() > 0) 130 { 131 logger.info("{} contents were not deleted. See previous logs for more information.", undeletedContents.size()); 132 } 133 } 134 } 135 136 return nbDeletedContents; 137 } 138 139 /** 140 * Delete contents 141 * @param contentsId The ids of contents to delete 142 * @param parameters the additional parameters 143 * @param rights the map of rights id with its prefix 144 * @param logger The logger 145 * @return the deleted and undeleted contents 146 */ 147 public Map<String, Object> deleteContents(List<String> contentsId, Map<String, Object> parameters, Map<String, String> rights, Logger logger) 148 { 149 Map<String, Object> results = new HashMap<>(); 150 151 List<String> alreadyDeletedContentIds = new ArrayList<>(); 152 for (String contentId : contentsId) 153 { 154 if (!alreadyDeletedContentIds.contains(contentId)) 155 { 156 Content content = _resolver.resolveById(contentId); 157 158 Map<String, Object> result = new HashMap<>(); 159 result.put("deleted-contents", new ArrayList<String>()); 160 result.put("undeleted-contents", new ArrayList<Content>()); 161 result.put("referenced-contents", new ArrayList<Content>()); 162 result.put("unauthorized-contents", new ArrayList<Content>()); 163 result.put("locked-contents", new ArrayList<Content>()); 164 result.put("initial-content", content.getId()); 165 results.put(contentId, result); 166 167 boolean referenced = isContentReferenced(content, logger); 168 if (referenced || !_checkBeforeDeletion(content, rights, result, logger)) 169 { 170 if (referenced) 171 { 172 // Indicate that the content is referenced. 173 @SuppressWarnings("unchecked") 174 List<Content> referencedContents = (List<Content>) result.get("referenced-contents"); 175 referencedContents.add(content); 176 } 177 result.put("check-before-deletion-failed", true); 178 } 179 else 180 { 181 // Process deletion 182 _deleteContent(content, parameters, rights, result, logger); 183 184 @SuppressWarnings("unchecked") 185 List<String> deletedContents = (List<String>) result.get("deleted-contents"); 186 if (deletedContents != null) 187 { 188 alreadyDeletedContentIds.addAll(deletedContents); 189 190 } 191 } 192 } 193 else 194 { 195 logger.info("Content with id '{}' has been already deleted during its parent deletion", contentId); 196 } 197 } 198 199 return results; 200 } 201 202 /** 203 * Delete one content 204 * @param content the content to delete 205 * @param parameters the additional parameters 206 * @param rights the map of rights id with its prefix 207 * @param results the results map 208 * @param logger The logger 209 */ 210 protected void _deleteContent(Content content, Map<String, Object> parameters, Map<String, String> rights, Map<String, Object> results, Logger logger) 211 { 212 // 1 - First remove relations 213 boolean success = _removeRelations(content, parameters, logger); 214 215 // 2 - If succeed, process to deletion 216 if (success) 217 { 218 _processContentDeletion(content, parameters, rights, results, logger); 219 } 220 else 221 { 222 @SuppressWarnings("unchecked") 223 List<Content> undeletedContents = (List<Content>) results.get("undeleted-contents"); 224 undeletedContents.add(content); 225 226 logger.warn("Can not delete content {} ('{}') : at least one relation to contents could not be removed", content.getTitle(), content.getId()); 227 } 228 } 229 230 /** 231 * Delete one content 232 * @param content the content to delete 233 * @param parameters the additional parameters 234 * @param rights the map of rights id with its prefix 235 * @param results the results map 236 * @param logger The logger 237 */ 238 @SuppressWarnings("unchecked") 239 protected void _processContentDeletion(Content content, Map<String, Object> parameters, Map<String, String> rights, Map<String, Object> results, Logger logger) 240 { 241 Set<String> toDelete = _getContentIdsToDelete(content, parameters, rights, results, logger); 242 243 List<Content> referencedContents = (List<Content>) results.get("referenced-contents"); 244 List<Content> lockedContents = (List<Content>) results.get("locked-contents"); 245 List<Content> unauthorizedContents = (List<Content>) results.get("unauthorized-contents"); 246 247 if (referencedContents.size() == 0 && lockedContents.size() == 0 && unauthorizedContents.size() == 0) 248 { 249 _finalizeDeleteContents(toDelete, content.getParent(), results, logger); 250 } 251 } 252 253 /** 254 * Finalize the deletion of contents. Call observers and remove contents 255 * @param contentIdsToDelete the list of content id to delete 256 * @param parent the jcr parent for saving changes 257 * @param results the results map 258 * @param logger The logger 259 */ 260 protected void _finalizeDeleteContents(Set<String> contentIdsToDelete, ModifiableAmetysObject parent, Map<String, Object> results, Logger logger) 261 { 262 @SuppressWarnings("unchecked") 263 List<Content> unauthorizedContents = (List<Content>) results.get("unauthorized-contents"); 264 @SuppressWarnings("unchecked") 265 List<Content> lockedContents = (List<Content>) results.get("locked-contents"); 266 267 if (!unauthorizedContents.isEmpty() || !lockedContents.isEmpty()) 268 { 269 //Do Nothing 270 return; 271 } 272 273 try 274 { 275 _solrIndexHelper.pauseSolrCommitForEvents(new String[] {ObservationConstants.EVENT_CONTENT_DELETED}); 276 277 Map<String, Map<String, Object>> eventParams = new HashMap<>(); 278 for (String id : contentIdsToDelete) 279 { 280 Content content = _resolver.resolveById(id); 281 Map<String, Object> eventParam = _getEventParametersForDeletion(content); 282 eventParams.put(id, eventParam); 283 284 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), eventParam)); 285 } 286 287 for (String id : contentIdsToDelete) 288 { 289 Content content = _resolver.resolveById(id); 290 291 // Remove the content. 292 LockableAmetysObject lockedContent = (LockableAmetysObject) content; 293 if (lockedContent.isLocked()) 294 { 295 lockedContent.unlock(); 296 } 297 298 ((RemovableAmetysObject) content).remove(); 299 } 300 301 parent.saveChanges(); 302 303 for (String id : contentIdsToDelete) 304 { 305 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), eventParams.get(id))); 306 307 @SuppressWarnings("unchecked") 308 List<String> deletedContents = (List<String>) results.get("deleted-contents"); 309 deletedContents.add(id); 310 } 311 } 312 finally 313 { 314 _solrIndexHelper.restartSolrCommitForEvents(new String[] {ObservationConstants.EVENT_CONTENT_DELETED}); 315 } 316 } 317 318 /** 319 * True if we can delete the content (check if removable, rights and if locked) 320 * @param content the content 321 * @param rights the map of rights id with its prefix 322 * @param results the results map 323 * @return true if we can delete the content 324 */ 325 protected boolean _canDeleteContent(Content content, Map<String, String> rights, Map<String, Object> results) 326 { 327 if (!(content instanceof RemovableAmetysObject)) 328 { 329 throw new IllegalArgumentException("The content [" + content.getId() + "] is not a RemovableAmetysObject, it can't be deleted."); 330 } 331 332 if (!_hasRight(content, rights)) 333 { 334 // User has no sufficient right 335 @SuppressWarnings("unchecked") 336 List<Content> norightContents = (List<Content>) results.get("unauthorized-contents"); 337 norightContents.add(content); 338 339 return false; 340 } 341 else if (_isLocked(content)) 342 { 343 @SuppressWarnings("unchecked") 344 List<Content> lockedContents = (List<Content>) results.get("locked-contents"); 345 lockedContents.add(content); 346 347 return false; 348 } 349 350 return true; 351 } 352 353 /** 354 * Get parameters for content deleted {@link Event} 355 * @param content the removed content 356 * @return the event's parameters 357 */ 358 protected Map<String, Object> _getEventParametersForDeletion (Content content) 359 { 360 Map<String, Object> eventParams = new HashMap<>(); 361 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 362 eventParams.put(ObservationConstants.ARGS_CONTENT_NAME, content.getName()); 363 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 364 return eventParams; 365 } 366 367 /** 368 * Determines if the content is locked 369 * @param content the content 370 * @return true if the content is locked 371 */ 372 protected boolean _isLocked(Content content) 373 { 374 return _smartHelper.isLocked(content); 375 } 376 377 /** 378 * Determines if the user has sufficient right for the given content 379 * @param content the content 380 * @param rights the map of rights id with its prefix 381 * @return true if user has sufficient right 382 */ 383 protected boolean _hasRight(Content content, Map<String, String> rights) 384 { 385 if (rights.isEmpty()) 386 { 387 return true; 388 } 389 390 return _smartHelper.hasRight(rights, content); 391 } 392 393 /** 394 * True if the content is referenced 395 * @param content the content 396 * @param logger The logger 397 * @return true if the content is referenced 398 */ 399 public abstract boolean isContentReferenced(Content content, Logger logger); 400 401 /** 402 * Check that deletion can be performed without blocking errors 403 * @param content The initial content to delete 404 * @param rights the map of rights id with its prefix 405 * @param results The results 406 * @param logger The logger 407 * @return true if the deletion can be performed 408 */ 409 protected abstract boolean _checkBeforeDeletion(Content content, Map<String, String> rights, Map<String, Object> results, Logger logger); 410 411 /** 412 * Remove relations 413 * @param content the content 414 * @param parameters the additional parameters 415 * @param logger The logger 416 * @return <code>true</code> if all relations have been removed 417 */ 418 protected abstract boolean _removeRelations(Content content, Map<String, Object> parameters, Logger logger); 419 420 /** 421 * Get the id of children to be deleted. 422 * All children shared with other contents which are not part of deletion, will be not deleted. 423 * @param content The content to delete 424 * @param parameters the additional parameters 425 * @param rights the map of rights id with its prefix 426 * @param results The results 427 * @param logger The logger 428 * @return The id of contents to be deleted 429 */ 430 protected abstract Set<String> _getContentIdsToDelete (Content content, Map<String, Object> parameters, Map<String, String> rights, Map<String, Object> results, Logger logger); 431 432}