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}