001/*
002 *  Copyright 2015 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.cms.repository;
017
018import java.util.ArrayList;
019import java.util.Date;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.concurrent.ExecutionException;
026import java.util.concurrent.Future;
027
028import javax.jcr.Node;
029import javax.jcr.RepositoryException;
030import javax.jcr.Session;
031
032import org.apache.avalon.framework.component.Component;
033import org.apache.avalon.framework.context.Context;
034import org.apache.avalon.framework.context.ContextException;
035import org.apache.avalon.framework.context.Contextualizable;
036import org.apache.avalon.framework.logger.AbstractLogEnabled;
037import org.apache.avalon.framework.service.ServiceException;
038import org.apache.avalon.framework.service.ServiceManager;
039import org.apache.avalon.framework.service.Serviceable;
040import org.apache.cocoon.components.ContextHelper;
041import org.apache.cocoon.environment.Request;
042import org.apache.commons.lang3.StringUtils;
043
044import org.ametys.cms.ObservationConstants;
045import org.ametys.cms.content.ContentHelper;
046import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper;
047import org.ametys.cms.contenttype.ContentType;
048import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
049import org.ametys.cms.contenttype.ContentTypesHelper;
050import org.ametys.cms.contenttype.MetadataSet;
051import org.ametys.cms.lock.LockContentManager;
052import org.ametys.cms.tag.Tag;
053import org.ametys.cms.tag.TagProviderExtensionPoint;
054import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
055import org.ametys.cms.workflow.ContentWorkflowHelper;
056import org.ametys.core.observation.Event;
057import org.ametys.core.observation.ObservationManager;
058import org.ametys.core.right.RightManager;
059import org.ametys.core.right.RightManager.RightResult;
060import org.ametys.core.ui.Callable;
061import org.ametys.core.user.CurrentUserProvider;
062import org.ametys.core.user.UserIdentity;
063import org.ametys.core.user.UserManager;
064import org.ametys.plugins.core.user.UserHelper;
065import org.ametys.plugins.explorer.ExplorerNode;
066import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
067import org.ametys.plugins.explorer.resources.Resource;
068import org.ametys.plugins.repository.AmetysObject;
069import org.ametys.plugins.repository.AmetysObjectResolver;
070import org.ametys.plugins.repository.AmetysRepositoryException;
071import org.ametys.plugins.repository.ModifiableAmetysObject;
072import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
073import org.ametys.plugins.repository.RemovableAmetysObject;
074import org.ametys.plugins.repository.TraversableAmetysObject;
075import org.ametys.plugins.repository.UnknownAmetysObjectException;
076import org.ametys.plugins.repository.lock.LockAwareAmetysObject;
077import org.ametys.plugins.repository.lock.LockHelper;
078import org.ametys.plugins.repository.lock.LockableAmetysObject;
079import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
080import org.ametys.plugins.repository.version.VersionableAmetysObject;
081import org.ametys.plugins.workflow.store.AbstractJackrabbitWorkflowStore;
082import org.ametys.plugins.workflow.store.AmetysObjectWorkflowStore;
083import org.ametys.plugins.workflow.support.WorkflowProvider;
084import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
085import org.ametys.runtime.parameter.ParameterHelper;
086
087import com.opensymphony.workflow.WorkflowException;
088import com.opensymphony.workflow.spi.Step;
089
090/**
091 * DAO for manipulating contents
092 *
093 */
094public class ContentDAO extends AbstractLogEnabled implements Serviceable, Component, Contextualizable
095{
096    /** Avalon Role */
097    public static final String ROLE = ContentDAO.class.getName();
098    
099    /** Ametys resolver */
100    protected AmetysObjectResolver _resolver;
101    /** Ametys observation manger */
102    protected ObservationManager _observationManager;
103    /** Component to get current user */
104    protected CurrentUserProvider _currentUserProvider;
105    /** Component to get tags */
106    protected TagProviderExtensionPoint _tagProvider;
107
108    /** Workflow component */
109    protected WorkflowProvider _workflowProvider;
110    /** Workflow helper component */
111    protected ContentWorkflowHelper _contentWorkflowHelper;
112    /** Component to manager lock */
113    protected LockContentManager _lockManager;
114    /** Content-type extension point */
115    protected ContentTypeExtensionPoint _contentTypeEP;
116    /** Content helper */
117    protected ContentHelper _contentHelper;
118    /** Content types helper */
119    protected ContentTypesHelper _cTypesHelper;
120    /** Rights manager */
121    protected RightManager _rightManager;
122    /** Cocoon context */
123    protected Context _context;
124    /** The user manager */
125    protected UserManager _usersManager;
126    /** Helper for users */
127    protected UserHelper _userHelper;
128    /** The helper component for hierarchical simple contents */
129    protected HierarchicalReferenceTablesHelper _hierarchicalSimpleContentsHelper;
130    /** The modifiable content helper */
131    protected ModifiableContentHelper _modifiableContentHelper;
132    
133    /** The mode for tag edition */
134    public enum TagMode 
135    {
136        /** Value will replace existing one */
137        REPLACE,
138        /** Value will be added to existing one */
139        INSERT,
140        /** Value will be removed from existing one */
141        REMOVE
142    }
143    
144    public void contextualize(Context context) throws ContextException
145    {
146        _context = context;
147    }
148    
149    @Override
150    public void service(ServiceManager smanager) throws ServiceException
151    {
152        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
153        _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE);
154        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
155        _usersManager = (UserManager) smanager.lookup(UserManager.ROLE);
156        _userHelper = (UserHelper) smanager.lookup(UserHelper.ROLE);
157        _tagProvider = (TagProviderExtensionPoint) smanager.lookup(TagProviderExtensionPoint.ROLE);
158        _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE);
159        _rightManager = (RightManager) smanager.lookup(RightManager.ROLE);
160        _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
161        _lockManager = (LockContentManager) smanager.lookup(LockContentManager.ROLE);
162        _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE);
163        _cTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
164        _contentHelper = (ContentHelper) smanager.lookup(ContentHelper.ROLE);
165        _modifiableContentHelper = (ModifiableContentHelper) smanager.lookup(ModifiableContentHelper.ROLE); 
166        _hierarchicalSimpleContentsHelper = (HierarchicalReferenceTablesHelper) smanager.lookup(HierarchicalReferenceTablesHelper.ROLE);
167    }
168    
169    /**
170     * Delete contents
171     * @param contentsId The ids of contents to delete
172     * @return the deleted and undeleted contents
173     */
174    @Callable
175    public Map<String, Object> deleteContents(List<String> contentsId)
176    {
177        return deleteContents(contentsId, false);
178    }
179    
180    /**
181     * Delete contents
182     * @param contentsId The ids of contents to delete
183     * @param ignoreRights true to ignore user rights
184     * @return the deleted and undeleted contents
185     */
186    public Map<String, Object> deleteContents(List<String> contentsId, boolean ignoreRights)
187    {
188        Map<String, Object> results = new HashMap<>();
189        
190        results.put("deleted-contents", new ArrayList<Map<String, Object>>());
191        results.put("undeleted-contents", new ArrayList<Map<String, Object>>());
192        results.put("referenced-contents", new ArrayList<Map<String, Object>>());
193        results.put("unauthorized-contents", new ArrayList<Map<String, Object>>());
194        results.put("locked-contents", new ArrayList<Map<String, Object>>());
195        
196        for (String contentId : contentsId)
197        {
198            Content content = _resolver.resolveById(contentId);
199            String contentName = content.getName();
200            String contentTitle = StringUtils.defaultString(_contentHelper.getTitle(content), contentName);
201            
202            Map<String, Object> contentParams = new HashMap<>();
203            contentParams.put("id", content.getId());
204            contentParams.put("title", contentTitle);
205            contentParams.put("name", contentName);
206            
207            if (!(content instanceof RemovableAmetysObject))
208            {
209                throw new IllegalArgumentException("The content [" + content.getId() + "] is not a RemovableAmetysObject, it can't be deleted.");
210            }
211            
212            try
213            {
214                if (!ignoreRights && !canDelete(content))
215                {
216                    // User has no sufficient right
217                    @SuppressWarnings("unchecked")
218                    List<Map<String, Object>> unauthorizedContents = (List<Map<String, Object>>) results.get("unauthorized-contents");
219                    unauthorizedContents.add(contentParams);
220                    continue;
221                }
222                
223                if (content instanceof LockableAmetysObject)
224                {
225                    // If the content is locked, try to unlock it.
226                    LockableAmetysObject lockableContent = (LockableAmetysObject) content;
227                    if (lockableContent.isLocked())
228                    {
229                        boolean canUnlockAll = _rightManager.hasRight(_currentUserProvider.getUser(), "CMS_Rights_UnlockAll", "/cms") == RightResult.RIGHT_ALLOW;
230                        if (LockHelper.isLockOwner(lockableContent, _currentUserProvider.getUser()) || canUnlockAll)
231                        {
232                            lockableContent.unlock();
233                        }
234                        else
235                        {
236                            @SuppressWarnings("unchecked")
237                            List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) results.get("locked-contents");
238                            lockedContents.add(contentParams);
239                            continue;
240                        }
241                    }
242                }
243                
244                if (_isContentReferenced(content))
245                {
246                    // Indicate that the content is referenced.
247                    @SuppressWarnings("unchecked")
248                    List<Map<String, Object>> referencedContents = (List<Map<String, Object>>) results.get("referenced-contents");
249                    referencedContents.add(contentParams);
250                }
251                else
252                {
253                    // All checks have been done, the content can be deleted
254                    Map<String, Object> eventParams = _getEventParametersForDeletion(content);
255                    
256                    _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), eventParams));
257                    
258                    RemovableAmetysObject removableContent = (RemovableAmetysObject) content;
259                    ModifiableAmetysObject parent = removableContent.getParent();
260                    
261                    // Remove the content.
262                    removableContent.remove();
263                    
264                    parent.saveChanges();
265                    
266                    _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), eventParams));
267                    
268                    @SuppressWarnings("unchecked")
269                    List<Map<String, Object>> deletedContents = (List<Map<String, Object>>) results.get("deleted-contents");
270                    deletedContents.add(contentParams);
271                }
272            }
273            catch (AmetysRepositoryException e)
274            {
275                getLogger().error("Unable to delete content '" + contentId + "'", e);
276                
277                @SuppressWarnings("unchecked")
278                List<Map<String, Object>> undeletedContents = (List<Map<String, Object>>) results.get("undeleted-contents");
279                undeletedContents.add(contentParams);
280            }
281        }
282        
283        return results;
284    }
285    
286    /**
287     * Test if content is still referenced before removing it
288     * @param content The content to remove
289     * @return true if content is still referenced
290     */
291    protected boolean _isContentReferenced (Content content)
292    {
293        return content.hasReferencingContents();
294    }
295    
296    /**
297     * Get parameters for content deleted {@link Event}
298     * @param content the removed content
299     * @return the event's parameters
300     */
301    protected Map<String, Object> _getEventParametersForDeletion (Content content)
302    {
303        Map<String, Object> eventParams = new HashMap<>();
304        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
305        eventParams.put(ObservationConstants.ARGS_CONTENT_NAME, content.getName());
306        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
307        return eventParams;
308    }
309    
310    /**
311     * Get the contents properties
312     * @param contentIds The ids of contents
313     * @param workspaceName The workspace name. Can be null to get contents in current workspace.
314     * @return The contents' properties
315     */
316    @Callable
317    public Map<String, Object> getContentsProperties (List<String> contentIds, String workspaceName)
318    {
319        Map<String, Object> result = new HashMap<>();
320        
321        List<Map<String, Object>> contents = new ArrayList<>();
322        List<String> contentsNotFound = new ArrayList<>();
323        
324        Request request = ContextHelper.getRequest(_context);
325        String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
326        try
327        {
328            if (StringUtils.isNotEmpty(workspaceName))
329            {
330                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
331            }
332            
333            for (String contentId : contentIds)
334            {
335                try
336                {
337                    Content content = _resolver.resolveById(contentId);
338                    contents.add(getContentProperties(content));
339                }
340                catch (UnknownAmetysObjectException e)
341                {
342                    contentsNotFound.add(contentId);
343                }
344            }
345            
346            result.put("contents", contents);
347            result.put("contentsNotFound", contentsNotFound);
348        }
349        finally
350        {
351            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace);
352        }
353        
354        return result;
355    }
356    
357    /**
358     * Get the content properties
359     * @param contentId The id of content
360     * @param workspaceName The workspace name. Can be null to get content in current workspace.
361     * @return The content's properties
362     */
363    @Callable
364    public Map<String, Object> getContentProperties (String contentId, String workspaceName)
365    {
366        Request request = ContextHelper.getRequest(_context);
367        String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
368        try
369        {
370            if (StringUtils.isNotEmpty(workspaceName))
371            {
372                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
373            }
374            
375            Content content = _resolver.resolveById(contentId);
376            return getContentProperties(content);
377        }
378        finally
379        {
380            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace);
381        }
382    }
383    
384    /**
385     * Get the content properties
386     * @param content The content
387     * @return The content properties
388     */
389    public Map<String, Object> getContentProperties (Content content)
390    {
391        Map<String, Object> infos = new HashMap<>();
392        
393        infos.put("id", content.getId());
394        infos.put("name", content.getName());
395        infos.put("title", _contentHelper.getTitle(content));
396        
397        if (content.getMetadataHolder().getType(DefaultContent.METADATA_TITLE) == org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType.MULTILINGUAL_STRING)
398        {
399            infos.put("titleVariants", _contentHelper.getTitleVariants(content));
400        }
401        infos.put("path", content.getPath());
402        infos.put("types", content.getTypes());
403        infos.put("mixins", content.getMixinTypes());
404        String lang = content.getLanguage();
405        if (lang != null)
406        {
407            infos.put("lang", lang);
408        }
409        infos.put("creator", _userHelper.user2json(content.getCreator()));
410        infos.put("lastContributor", _userHelper.user2json(content.getLastContributor()));
411        infos.put("creationDate", ParameterHelper.valueToString(content.getCreationDate()));
412        infos.put("lastModified", ParameterHelper.valueToString(content.getLastModified()));
413        infos.put("isSimple", _contentHelper.isSimple(content));
414        infos.put("isReferenceTable", _contentHelper.isReferenceTable(content));
415        infos.put("parent", _hierarchicalSimpleContentsHelper.getParent(content));
416        
417        if (content instanceof WorkflowAwareContent)
418        {
419            WorkflowAwareContent waContent = (WorkflowAwareContent) content;
420            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
421            
422            infos.put("workflowName", workflow.getWorkflowName(waContent.getWorkflowId()));
423            
424            List<Long> workflowSteps = new ArrayList<>();
425            
426            List<Step> currentSteps = workflow.getCurrentSteps(waContent.getWorkflowId());
427            for (Step step : currentSteps)
428            {
429                workflowSteps.add(step.getId());
430            }
431            infos.put("workflowStep", workflowSteps);
432            
433            int[] availableActions = _contentWorkflowHelper.getAvailableActions(waContent);
434            infos.put("availableActions", availableActions);
435        }
436        
437        if (content instanceof ModifiableContent)
438        {
439            infos.put("isModifiable", true);
440        }
441        
442        if (content instanceof LockAwareAmetysObject)
443        {
444            LockAwareAmetysObject lockableContent = (LockAwareAmetysObject) content;
445            if (lockableContent.isLocked())
446            {
447                infos.put("locked", true);
448                infos.put("lockOwner", lockableContent.getLockOwner());
449                infos.put("canUnlock", _lockManager.canUnlock(lockableContent));
450            }
451        }
452        
453        infos.put("rights", getUserRights(content));
454        
455        Map<String, Object> additionalData = new HashMap<>();
456        
457        String[] contenttypes = content.getTypes();
458        for (String cTypeId : contenttypes)
459        {
460            ContentType cType = _contentTypeEP.getExtension(cTypeId);
461            if (cType != null)
462            {
463                additionalData.putAll(cType.getAdditionalData(content));
464            }
465        }
466        
467        if (!additionalData.isEmpty())
468        {
469            infos.put("additionalData", additionalData); 
470        }
471        
472        return infos;
473    }
474    
475    /**
476     * Get the content's properties for description
477     * @param contentId The id of content
478     * @param workspaceName The workspace name. Can be null to get content in current workspace.
479     * @return The content's properties for description
480     */
481    @Callable
482    public Map<String, Object> getContentDescription (String contentId, String workspaceName)
483    {
484        Request request = ContextHelper.getRequest(_context);
485        String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
486        try
487        {
488            if (StringUtils.isNotEmpty(workspaceName))
489            {
490                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
491            }
492            
493            Content content = _resolver.resolveById(contentId);
494            return getContentDescription(content);
495        }
496        finally
497        {
498            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace);
499        }
500    }
501    
502    /**
503     *Get the content's properties for description
504     * @param content The content
505     * @return The content's properties for description
506     */
507    public Map<String, Object> getContentDescription (Content content)
508    {
509        Map<String, Object> infos = new HashMap<>();
510        
511        infos.put("id", content.getId());
512        infos.put("name", content.getName());
513        infos.put("title", _contentHelper.getTitle(content));
514        infos.put("types", content.getTypes());
515        infos.put("mixins", content.getMixinTypes());
516        infos.put("lang", content.getLanguage());
517        infos.put("creator", _userHelper.user2json(content.getCreator()));
518        infos.put("lastContributor", _userHelper.user2json(content.getLastContributor()));
519        infos.put("lastModified", ParameterHelper.valueToString(content.getLastModified()));
520        infos.put("iconGlyph", _cTypesHelper.getIconGlyph(content));
521        infos.put("iconDecorator", _cTypesHelper.getIconDecorator(content));
522        infos.put("smallIcon", _cTypesHelper.getSmallIcon(content));
523        infos.put("mediumIcon", _cTypesHelper.getMediumIcon(content));
524        infos.put("largeIcon", _cTypesHelper.getLargeIcon(content));
525        
526        return infos;
527    }
528    
529    /**
530     * Get the metadata sets of a content
531     * @param contentId the content's id
532     * @param edition Set to true to get edition metadata set. False otherwise.
533     * @param includeInternal Set to true to include internal metadata sets.
534     * @return the metadata sets
535     */
536    @Callable
537    public List<Map<String, Object>> getContentMetadataSets (String contentId, boolean edition, boolean includeInternal)
538    {
539        List<Map<String, Object>> metadataSets = new ArrayList<>();
540        
541        Content content = _resolver.resolveById(contentId);
542        String contentTypeId = _cTypesHelper.getContentTypeIdForRendering(content);
543        
544        ContentType cType = _contentTypeEP.getExtension(contentTypeId);
545        
546        Set<String> metadataSetNames = edition ? cType.getEditionMetadataSetNames(includeInternal) : cType.getViewMetadataSetNames(includeInternal);
547        for (String metadataSetName : metadataSetNames)
548        {
549            MetadataSet metadataSet = edition ? cType.getMetadataSetForEdition(metadataSetName) : cType.getMetadataSetForView(metadataSetName);
550            
551            Map<String, Object> viewInfos = new HashMap<>();
552            viewInfos.put("name", metadataSetName);
553            viewInfos.put("label", metadataSet.getLabel());
554            viewInfos.put("description", metadataSet.getDescription());
555            metadataSets.add(viewInfos);
556        }
557        
558        return metadataSets;
559    }
560    
561    /**
562     * Get the user rights on content
563     * @param content The content
564     * @return The user's rights
565     */
566    protected Set<String> getUserRights (Content content)
567    {
568        UserIdentity user = _currentUserProvider.getUser();
569        return _rightManager.getUserRights(user, content);
570    }
571    
572    /**
573     * Get the tags of contents
574     * @param contentIds The content's ids
575     * @return the tags
576     */
577    @Callable
578    public Set<String> getTags (List<String> contentIds)
579    {
580        Set<String> tags = new HashSet<>();
581        
582        for (String contentId : contentIds)
583        {
584            Content content = _resolver.resolveById(contentId);
585            tags.addAll(content.getTags());
586        }
587        
588        return tags;
589    }
590    
591    /**
592     * Tag a list of contents with the given tags
593     * @param contentIds The ids of contents to tag
594     * @param tagNames The tags
595     * @param contextualParameters The contextual parameters
596     * @return the result
597     */
598    @Callable
599    public Map<String, Object> tag (List<String> contentIds, List<String> tagNames, Map<String, Object> contextualParameters)
600    {
601        return tag(contentIds, tagNames, TagMode.REPLACE.toString(), contextualParameters);
602    }
603    
604    /**
605     * Tag a list of contents
606     * @param contentIds The ids of contents to tag
607     * @param tagNames The tags
608     * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags.
609     * @param contextualParameters The contextual parameters
610     * @return the result
611     */
612    @Callable
613    public Map<String, Object> tag (List<String> contentIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters)
614    {
615        Map<String, Object> result = new HashMap<>();
616        
617        result.put("notaggable-contents", new ArrayList<Map<String, Object>>());
618        result.put("invalid-tags", new ArrayList<String>());
619        result.put("allright-contents", new ArrayList<Map<String, Object>>());
620        result.put("locked-contents", new ArrayList<Map<String, Object>>());
621        
622        for (String contentId : contentIds)
623        {
624            Content content = _resolver.resolveById(contentId);
625            
626            Map<String, Object> content2json = new HashMap<>();
627            content2json.put("id", content.getId());
628            content2json.put("title", _contentHelper.getTitle(content));
629            
630            if (content instanceof TaggableAmetysObject)
631            {
632                TaggableAmetysObject mContent = (TaggableAmetysObject) content;
633                
634                boolean wasLocked = false;
635                
636                if (content instanceof LockableAmetysObject)
637                {
638                    LockableAmetysObject lockableContent = (LockableAmetysObject) content;
639                    UserIdentity user = _currentUserProvider.getUser();
640                    if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, user))
641                    {
642                        @SuppressWarnings("unchecked")
643                        List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) result.get("locked-contents");
644                        content2json.put("lockOwner", lockableContent.getLockOwner());
645                        lockedContents.add(content2json);
646                        
647                        // Stop process
648                        continue;
649                    }
650                    
651                    if (lockableContent.isLocked())
652                    {
653                        wasLocked = true;
654                        lockableContent.unlock();
655                    }
656                }
657                
658                TagMode tagMode = TagMode.valueOf(mode);
659                
660                Set<String> oldTags = mContent.getTags();
661                _removeAllTagsInReplaceMode(mContent, tagMode, oldTags);
662                
663                // Then set new tags
664                for (String tagName : tagNames)
665                {
666                    if (_isTagValid(tagName, contextualParameters))
667                    {
668                        if (TagMode.REMOVE.equals(tagMode))
669                        {
670                            mContent.untag(tagName);
671                        }
672                        else if (TagMode.REPLACE.equals(tagMode) || !oldTags.contains(tagName))
673                        {
674                            mContent.tag(tagName);
675                        }
676                        
677                    }
678                    else
679                    {
680                        @SuppressWarnings("unchecked")
681                        List<String> invalidTags = (List<String>) result.get("invalid-tags");
682                        invalidTags.add(tagName);
683                    }
684                }
685                
686                ((ModifiableAmetysObject) content).saveChanges();
687                
688                if (wasLocked)
689                {
690                    // Relock content if it was locked before tagging
691                    ((LockableAmetysObject) content).lock();
692                }
693                
694                content2json.put("tags", content.getTags());
695                @SuppressWarnings("unchecked")
696                List<Map<String, Object>> allRightPages = (List<Map<String, Object>>) result.get("allright-contents");
697                allRightPages.add(content2json);
698                
699                if (!oldTags.equals(content.getTags()))
700                {
701                    // Notify observers that the content has been tagged
702                    Map<String, Object> eventParams = new HashMap<>();
703                    eventParams.put(org.ametys.cms.ObservationConstants.ARGS_CONTENT, content);
704                    eventParams.put(org.ametys.cms.ObservationConstants.ARGS_CONTENT_ID, content.getId());
705                    eventParams.put("content.tags", content.getTags());
706                    eventParams.put("content.old.tags", oldTags);
707                    _observationManager.notify(new Event(org.ametys.cms.ObservationConstants.EVENT_CONTENT_TAGGED, _currentUserProvider.getUser(), eventParams));
708                }
709            }
710            else
711            {
712                @SuppressWarnings("unchecked")
713                List<Map<String, Object>> notaggableContents = (List<Map<String, Object>>) result.get("notaggable-contents");
714                notaggableContents.add(content2json);
715            }
716        }
717        
718        return result;
719    }
720
721    private void _removeAllTagsInReplaceMode(TaggableAmetysObject mContent, TagMode tagMode, Set<String> oldTags)
722    {
723        if (TagMode.REPLACE.equals(tagMode))
724        {
725            // First delete old tags
726            for (String tagName : oldTags)
727            {
728                mContent.untag(tagName);
729            }
730        }
731    }
732    
733    /**
734     * Is the tag a content tag
735     * @param tagName The tag name
736     * @param contextualParameters The contextual parameters
737     * @return true if the tag is a valid content tag
738     */
739    public boolean _isTagValid (String tagName, Map<String, Object> contextualParameters)
740    {
741        Tag tag = _tagProvider.getTag(tagName, contextualParameters);
742        return tag.getTarget().getName().equals("CONTENT");
743    }
744    
745    /**
746     * Copy a content.
747     * @param originalContent the original content.
748     * @param parent the object in which to create a content.
749     * @param name the content name.
750     * @param initWorkflowActionId The initial workflow action id
751     * @return the copied content.
752     * @throws AmetysRepositoryException If an error occured
753     */
754    public ModifiableContent copy(DefaultContent originalContent, ModifiableTraversableAmetysObject parent, String name, int initWorkflowActionId) throws AmetysRepositoryException
755    {
756        return copy(originalContent, parent, name, null, initWorkflowActionId);
757    }
758    
759    /**
760     * Copy a content.
761     * @param originalContent the original content.
762     * @param parent the object in which to create a content.
763     * @param name the content name.
764     * @param lang the content language. If null, the content language will be the same of the original content 
765     * @param initWorkflowActionId The initial workflow action id
766     * @return the copied content.
767     * @throws AmetysRepositoryException If an error occured
768     */
769    public ModifiableContent copy(DefaultContent originalContent, ModifiableTraversableAmetysObject parent, String name, String lang, int initWorkflowActionId) throws AmetysRepositoryException
770    {
771        return copy(originalContent, parent, name, lang, initWorkflowActionId, true, false, false);
772    }
773    
774    /**
775     * Copy a content.
776     * @param originalContent the original content.
777     * @param parent the object in which to create a content.
778     * @param name the content name.
779     * @param lang the content language. If null, the content language will be the same of the original content 
780     * @param initWorkflowActionId The initial workflow action id
781     * @param notifyObservers Set to false to do not fire observer events
782     * @param waitAsyncObservers true to wait for asynchronous observers to complete
783     * @param copyACL true to copy ACL of source content
784     * @return the copied content.
785     * @throws AmetysRepositoryException If an error occured
786     */
787    public ModifiableContent copy(DefaultContent originalContent, ModifiableTraversableAmetysObject parent, String name, String lang, int initWorkflowActionId, boolean notifyObservers, boolean waitAsyncObservers, boolean copyACL) throws AmetysRepositoryException
788    {
789        try
790        {
791            String originalName = name == null ? originalContent.getName() : name;
792            String contentName = originalName;
793            int index = 2;
794            while (parent.hasChild(contentName))
795            {
796                contentName = originalName + "-" + (index++);
797            }
798            
799            String originalContentType = originalContent.getNode().getPrimaryNodeType().getName();
800            
801            ModifiableContent content = parent.createChild(contentName, originalContentType);
802            
803            String targetLanguage = lang == null ? originalContent.getLanguage() : lang;
804            if (targetLanguage != null)
805            {
806                content.setLanguage(targetLanguage);
807            }
808            
809            content.setTypes(originalContent.getTypes());
810            
811            _modifiableContentHelper.copyTitle(originalContent, content);
812            
813            if (originalContent instanceof WorkflowAwareContent)
814            {
815                WorkflowAwareContent waOriginalContent = (WorkflowAwareContent) originalContent;
816                AmetysObjectWorkflow originalContentWorkflow = _workflowProvider.getAmetysObjectWorkflow(waOriginalContent);
817                String workflowName = originalContentWorkflow.getWorkflowName(waOriginalContent.getWorkflowId());
818                
819                // Initialize new content workflow
820                WorkflowAwareContent waContent = (WorkflowAwareContent) content;
821                AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
822                
823                HashMap<String, Object> inputs = new HashMap<>();
824                // Provide the content key
825                inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, waContent);
826                
827                long workflowId = workflow.initialize(workflowName, initWorkflowActionId, inputs);
828                waContent.setWorkflowId(workflowId);
829                
830                // Set the current step ID property
831                Step currentStep = (Step) workflow.getCurrentSteps(workflowId).iterator().next();
832                waContent.setCurrentStepId(currentStep.getStepId());
833                
834                Node workflowEntryNode = null;
835                Node node = waContent.getNode();
836                Session session = node.getSession();
837                
838                
839                try
840                {
841                    AbstractJackrabbitWorkflowStore workflowStore = (AbstractJackrabbitWorkflowStore) workflow.getConfiguration().getWorkflowStore();
842                    
843                    if (workflowStore instanceof AmetysObjectWorkflowStore)
844                    {
845                        AmetysObjectWorkflowStore ametysObjectWorkflowStore = (AmetysObjectWorkflowStore) workflowStore;
846                        ametysObjectWorkflowStore.bindAmetysObject(waContent);
847                    }
848                    
849                    workflowEntryNode = workflowStore.getEntryNode(session, workflowId);
850                    workflowEntryNode.setProperty("ametys-internal:initialActionId", initWorkflowActionId);
851                }
852                catch (RepositoryException e)
853                {
854                    throw new AmetysRepositoryException("Unable to link the workflow to the content", e);
855                }
856            }
857            
858            // Copy metadata
859            originalContent.getMetadataHolder().copyTo(content.getMetadataHolder());
860            
861            if (copyACL)
862            {
863                _copyACL(originalContent, content);
864            }
865            
866            if (_currentUserProvider.getUser() != null)
867            {
868                content.setCreator(_currentUserProvider.getUser());
869                content.setLastModified(new Date());
870                content.setCreationDate(new Date());
871            }
872            
873            parent.saveChanges();
874            
875            // Create a new version
876            if (content instanceof VersionableAmetysObject)
877            {
878                ((VersionableAmetysObject) content).checkpoint();
879            }
880
881            if (notifyObservers)
882            {
883                _notifyContentCopied(content, waitAsyncObservers);
884            }
885            
886            return content;
887        }
888        catch (WorkflowException e)
889        {
890            throw new AmetysRepositoryException(e);
891        }
892        catch (RepositoryException e)
893        {
894            throw new AmetysRepositoryException(e);
895        }
896    }
897    
898    /**
899     * Copy the ACL of a content
900     * @param srcContent The source content
901     * @param targetContent The target content
902     */
903    protected void _copyACL(Content srcContent, Content targetContent)
904    {
905        if (srcContent instanceof DefaultContent && targetContent instanceof DefaultContent)
906        {
907            Node srcNode = ((DefaultContent) srcContent).getNode();
908            Node targetNode = ((DefaultContent) targetContent).getNode();
909            
910            try
911            {
912                String aclNodeName = "ametys-internal:acl";
913                if (srcNode.hasNode(aclNodeName))
914                {
915                    Node aclNode = srcNode.getNode(aclNodeName);
916                    aclNode.getSession().getWorkspace().copy(aclNode.getPath(), targetNode.getPath() + "/" + aclNodeName);
917                }
918            }
919            catch (RepositoryException e)
920            {
921                getLogger().error("Failed to copy ACL from content " + srcContent + " to content " + targetContent, e);
922            }
923        }
924    }
925    
926    /**
927     * Notify observers that the content has been created
928     * @param content The content added
929     * @param waitAsyncObservers true to wait for asynchonous observers to finish
930     * @throws WorkflowException If an error occurred
931     */
932    protected void _notifyContentCopied(Content content, boolean waitAsyncObservers) throws WorkflowException
933    {
934        Map<String, Object> eventParams = new HashMap<>();
935        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
936        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
937
938        List<Future> futures = _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_ADDED, _currentUserProvider.getUser(), eventParams));
939        futures.addAll(_observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_WORKFLOW_CHANGED, _currentUserProvider.getUser(), eventParams)));
940        
941        if (waitAsyncObservers)
942        {
943            // Wait for asynchonous observers to finish
944            for (Future future : futures)
945            {
946                try
947                {
948                    future.get();
949                }
950                catch (ExecutionException | InterruptedException e)
951                {
952                    getLogger().error(String.format("Error while waiting for async observer to complete"));
953                }
954            }
955        }
956    }
957    
958    /**
959     * Returns the content's attachments root node
960     * @param id the content's id
961     * @return The attachments' root node informations
962     */
963    @Callable
964    public Map<String, Object> getAttachmentsRootNode (String id)
965    {
966        Map<String, Object> result = new HashMap<>();
967        
968        Content content = _resolver.resolveById(id);
969        
970        result.put("title", _contentHelper.getTitle(content));
971        result.put("contentId", content.getId());
972        
973        TraversableAmetysObject attachments = content.getRootAttachments();
974        
975        if (attachments != null)
976        {
977            result.put("id", attachments.getId());
978            if (attachments instanceof ModifiableAmetysObject)
979            {
980                result.put("isModifiable", true);
981            }
982            if (attachments instanceof ModifiableResourceCollection)
983            {
984                result.put("canCreateChild", true);
985            }
986            
987            boolean hasChildNodes = false;
988            boolean hasResources = false;
989
990            for (AmetysObject child : attachments.getChildren())
991            {
992                if (child instanceof Resource)
993                {
994                    hasResources = true;
995                }
996                else if (child instanceof ExplorerNode)
997                {
998                    hasChildNodes = true;
999                }
1000            }
1001
1002            if (hasChildNodes)
1003            {
1004                result.put("hasChildNodes", true);
1005            }
1006
1007            if (hasResources)
1008            {
1009                result.put("hasResources", true);
1010            }
1011            
1012            return result;
1013        }
1014        
1015        throw new IllegalArgumentException("Content with id '" + id + "' does not support attachments.");
1016    }
1017    
1018    /**
1019     * Dertermines if the current user has right to delete the content
1020     * @param content The content
1021     * @return true if current user is authorized to delete the content
1022     */
1023    public boolean canDelete(Content content)
1024    {
1025        UserIdentity user = _currentUserProvider.getUser();
1026        if (_rightManager.hasRight(user, "CMS_Rights_DeleteContent", content) == RightResult.RIGHT_ALLOW)
1027        {
1028            return true;
1029        }
1030        
1031        return false;
1032    }
1033}