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