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}