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.Arrays;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026import java.util.concurrent.ExecutionException;
027import java.util.concurrent.Future;
028import java.util.stream.Collectors;
029
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.commons.lang.StringUtils;
033import org.apache.commons.lang3.ArrayUtils;
034import org.apache.solr.client.solrj.SolrServerException;
035
036import org.ametys.cms.ObservationConstants;
037import org.ametys.cms.clientsideelement.SmartContentClientSideElement;
038import org.ametys.cms.content.external.ExternalizableMetadataHelper;
039import org.ametys.cms.content.indexing.solr.SolrIndexer;
040import org.ametys.cms.repository.Content;
041import org.ametys.cms.repository.WorkflowAwareContent;
042import org.ametys.cms.workflow.ContentWorkflowHelper;
043import org.ametys.cms.workflow.copy.CopyContentClientInteraction;
044import org.ametys.core.observation.Event;
045import org.ametys.core.observation.ObservationManager;
046import org.ametys.core.ui.Callable;
047import org.ametys.core.util.JSONUtils;
048import org.ametys.odf.ODFHelper;
049import org.ametys.odf.ProgramItem;
050import org.ametys.odf.course.Course;
051import org.ametys.odf.courselist.CourseList;
052import org.ametys.odf.courselist.CourseListContainer;
053import org.ametys.odf.program.Program;
054import org.ametys.odf.program.ProgramPart;
055import org.ametys.odf.program.TraversableProgramPart;
056import org.ametys.plugins.repository.AmetysRepositoryException;
057import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
058
059import com.opensymphony.workflow.WorkflowException;
060
061/**
062 * Client side element for ODF content copy
063 * 
064 */
065public class CopyODFContentClientSideElement extends SmartContentClientSideElement
066{
067    /** The key to get the duplication mode */
068    public static final String DUPLICATION_MODE_KEY = "$duplicationMode";
069    
070    /** The key to tell if we keep the creation title */
071    public static final String KEEP_CREATION_TITLE_KEY = "$keepCreationTitle";
072    
073    /** The content workflow helper */
074    protected ContentWorkflowHelper _contentWorkflowHelper;
075    /** The observation manager */
076    protected ObservationManager _observationManager;
077    /** The solr indexer */
078    protected SolrIndexer _solrIndexer;
079    /** The ODF helper */
080    protected ODFHelper _odfHelper;
081
082    private CopyContentClientInteraction _copyClientSideInteraction;
083
084    private JSONUtils _jsonUtils;
085    
086    @Override
087    public void service(ServiceManager sManager) throws ServiceException
088    {
089        super.service(sManager);
090        _contentWorkflowHelper = (ContentWorkflowHelper) sManager.lookup(ContentWorkflowHelper.ROLE);
091        _observationManager = (ObservationManager) sManager.lookup(ObservationManager.ROLE);
092        _solrIndexer = (SolrIndexer) sManager.lookup(SolrIndexer.ROLE);
093        _odfHelper = (ODFHelper) sManager.lookup(ODFHelper.ROLE);
094        _copyClientSideInteraction = (CopyContentClientInteraction) sManager.lookup(CopyContentClientInteraction.class.getName());
095        _jsonUtils = (JSONUtils) sManager.lookup(JSONUtils.ROLE);
096    }
097    
098    /**
099     * Creates a content by copy of another one.<br>
100     * Also handle the possible inner duplication depending on the duplication mode for each metadata of type "content". 
101     * @param baseContentId The id of content to copy
102     * @param newContentTitle The title of content to create
103     * @param metadataSetNameToCopy The metadata set name to copy. Can be null
104     * @param metadataSetTypeToCopy The metadata set type to copy. Can be null
105     * @param initActionId The init workflow action id for copy
106     * @param editActionId The workflow action for editing content
107     * @param duplicationModeAsString the duplication mode
108     * @param parentContentId the parent id under which the duplicated content will be created. Can be null
109     * @return the copy result
110     * @throws Exception if an error occurred during copy
111     */
112    @Callable
113    public Map<String, Object> createContentByCopy(String baseContentId, String newContentTitle, String metadataSetNameToCopy, String metadataSetTypeToCopy, int initActionId, int editActionId, String duplicationModeAsString, String parentContentId) throws Exception
114    {
115        Map<String, Object> result = new HashMap<>();
116        result.put("locked-contents", new ArrayList<Map<String, Object>>());
117        
118        try
119        {
120            String [] handledEventIds = new String[] {ObservationConstants.EVENT_CONTENT_ADDED, ObservationConstants.EVENT_CONTENT_MODIFIED,  ObservationConstants.EVENT_CONTENT_WORKFLOW_CHANGED};
121            _observationManager.addArgumentForEvents(handledEventIds, ObservationConstants.ARGS_CONTENT_COMMIT, false);
122            
123            DuplicationMode duplicationMode = StringUtils.isNotBlank(duplicationModeAsString) ? DuplicationMode.valueOf(duplicationModeAsString.toUpperCase()) : DuplicationMode.SINGLE;
124            
125            if (!checkBeforeDuplication(baseContentId, parentContentId, duplicationMode, result))
126            {
127                result.put("check-before-duplication-failed", true);
128            }
129            else
130            {
131                Map<String, Object> copyMap = new HashMap<>();
132                copyMap.put(DUPLICATION_MODE_KEY, duplicationMode.toString());
133                copyMap.put(KEEP_CREATION_TITLE_KEY, true);
134                
135                String jsonMap = _jsonUtils.convertObjectToJson(copyMap);
136                result = _copyClientSideInteraction.createContentByCopy(baseContentId, newContentTitle, jsonMap, metadataSetNameToCopy, metadataSetTypeToCopy, initActionId, editActionId);
137                
138                if (StringUtils.isNotBlank(parentContentId) && result.containsKey("mainContentId"))
139                {
140                    String mainContentId = (String) result.get("mainContentId");
141                    _linkCopiedContentToParent(mainContentId, parentContentId);
142                }
143            }
144        }
145        finally 
146        {
147            _observationManager.removeArgumentForEvents(new String[] {ObservationConstants.EVENT_CONTENT_ADDED}, ObservationConstants.ARGS_CONTENT_COMMIT);
148            _commitAllChanges();
149        }
150        
151        return result;
152    }
153    
154    /**
155     * Check that duplication can be performed without blocking errors
156     * @param programItemId The program item id to copy
157     * @param parentContentId The parent content id
158     * @param duplicationMode The duplication mode
159     * @param results the results map
160     * @return true if the duplication can be performed
161     */
162    protected boolean checkBeforeDuplication(String programItemId, String parentContentId, DuplicationMode duplicationMode, Map<String, Object> results)
163    {
164        boolean allRight = true;
165        if (StringUtils.isNotBlank(parentContentId))
166        {
167            // Check if the parent is locked
168            Content parentContent = _resolver.resolveById(parentContentId);
169            if (_isLocked(parentContent))
170            {
171                @SuppressWarnings("unchecked")
172                List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) results.get("locked-contents");
173                Map<String, Object> contentParams = getContentDefaultParameters (parentContent);
174                contentParams.put("description", _getLockedDescription(parentContent));
175                lockedContents.add(contentParams);
176            }
177        }
178            
179        ProgramItem programItem = _resolver.resolveById(programItemId);
180        if (duplicationMode == DuplicationMode.SINGLE)
181        {
182            // Check if the child are locked
183            for (ProgramItem programItemChild : _odfHelper.getChildProgramItems(programItem))
184            {
185                if (_isLocked((Content) programItemChild))
186                {
187                    @SuppressWarnings("unchecked")
188                    List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) results.get("locked-contents");
189                    Map<String, Object> contentParams = getContentDefaultParameters ((Content) programItemChild);
190                    contentParams.put("description", _getLockedDescription((Content) programItemChild));
191                    lockedContents.add(contentParams);
192                    
193                    allRight = false;
194                }
195            }
196        }
197        else if (duplicationMode == DuplicationMode.STRUCTURE_ONLY)
198        {
199            // Check if course child are locked
200            for (Course course : _getCourse(programItem))
201            {
202                if (_isLocked(course))
203                {
204                    @SuppressWarnings("unchecked")
205                    List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) results.get("locked-contents");
206                    Map<String, Object> contentParams = getContentDefaultParameters (course);
207                    contentParams.put("description", _getLockedDescription(course));
208                    lockedContents.add(contentParams);
209                    
210                    allRight = false;
211                }
212            }
213        }
214        
215        return allRight;
216    }
217    
218    /**
219     * Get all first courses in sub item of the program item
220     * @param programItem the program item
221     * @return a set of courses
222     */
223    protected Set<Course> _getCourse(ProgramItem programItem)
224    {
225        Set<Course> courses = new HashSet<>();
226        for (ProgramItem programItemChild : _odfHelper.getChildProgramItems(programItem))
227        {
228            if (programItemChild instanceof Course)
229            {
230                courses.add((Course) programItemChild);
231            }
232            else
233            {
234                courses.addAll(_getCourse(programItemChild));
235            }
236        }
237        
238        return courses;
239    }
240
241    /**
242     * Check if we can add the copied content to the parent content and set the relation
243     * @param copiedContentId the copied content id
244     * @param parentContentId the parent content id
245     */
246    protected void _linkCopiedContentToParent(String copiedContentId, String parentContentId)
247    {
248        if (StringUtils.isNotBlank(parentContentId))
249        {
250            Content parentContent = _resolver.resolveById(parentContentId);
251            if (_isLocked(parentContent))
252            {
253                //The content will be copied with no parent
254                return;
255            }
256            Content copiedContent = _resolver.resolveById(copiedContentId);
257            
258            if (!(copiedContent instanceof Program))
259            {
260                if (copiedContent instanceof CourseList && parentContent instanceof CourseListContainer)
261                {
262                    if (parentContent instanceof Course)
263                    {
264                        _addRelation((WorkflowAwareContent) parentContent, (WorkflowAwareContent) copiedContent, Course.METADATA_CHILD_COURSE_LISTS, CourseList.METADATA_PARENT_COURSES, 22);
265                    }
266                    else
267                    {
268                        _addRelation((WorkflowAwareContent) parentContent, (WorkflowAwareContent) copiedContent, TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS, ProgramPart.METADATA_PARENT_PROGRAM_PARTS, 22);
269                    }
270                }
271                else if (copiedContent instanceof ProgramPart && parentContent instanceof ProgramPart)
272                {
273                    _addRelation((WorkflowAwareContent) parentContent, (WorkflowAwareContent) copiedContent, TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS, ProgramPart.METADATA_PARENT_PROGRAM_PARTS, 22);
274                    
275                }
276                else if (copiedContent instanceof Course && parentContent instanceof CourseList)
277                {
278                    _addRelation((WorkflowAwareContent) parentContent, (WorkflowAwareContent) copiedContent, CourseList.METADATA_CHILD_COURSES, Course.METADATA_PARENT_COURSE_LISTS, 22);
279                    
280                }
281            }
282        }
283    }
284    
285    /**
286     * Add the relation parent-child relation on content. 
287     * @param parentContent The parent content
288     * @param childContent The child content to be added from parentContent
289     * @param parentMetadataName The name of the parent metadata holding the child relationship
290     * @param childMetadataName The name of the child metadata holding the parent relationship
291     * @param actionId The id of workflow action to edit the relation
292     * @return boolean true if add relation successfully
293     */
294    protected boolean _addRelation(WorkflowAwareContent parentContent, WorkflowAwareContent childContent, String parentMetadataName, String childMetadataName, int actionId)
295    {
296        try
297        {
298            setJCRReference(parentContent, childContent, parentMetadataName);
299            setJCRReference(childContent, parentContent, childMetadataName);
300
301            _applyChanges(parentContent, actionId);
302            return true;
303        }
304        catch (WorkflowException | AmetysRepositoryException e)
305        {
306            getLogger().error("Unable to add relationship to content " + parentContent.getTitle() + " (" + parentContent.getId() + ") with content " + childContent.getTitle() + " (" + childContent.getId() + ") for metadatas " + parentMetadataName + " and " + childMetadataName, e);
307            return false;
308        }
309    }
310    
311    /**
312     * Add the jcr relation on content. 
313     * @param contentToEdit The content edit
314     * @param refContent The referenced content to be added
315     * @param metadataName The name of the metadata holding the relationship
316     */
317    protected void setJCRReference(WorkflowAwareContent contentToEdit, Content refContent, String metadataName)
318    {
319        ModifiableCompositeMetadata metadataHolder = contentToEdit.getMetadataHolder();
320        String[] values = metadataHolder.getStringArray(metadataName, new String[0]);
321        
322        if (!ArrayUtils.contains(values, refContent.getId()))
323        {
324            String[] newValues = ArrayUtils.add(values, refContent.getId());
325            
326            List<Content> newContents = Arrays.asList(newValues).stream()
327                    .map(id -> (Content) _resolver.resolveById(id))
328                    .collect(Collectors.toList());
329            
330            // Set Jcr content reference if we pass contents in arguments
331            ExternalizableMetadataHelper.setMetadata(metadataHolder, metadataName, newContents.toArray(new Content[newContents.size()]));
332        }
333    }
334    
335    /**
336     * Apply changes to the content
337     * @param content the content changed
338     * @param actionId the workflow action id
339     * @throws WorkflowException if an error occurred
340     */
341    protected void _applyChanges(WorkflowAwareContent content, int actionId) throws WorkflowException
342    {
343        // Notify listeners
344        Map<String, Object> eventParams = new HashMap<>();
345        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
346        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams));
347       
348        _contentWorkflowHelper.doAction(content, actionId);
349    }
350    
351    /**
352     * Commit all changes in solr
353     */
354    protected void _commitAllChanges()
355    {
356        // Before trying to commit, be sure all the async observers of the current request are finished
357        for (Future future : _observationManager.getFuturesForRequest())
358        {
359            try
360            {
361                future.get();
362            }
363            catch (ExecutionException | InterruptedException e)
364            {
365                getLogger().info("An exception occured when calling #get() on Future result of an observer." , e);
366            }
367        }
368        
369        // Commit all uncommited changes
370        try
371        {
372            _solrIndexer.commit();
373            
374            if (getLogger().isDebugEnabled())
375            {
376                getLogger().info("Deleted contents are now committed into Solr.");
377            }
378        }
379        catch (IOException | SolrServerException e)
380        {
381            getLogger().error("Impossible to commit changes", e);
382        }
383    }
384    
385    /**
386     * Enumeration for the mode of duplication
387     */
388    public enum DuplicationMode
389    {
390        /** Duplicate the content only */
391        SINGLE,
392        /** Duplicate the content and its structure */
393        STRUCTURE_ONLY,
394        /** Duplicate the content and its structure and its courses */
395        FULL
396    }
397}