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