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