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}