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