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