001/*
002 *  Copyright 2017 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.odf.workflow.copy;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.commons.lang.StringUtils;
028
029import org.ametys.cms.ObservationConstants;
030import org.ametys.cms.clientsideelement.SmartContentClientSideElement;
031import org.ametys.cms.indexing.solr.SolrIndexHelper;
032import org.ametys.cms.repository.Content;
033import org.ametys.cms.workflow.ContentWorkflowHelper;
034import org.ametys.core.observation.ObservationManager;
035import org.ametys.core.ui.Callable;
036import org.ametys.core.user.UserIdentity;
037import org.ametys.core.util.JSONUtils;
038import org.ametys.odf.ODFHelper;
039import org.ametys.odf.ProgramItem;
040import org.ametys.odf.course.Course;
041import org.ametys.odf.course.ShareableCourseHelper;
042import org.ametys.odf.courselist.CourseList;
043import org.ametys.runtime.i18n.I18nizableText;
044
045/**
046 * Client side element for ODF content copy
047 * 
048 */
049public class CopyODFContentClientSideElement extends SmartContentClientSideElement
050{
051    /** The key to get the duplication mode */
052    public static final String DUPLICATION_MODE_KEY = "$duplicationMode";
053    
054    /** The key to get the parent ProgramPart's id */
055    public static final String PARENT_KEY = "$parent";
056    
057    /** The key to tell if we keep the creation title */
058    public static final String KEEP_CREATION_TITLE_KEY = "$keepCreationTitle";
059    
060    /** The content workflow helper */
061    protected ContentWorkflowHelper _contentWorkflowHelper;
062    /** The observation manager */
063    protected ObservationManager _observationManager;
064    /** The ODF helper */
065    protected ODFHelper _odfHelper;
066    /** The client side element for copy */
067    protected CopyContentClientInteraction _copyClientSideInteraction;
068    /** JSON utils */
069    protected JSONUtils _jsonUtils;
070    /** The shareable course helper */
071    protected ShareableCourseHelper _shareableCourseHelper;
072    /** The Solr index helper */
073    protected SolrIndexHelper _solrIndexHelper;
074    
075    @Override
076    public void service(ServiceManager sManager) throws ServiceException
077    {
078        super.service(sManager);
079        _contentWorkflowHelper = (ContentWorkflowHelper) sManager.lookup(ContentWorkflowHelper.ROLE);
080        _observationManager = (ObservationManager) sManager.lookup(ObservationManager.ROLE);
081        _odfHelper = (ODFHelper) sManager.lookup(ODFHelper.ROLE);
082        _copyClientSideInteraction = (CopyContentClientInteraction) sManager.lookup(CopyContentClientInteraction.class.getName());
083        _jsonUtils = (JSONUtils) sManager.lookup(JSONUtils.ROLE);
084        _shareableCourseHelper = (ShareableCourseHelper) sManager.lookup(ShareableCourseHelper.ROLE);
085        _solrIndexHelper = (SolrIndexHelper) sManager.lookup(SolrIndexHelper.ROLE);
086    }
087    
088    /**
089     * Determines if a ODF content can be copied and linked to a target content
090     * @param copiedContentId The id of copied content
091     * @param targetContentId The id of target content
092     * @param contextualParameters the contextual parameters
093     * @return the result with success to true if copy is available
094     */
095    @Callable
096    public Map<String, Object> canCopyTo(String copiedContentId, String targetContentId, Map<String, Object> contextualParameters)
097    {
098        Content copiedContent = _resolver.resolveById(copiedContentId);
099        Content targetContent = _resolver.resolveById(targetContentId);
100           
101        Map<String, Object> result = new HashMap<>();
102        
103        // Put the mode to copy to prevent to check shareable course fields
104        contextualParameters.put("mode", "copy");
105        
106        List<I18nizableText> errors = new ArrayList<>();
107        if (!_odfHelper.isRelationCompatible(copiedContent, targetContent, errors, contextualParameters))
108        {
109            // Invalid target
110            result.put("errorMessages", errors);
111            result.put("success", false); 
112        }
113        else
114        {
115            result.put("success", true); 
116        }
117        
118        return result;
119    }
120    
121    /**
122     * Creates a content by copy of another one.<br>
123     * Also handle the possible inner duplication depending on the duplication mode for each attribute of type "content". 
124     * @param baseContentId The id of content to copy
125     * @param newContentTitle The title of content to create
126     * @param viewNameToCopy The view name to copy. Can be null
127     * @param fallbackViewNameToCopy The fallback view name to use if 'viewNameToCopy' does not exist. Can be null
128     * @param viewMode The view type to copy. Can be null
129     * @param initActionId The init workflow action id for copy
130     * @param editActionId The workflow action for editing content
131     * @param duplicationModeAsString the duplication mode
132     * @param parentContentId the parent id under which the duplicated content will be created. Can be null
133     * @return the copy result
134     * @throws Exception if an error occurred during copy
135     */
136    @Callable
137    public Map<String, Object> createContentByCopy(String baseContentId, String newContentTitle, String viewNameToCopy, String fallbackViewNameToCopy, String viewMode, int initActionId, int editActionId, String duplicationModeAsString, String parentContentId) throws Exception
138    {
139        Map<String, Object> result = new HashMap<>();
140        result.put("locked-contents", new ArrayList<Map<String, Object>>());
141
142        String [] handledEventIds = new String[] {ObservationConstants.EVENT_CONTENT_ADDED, ObservationConstants.EVENT_CONTENT_MODIFIED,  ObservationConstants.EVENT_CONTENT_WORKFLOW_CHANGED};
143        try
144        {
145            _solrIndexHelper.pauseSolrCommitForEvents(handledEventIds);
146            
147            DuplicationMode duplicationMode = StringUtils.isNotBlank(duplicationModeAsString) ? DuplicationMode.valueOf(duplicationModeAsString.toUpperCase()) : DuplicationMode.SINGLE;
148            
149            if (!checkBeforeDuplication(baseContentId, parentContentId, duplicationMode, result))
150            {
151                result.put("check-before-duplication-failed", true);
152            }
153            else
154            {
155                Map<String, Object> copyMap = new HashMap<>();
156                copyMap.put(DUPLICATION_MODE_KEY, duplicationMode.toString());
157                copyMap.put(PARENT_KEY, parentContentId);
158                
159                String jsonMap = _jsonUtils.convertObjectToJson(copyMap);
160                result = _copyClientSideInteraction.createContentByCopy(baseContentId, newContentTitle, jsonMap, viewNameToCopy, fallbackViewNameToCopy, viewMode, initActionId, editActionId);
161                
162                if (result.containsKey("contentIds"))
163                {
164                    @SuppressWarnings("unchecked")
165                    List<String> contentIds = (List<String>) result.getOrDefault("contentIds", new ArrayList<>());
166                    for (String contentId : contentIds)
167                    {
168                        _initializeShareableFields(contentId, parentContentId, contentIds);
169                    }
170                }
171            }
172        }
173        finally 
174        {
175            _solrIndexHelper.restartSolrCommitForEvents(handledEventIds);
176        }
177        
178        return result;
179    }
180    
181    /**
182     * Initialize shareable fields for the copied content
183     * @param copiedContentId the copied content id
184     * @param parentContentId the parent content id
185     * @param createdContentIds the list of created content ids by copy
186     */
187    protected void _initializeShareableFields(String copiedContentId, String parentContentId, List<String> createdContentIds)
188    {
189        Content mainContent = _resolver.resolveById(copiedContentId);
190        if (mainContent instanceof Course)
191        {
192            // Get created parent course list of created course content
193            Course course = (Course) mainContent;
194            String courseListParentId = course.getParentCourseLists()
195                .stream()
196                .map(Content::getId)
197                .filter(createdContentIds::contains) // filter on created course list by the copy
198                .findFirst()
199                .orElse(parentContentId);               
200            
201            UserIdentity user = _currentUserProvider.getUser();
202            
203            CourseList courseListParent = StringUtils.isNotBlank(courseListParentId) ? _resolver.resolveById(courseListParentId) : null;
204            if (_shareableCourseHelper.initializeShareableFields(course, courseListParent, user, false))
205            {
206                course.saveChanges();
207                
208                // Create a new version
209                course.checkpoint();
210            }
211        }
212    }
213    
214    /**
215     * Check that duplication can be performed without blocking errors
216     * @param contentId The content id to copy
217     * @param parentContentId The parent content id
218     * @param duplicationMode The duplication mode
219     * @param results the results map
220     * @return true if the duplication can be performed
221     */
222    protected boolean checkBeforeDuplication(String contentId, String parentContentId, DuplicationMode duplicationMode, Map<String, Object> results)
223    {
224        boolean allRight = true;
225        if (StringUtils.isNotBlank(parentContentId))
226        {
227            // Check if the parent is locked
228            Content parentContent = _resolver.resolveById(parentContentId);
229            if (_isLocked(parentContent))
230            {
231                @SuppressWarnings("unchecked")
232                List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) results.get("locked-contents");
233                Map<String, Object> contentParams = getContentDefaultParameters (parentContent);
234                contentParams.put("description", _getLockedDescription(parentContent));
235                lockedContents.add(contentParams);
236            }
237        }
238            
239        Content content = _resolver.resolveById(contentId);
240        if (content instanceof ProgramItem programItem)
241        {
242            if (duplicationMode == DuplicationMode.SINGLE)
243            {
244                // Check if the child are locked
245                for (ProgramItem programItemChild : _odfHelper.getChildProgramItems(programItem))
246                {
247                    if (_isLocked((Content) programItemChild))
248                    {
249                        @SuppressWarnings("unchecked")
250                        List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) results.get("locked-contents");
251                        Map<String, Object> contentParams = getContentDefaultParameters ((Content) programItemChild);
252                        contentParams.put("description", _getLockedDescription((Content) programItemChild));
253                        lockedContents.add(contentParams);
254                        
255                        allRight = false;
256                    }
257                }
258            }
259            else if (duplicationMode == DuplicationMode.STRUCTURE_ONLY)
260            {
261                // Check if course child are locked
262                for (Course course : _getCourse(programItem))
263                {
264                    if (_isLocked(course))
265                    {
266                        @SuppressWarnings("unchecked")
267                        List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) results.get("locked-contents");
268                        Map<String, Object> contentParams = getContentDefaultParameters (course);
269                        contentParams.put("description", _getLockedDescription(course));
270                        lockedContents.add(contentParams);
271                        
272                        allRight = false;
273                    }
274                }
275            }
276        }
277        
278        return allRight;
279    }
280    
281    /**
282     * Get all first courses in sub item of the program item
283     * @param programItem the program item
284     * @return a set of courses
285     */
286    protected Set<Course> _getCourse(ProgramItem programItem)
287    {
288        Set<Course> courses = new HashSet<>();
289        for (ProgramItem programItemChild : _odfHelper.getChildProgramItems(programItem))
290        {
291            if (programItemChild instanceof Course)
292            {
293                courses.add((Course) programItemChild);
294            }
295            else
296            {
297                courses.addAll(_getCourse(programItemChild));
298            }
299        }
300        
301        return courses;
302    }
303
304    /**
305     * Enumeration for the mode of duplication
306     */
307    public enum DuplicationMode
308    {
309        /** Duplicate the content only */
310        SINGLE,
311        /** Duplicate the content and its structure */
312        STRUCTURE_ONLY,
313        /** Duplicate the content and its structure and its courses */
314        FULL
315    }
316}