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}