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}