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