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