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; 058import org.ametys.runtime.i18n.I18nizableText; 059 060import com.opensymphony.workflow.WorkflowException; 061 062/** 063 * Client side element for ODF content copy 064 * 065 */ 066public class CopyODFContentClientSideElement extends SmartContentClientSideElement 067{ 068 /** The key to get the duplication mode */ 069 public static final String DUPLICATION_MODE_KEY = "$duplicationMode"; 070 071 /** The key to tell if we keep the creation title */ 072 public static final String KEEP_CREATION_TITLE_KEY = "$keepCreationTitle"; 073 074 /** The content workflow helper */ 075 protected ContentWorkflowHelper _contentWorkflowHelper; 076 /** The observation manager */ 077 protected ObservationManager _observationManager; 078 /** The solr indexer */ 079 protected SolrIndexer _solrIndexer; 080 /** The ODF helper */ 081 protected ODFHelper _odfHelper; 082 /** The client side element for copy */ 083 protected CopyContentClientInteraction _copyClientSideInteraction; 084 /** JSON utils */ 085 protected JSONUtils _jsonUtils; 086 087 @Override 088 public void service(ServiceManager sManager) throws ServiceException 089 { 090 super.service(sManager); 091 _contentWorkflowHelper = (ContentWorkflowHelper) sManager.lookup(ContentWorkflowHelper.ROLE); 092 _observationManager = (ObservationManager) sManager.lookup(ObservationManager.ROLE); 093 _solrIndexer = (SolrIndexer) sManager.lookup(SolrIndexer.ROLE); 094 _odfHelper = (ODFHelper) sManager.lookup(ODFHelper.ROLE); 095 _copyClientSideInteraction = (CopyContentClientInteraction) sManager.lookup(CopyContentClientInteraction.class.getName()); 096 _jsonUtils = (JSONUtils) sManager.lookup(JSONUtils.ROLE); 097 } 098 099 /** 100 * Determines if a ODF content can be copied and linked to a target content 101 * @param copiedContentId The id of copied content 102 * @param targetContentId The id of target content 103 * @param contextualParameters the contextual parameters 104 * @return the result with success to true if copy is available 105 */ 106 @Callable 107 public Map<String, Object> canCopyTo(String copiedContentId, String targetContentId, Map<String, Object> contextualParameters) 108 { 109 Content copiedContent = _resolver.resolveById(copiedContentId); 110 Content targetContent = _resolver.resolveById(targetContentId); 111 112 Map<String, Object> result = new HashMap<>(); 113 114 List<I18nizableText> errors = new ArrayList<>(); 115 if (!_odfHelper.isRelationCompatible(copiedContent, targetContent, errors, contextualParameters)) 116 { 117 // Invalid target 118 result.put("errorMessages", errors); 119 result.put("success", false); 120 } 121 else 122 { 123 result.put("success", true); 124 } 125 126 return result; 127 } 128 129 /** 130 * Creates a content by copy of another one.<br> 131 * Also handle the possible inner duplication depending on the duplication mode for each metadata of type "content". 132 * @param baseContentId The id of content to copy 133 * @param newContentTitle The title of content to create 134 * @param metadataSetNameToCopy The metadata set name to copy. Can be null 135 * @param metadataSetTypeToCopy The metadata set type to copy. Can be null 136 * @param initActionId The init workflow action id for copy 137 * @param editActionId The workflow action for editing content 138 * @param duplicationModeAsString the duplication mode 139 * @param parentContentId the parent id under which the duplicated content will be created. Can be null 140 * @return the copy result 141 * @throws Exception if an error occurred during copy 142 */ 143 @Callable 144 public Map<String, Object> createContentByCopy(String baseContentId, String newContentTitle, String metadataSetNameToCopy, String metadataSetTypeToCopy, 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<Map<String, Object>>()); 148 149 String [] handledEventIds = new String[] {ObservationConstants.EVENT_CONTENT_ADDED, ObservationConstants.EVENT_CONTENT_MODIFIED, ObservationConstants.EVENT_CONTENT_WORKFLOW_CHANGED}; 150 try 151 { 152 _observationManager.addArgumentForEvents(handledEventIds, ObservationConstants.ARGS_CONTENT_COMMIT, false); 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(KEEP_CREATION_TITLE_KEY, true); 165 166 String jsonMap = _jsonUtils.convertObjectToJson(copyMap); 167 result = _copyClientSideInteraction.createContentByCopy(baseContentId, newContentTitle, jsonMap, metadataSetNameToCopy, metadataSetTypeToCopy, initActionId, editActionId); 168 169 if (StringUtils.isNotBlank(parentContentId) && result.containsKey("mainContentId")) 170 { 171 String mainContentId = (String) result.get("mainContentId"); 172 _linkCopiedContentToParent(mainContentId, parentContentId); 173 } 174 } 175 } 176 finally 177 { 178 _observationManager.removeArgumentForEvents(handledEventIds, ObservationConstants.ARGS_CONTENT_COMMIT); 179 _commitAllChanges(); 180 } 181 182 return result; 183 } 184 185 /** 186 * Check that duplication can be performed without blocking errors 187 * @param programItemId The program item id to copy 188 * @param parentContentId The parent content id 189 * @param duplicationMode The duplication mode 190 * @param results the results map 191 * @return true if the duplication can be performed 192 */ 193 protected boolean checkBeforeDuplication(String programItemId, String parentContentId, DuplicationMode duplicationMode, Map<String, Object> results) 194 { 195 boolean allRight = true; 196 if (StringUtils.isNotBlank(parentContentId)) 197 { 198 // Check if the parent is locked 199 Content parentContent = _resolver.resolveById(parentContentId); 200 if (_isLocked(parentContent)) 201 { 202 @SuppressWarnings("unchecked") 203 List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) results.get("locked-contents"); 204 Map<String, Object> contentParams = getContentDefaultParameters (parentContent); 205 contentParams.put("description", _getLockedDescription(parentContent)); 206 lockedContents.add(contentParams); 207 } 208 } 209 210 ProgramItem programItem = _resolver.resolveById(programItemId); 211 if (duplicationMode == DuplicationMode.SINGLE) 212 { 213 // Check if the child are locked 214 for (ProgramItem programItemChild : _odfHelper.getChildProgramItems(programItem)) 215 { 216 if (_isLocked((Content) programItemChild)) 217 { 218 @SuppressWarnings("unchecked") 219 List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) results.get("locked-contents"); 220 Map<String, Object> contentParams = getContentDefaultParameters ((Content) programItemChild); 221 contentParams.put("description", _getLockedDescription((Content) programItemChild)); 222 lockedContents.add(contentParams); 223 224 allRight = false; 225 } 226 } 227 } 228 else if (duplicationMode == DuplicationMode.STRUCTURE_ONLY) 229 { 230 // Check if course child are locked 231 for (Course course : _getCourse(programItem)) 232 { 233 if (_isLocked(course)) 234 { 235 @SuppressWarnings("unchecked") 236 List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) results.get("locked-contents"); 237 Map<String, Object> contentParams = getContentDefaultParameters (course); 238 contentParams.put("description", _getLockedDescription(course)); 239 lockedContents.add(contentParams); 240 241 allRight = false; 242 } 243 } 244 } 245 246 return allRight; 247 } 248 249 /** 250 * Get all first courses in sub item of the program item 251 * @param programItem the program item 252 * @return a set of courses 253 */ 254 protected Set<Course> _getCourse(ProgramItem programItem) 255 { 256 Set<Course> courses = new HashSet<>(); 257 for (ProgramItem programItemChild : _odfHelper.getChildProgramItems(programItem)) 258 { 259 if (programItemChild instanceof Course) 260 { 261 courses.add((Course) programItemChild); 262 } 263 else 264 { 265 courses.addAll(_getCourse(programItemChild)); 266 } 267 } 268 269 return courses; 270 } 271 272 /** 273 * Check if we can add the copied content to the parent content and set the relation 274 * @param copiedContentId the copied content id 275 * @param parentContentId the parent content id 276 */ 277 protected void _linkCopiedContentToParent(String copiedContentId, String parentContentId) 278 { 279 if (StringUtils.isNotBlank(parentContentId)) 280 { 281 Content parentContent = _resolver.resolveById(parentContentId); 282 if (_isLocked(parentContent)) 283 { 284 //The content will be copied with no parent 285 return; 286 } 287 Content copiedContent = _resolver.resolveById(copiedContentId); 288 289 if (!(copiedContent instanceof Program)) 290 { 291 if (copiedContent instanceof CourseList && parentContent instanceof CourseListContainer) 292 { 293 if (parentContent instanceof Course) 294 { 295 _addRelation((WorkflowAwareContent) parentContent, (WorkflowAwareContent) copiedContent, Course.METADATA_CHILD_COURSE_LISTS, CourseList.METADATA_PARENT_COURSES, 22); 296 } 297 else 298 { 299 _addRelation((WorkflowAwareContent) parentContent, (WorkflowAwareContent) copiedContent, TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS, ProgramPart.METADATA_PARENT_PROGRAM_PARTS, 22); 300 } 301 } 302 else if (copiedContent instanceof ProgramPart && parentContent instanceof ProgramPart) 303 { 304 _addRelation((WorkflowAwareContent) parentContent, (WorkflowAwareContent) copiedContent, TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS, ProgramPart.METADATA_PARENT_PROGRAM_PARTS, 22); 305 306 } 307 else if (copiedContent instanceof Course && parentContent instanceof CourseList) 308 { 309 _addRelation((WorkflowAwareContent) parentContent, (WorkflowAwareContent) copiedContent, CourseList.METADATA_CHILD_COURSES, Course.METADATA_PARENT_COURSE_LISTS, 22); 310 311 } 312 } 313 } 314 } 315 316 /** 317 * Add the relation parent-child relation on content. 318 * @param parentContent The parent content 319 * @param childContent The child content to be added from parentContent 320 * @param parentMetadataName The name of the parent metadata holding the child relationship 321 * @param childMetadataName The name of the child metadata holding the parent relationship 322 * @param actionId The id of workflow action to edit the relation 323 * @return boolean true if add relation successfully 324 */ 325 protected boolean _addRelation(WorkflowAwareContent parentContent, WorkflowAwareContent childContent, String parentMetadataName, String childMetadataName, int actionId) 326 { 327 try 328 { 329 setJCRReference(parentContent, childContent, parentMetadataName); 330 setJCRReference(childContent, parentContent, childMetadataName); 331 332 _applyChanges(parentContent, actionId); 333 return true; 334 } 335 catch (WorkflowException | AmetysRepositoryException e) 336 { 337 getLogger().error("Unable to add relationship to content {} ({}) with content {} ({}) for metadatas {} and {}", parentContent.getTitle(), parentContent.getId(), childContent.getTitle(), childContent.getId(), parentMetadataName, childMetadataName, e); 338 return false; 339 } 340 } 341 342 /** 343 * Add the jcr relation on content. 344 * @param contentToEdit The content edit 345 * @param refContent The referenced content to be added 346 * @param metadataName The name of the metadata holding the relationship 347 */ 348 protected void setJCRReference(WorkflowAwareContent contentToEdit, Content refContent, String metadataName) 349 { 350 ModifiableCompositeMetadata metadataHolder = contentToEdit.getMetadataHolder(); 351 String[] values = metadataHolder.getStringArray(metadataName, new String[0]); 352 353 if (!ArrayUtils.contains(values, refContent.getId())) 354 { 355 String[] newValues = ArrayUtils.add(values, refContent.getId()); 356 357 List<Content> newContents = Arrays.asList(newValues).stream() 358 .map(id -> (Content) _resolver.resolveById(id)) 359 .collect(Collectors.toList()); 360 361 // Set Jcr content reference if we pass contents in arguments 362 ExternalizableMetadataHelper.setMetadata(metadataHolder, metadataName, newContents.toArray(new Content[newContents.size()])); 363 } 364 } 365 366 /** 367 * Apply changes to the content 368 * @param content the content changed 369 * @param actionId the workflow action id 370 * @throws WorkflowException if an error occurred 371 */ 372 protected void _applyChanges(WorkflowAwareContent content, int actionId) throws WorkflowException 373 { 374 // Notify listeners 375 Map<String, Object> eventParams = new HashMap<>(); 376 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 377 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams)); 378 379 _contentWorkflowHelper.doAction(content, actionId); 380 } 381 382 /** 383 * Commit all changes in solr 384 */ 385 protected void _commitAllChanges() 386 { 387 // Before trying to commit, be sure all the async observers of the current request are finished 388 for (Future future : _observationManager.getFuturesForRequest()) 389 { 390 try 391 { 392 future.get(); 393 } 394 catch (ExecutionException | InterruptedException e) 395 { 396 getLogger().info("An exception occured when calling #get() on Future result of an observer." , e); 397 } 398 } 399 400 // Commit all uncommited changes 401 try 402 { 403 _solrIndexer.commit(); 404 405 getLogger().debug("Deleted contents are now committed into Solr."); 406 } 407 catch (IOException | SolrServerException e) 408 { 409 getLogger().error("Impossible to commit changes", e); 410 } 411 } 412 413 /** 414 * Enumeration for the mode of duplication 415 */ 416 public enum DuplicationMode 417 { 418 /** Duplicate the content only */ 419 SINGLE, 420 /** Duplicate the content and its structure */ 421 STRUCTURE_ONLY, 422 /** Duplicate the content and its structure and its courses */ 423 FULL 424 } 425}