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}