001/* 002 * Copyright 2013 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.cms.content; 017 018import java.io.InputStream; 019import java.io.OutputStream; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.List; 027import java.util.Locale; 028import java.util.Map; 029import java.util.Map.Entry; 030import java.util.Properties; 031import java.util.Set; 032 033import javax.xml.transform.OutputKeys; 034import javax.xml.transform.TransformerFactory; 035import javax.xml.transform.sax.SAXTransformerFactory; 036import javax.xml.transform.sax.TransformerHandler; 037import javax.xml.transform.stream.StreamResult; 038 039import org.apache.avalon.framework.component.Component; 040import org.apache.avalon.framework.service.ServiceException; 041import org.apache.avalon.framework.service.ServiceManager; 042import org.apache.avalon.framework.service.Serviceable; 043import org.apache.cocoon.ProcessingException; 044import org.apache.commons.lang3.BooleanUtils; 045import org.apache.commons.lang3.StringUtils; 046import org.apache.excalibur.xml.sax.ContentHandlerProxy; 047import org.apache.excalibur.xml.sax.SAXParser; 048import org.apache.xml.serializer.OutputPropertiesFactory; 049import org.slf4j.Logger; 050import org.xml.sax.Attributes; 051import org.xml.sax.ContentHandler; 052import org.xml.sax.InputSource; 053import org.xml.sax.SAXException; 054 055import org.ametys.cms.content.CopyReport.CopyMode; 056import org.ametys.cms.content.CopyReport.CopyState; 057import org.ametys.cms.content.references.OutgoingReferences; 058import org.ametys.cms.content.references.OutgoingReferencesExtractor; 059import org.ametys.cms.contenttype.AbstractMetadataSetElement; 060import org.ametys.cms.contenttype.ContentType; 061import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 062import org.ametys.cms.contenttype.ContentTypesHelper; 063import org.ametys.cms.contenttype.MetadataDefinition; 064import org.ametys.cms.contenttype.MetadataDefinitionReference; 065import org.ametys.cms.contenttype.MetadataManager; 066import org.ametys.cms.contenttype.MetadataSet; 067import org.ametys.cms.contenttype.MetadataType; 068import org.ametys.cms.contenttype.RepeaterDefinition; 069import org.ametys.cms.contenttype.RichTextUpdater; 070import org.ametys.cms.repository.Content; 071import org.ametys.cms.repository.DefaultContent; 072import org.ametys.cms.repository.ModifiableContent; 073import org.ametys.cms.repository.WorkflowAwareContent; 074import org.ametys.cms.workflow.AbstractContentWorkflowComponent; 075import org.ametys.cms.workflow.CreateContentFunction; 076import org.ametys.cms.workflow.EditContentFunction; 077import org.ametys.cms.workflow.copy.CreateContentByCopyFunction; 078import org.ametys.cms.workflow.copy.EditContentByCopyFunction; 079import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 080import org.ametys.plugins.explorer.resources.ResourceCollection; 081import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory; 082import org.ametys.plugins.repository.AmetysObject; 083import org.ametys.plugins.repository.AmetysObjectIterable; 084import org.ametys.plugins.repository.AmetysObjectResolver; 085import org.ametys.plugins.repository.AmetysRepositoryException; 086import org.ametys.plugins.repository.CopiableAmetysObject; 087import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 088import org.ametys.plugins.repository.TraversableAmetysObject; 089import org.ametys.plugins.repository.metadata.BinaryMetadata; 090import org.ametys.plugins.repository.metadata.CompositeMetadata; 091import org.ametys.plugins.repository.metadata.File; 092import org.ametys.plugins.repository.metadata.Folder; 093import org.ametys.plugins.repository.metadata.ModifiableBinaryMetadata; 094import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 095import org.ametys.plugins.repository.metadata.ModifiableFile; 096import org.ametys.plugins.repository.metadata.ModifiableFolder; 097import org.ametys.plugins.repository.metadata.ModifiableResource; 098import org.ametys.plugins.repository.metadata.ModifiableRichText; 099import org.ametys.plugins.repository.metadata.MultilingualString; 100import org.ametys.plugins.repository.metadata.Resource; 101import org.ametys.plugins.repository.metadata.RichText; 102import org.ametys.plugins.workflow.AbstractWorkflowComponent; 103import org.ametys.plugins.workflow.component.CheckRightsCondition; 104import org.ametys.plugins.workflow.support.WorkflowProvider; 105import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 106import org.ametys.runtime.i18n.I18nizableText; 107import org.ametys.runtime.plugin.component.AbstractLogEnabled; 108 109/** 110 * <p> 111 * This component is used to copy a content (either totally or partially). 112 * </p><p> 113 * In this whole file a Map named <em>copyMap</em> is regularly used. This map 114 * provide the name of the metadata to copy as well as some optional parameters. 115 * It has the following form (JSON) : 116 * </p> 117 * <pre> 118 * { 119 * "$param1": value, 120 * "metadataA": null, 121 * "metadataB": { 122 * "subMetadataB1": null, 123 * "subMetadataB2": { 124 * "$param1": value, 125 * "$param2": value, 126 * "subSubMetadataB21": {...} 127 * }, 128 * ... 129 * } 130 * } 131 * </pre> 132 * <p> 133 * Each metadata that should be copied must be present as a key in the map. 134 * Composite metadata can contains child metadata but as seen on the example the 135 * map must be well structured, it is not a flat map. Parameters in the map must 136 * always start with the reserved character '$', in order to be differentiated 137 * from metadata name. 138 * </p><p> 139 * There are two main entry points for this helper component: 140 * </p> 141 * <ul> 142 * <li>copyContent and editContent are methods that run a dedicated workflow 143 * function (createByCopy or editByCopy) that will later call the 144 * copyMetadataMap function (see below).</li> 145 * <li>copyMetadataMap is the method that is responsible for the recursive copy 146 * of the metadata (by following the copyMap structure). During this process, 147 * underlying content creation can be requested in which case the copyContent 148 * method will be called.</li> 149 * </ul> 150 */ 151public class CopyContentMetadataComponent extends AbstractLogEnabled implements Serviceable, Component 152{ 153 /** Avalon ROLE. */ 154 public static final String ROLE = CopyContentMetadataComponent.class.getName(); 155 156 /** Workflow provider. */ 157 protected WorkflowProvider _workflowProvider; 158 159 /** Ametys object resolver available to subclasses. */ 160 protected AmetysObjectResolver _resolver; 161 162 /** Content type extension point. */ 163 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 164 165 /** Helper for content types */ 166 protected ContentTypesHelper _contentTypesHelper; 167 168 /** The outgoing references extractor */ 169 protected OutgoingReferencesExtractor _outgoingReferencesExtractor; 170 171 /** The content helper */ 172 protected ContentHelper _contentHelper; 173 174 /** Avalon service manager */ 175 protected ServiceManager _manager; 176 177 @Override 178 public void service(ServiceManager manager) throws ServiceException 179 { 180 _manager = manager; 181 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 182 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 183 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 184 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 185 _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE); 186 _outgoingReferencesExtractor = (OutgoingReferencesExtractor) manager.lookup(OutgoingReferencesExtractor.ROLE); 187 } 188 189 /** 190 * Copy a content by creating a new content and copying the value of the 191 * metadata of a source content into the new one. 192 * The title of the new content will the one from the base content. 193 * 194 * @param baseContentId The identifier of the base content 195 * @param copyMap The map of properties as described in 196 * {@link CopyContentMetadataComponent}. Can be null in which 197 * case the map will be constructed from a metadataSet. 198 * @param metadataSetName The name of the metadata set to be used to 199 * construct to copyMap if not provided. This will also be the 200 * default name for possible inner copies (if not provided as a 201 * copyMap parameter). 202 * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist. 203 * @param metadataSetType The type of the metadata set to be used to 204 * construct to copyMap if not provided. This will also be the 205 * default type for possible inner copies. 206 * 207 * @param initActionId The workflow action id to use to create the new content 208 * @param editActionId The workflow action id to use to edit the newly created content 209 * 210 * @return The copy report containing valuable information about the copy 211 * and the possible encountered errors. 212 */ 213 public CopyReport copyContent(String baseContentId, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, int initActionId, int editActionId) 214 { 215 return copyContent(baseContentId, null, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, initActionId, editActionId); 216 } 217 218 /** 219 * Copy a content by creating a new content and copying the value of the 220 * metadata of a source content into the new one. 221 * 222 * @param baseContentId The identifier of the base content 223 * @param title Desired title for the new content. 224 * @param copyMap The map of properties as described in 225 * {@link CopyContentMetadataComponent}. Can be null in which 226 * case the map will be constructed from a metadataSet. 227 * @param metadataSetName The name of the metadata set to be used to 228 * construct to copyMap if not provided. This will also be the 229 * default name for possible inner copies (if not provided as a 230 * copyMap parameter). 231 * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist. 232 * @param metadataSetType The type of the metadata set to be used to 233 * construct to copyMap if not provided. This will also be the 234 * default type for possible inner copies. 235 * @param initActionId The init workflow action id for main content only 236 * @param editActionId The workflow action for editing main content only 237 * @return The copy report containing valuable information about the copy 238 * and the possible encountered errors. 239 */ 240 public CopyReport copyContent(String baseContentId, String title, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, int initActionId, int editActionId) 241 { 242 return copyContent(baseContentId, title, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, null, initActionId, editActionId); 243 } 244 245 /** 246 * Create a new content by copy of another. The type of created content can be different of the source content. 247 * The source and target contents must have a common content type ancestor. 248 * @param baseContentId The id of content to copy 249 * @param title The title of the new created content 250 * @param copyMap The map of metadata to copy. Can be null to copy the whole metadata set. 251 * @param metadataSetName The name of metadata set to copy 252 * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist. 253 * @param metadataSetType The type of metadata set to copy 254 * @param targetContentType The type of content to create. If null the type(s) of created content will be those of base content. 255 * @param initActionId The init workflow action id for main content only 256 * @param editActionId The workflow action for editing main content only 257 * @return The copy report 258 */ 259 public CopyReport copyContent(String baseContentId, String title, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, String targetContentType, int initActionId, int editActionId) 260 { 261 try 262 { 263 Content baseContent = _resolver.resolveById(baseContentId); 264 return copyContent(baseContent, title, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, targetContentType, initActionId, editActionId); 265 } 266 catch (AmetysRepositoryException e) 267 { 268 getLogger().error("An error has been encountered during the content copy, or the copy is not allowed (base content identifier : " + baseContentId + ").", e); 269 270 boolean isSimple = true; 271 272 CopyReport report = new CopyReport(baseContentId, isSimple, metadataSetName, fallbackMetadataSetName, metadataSetType, CopyMode.CREATION); 273 report.notifyContentCopyError(); 274 275 return report; 276 } 277 } 278 279 /** 280 * Copy a content by creating a new content and copying the value of the 281 * metadata of a source content into the new one. 282 * 283 * @param baseContent The base content. 284 * @param copyMap The map of properties as described in 285 * {@link CopyContentMetadataComponent}. Can be null in which 286 * case the map will be constructed from a metadataSet. 287 * @param metadataSetName The name of the metadata set to be used to 288 * construct to copyMap if not provided. This will also be the 289 * default name for possible inner copies (if not provided as a 290 * copyMap parameter). 291 * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist. 292 * @param metadataSetType The type of the metadata set to be used to 293 * construct to copyMap if not provided. This will also be the 294 * default type for possible inner copies. 295 * @return The copy report containing valuable information about the copy 296 * and the possible encountered errors. 297 */ 298 public CopyReport copyContent(Content baseContent, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType) 299 { 300 return copyContent(baseContent, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, getDefaultInitActionId(), getDefaultActionIdForEditingContentReferences()); 301 } 302 303 /** 304 * Copy a content by creating a new content and copying the value of the 305 * metadata of a source content into the new one. 306 * 307 * @param baseContent The base content. 308 * @param copyMap The map of properties as described in 309 * {@link CopyContentMetadataComponent}. Can be null in which 310 * case the map will be constructed from a metadataSet. 311 * @param metadataSetName The name of the metadata set to be used to 312 * construct to copyMap if not provided. This will also be the 313 * default name for possible inner copies (if not provided as a 314 * copyMap parameter). 315 * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist. 316 * @param metadataSetType The type of the metadata set to be used to 317 * construct to copyMap if not provided. This will also be the 318 * default type for possible inner copies. 319 * @param initActionId The init workflow action id for main content only 320 * @param editRefActionId The workflow action for editing references 321 * @return The copy report containing valuable information about the copy 322 * and the possible encountered errors. 323 */ 324 public CopyReport copyContent(Content baseContent, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, int initActionId, int editRefActionId) 325 { 326 return copyContent(baseContent, null, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, initActionId, editRefActionId); 327 } 328 329 /** 330 * Copy a content by creating a new content and copying the value of the 331 * metadata of a source content into the new one. 332 * 333 * @param baseContent The base content. 334 * @param title Desired title for the new content. 335 * @param copyMap The map of properties as described in 336 * {@link CopyContentMetadataComponent}. Can be null in which 337 * case the map will be constructed from a metadataSet. 338 * @param metadataSetName The name of the metadata set to be used to 339 * construct to copyMap if not provided. This will also be the 340 * default name for possible inner copies (if not provided as a 341 * copyMap parameter). 342 * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist. 343 * @param metadataSetType The type of the metadata set to be used to 344 * construct to copyMap if not provided. This will also be the 345 * default type for possible inner copies. 346 * @param initActionId The init workflow action id for main content only 347 * @param editRefActionId The workflow action for editing references 348 * @return The copy report containing valuable information about the copy 349 * and the possible encountered errors. 350 */ 351 public CopyReport copyContent(Content baseContent, String title, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, int initActionId, int editRefActionId) 352 { 353 return copyContent(baseContent, title, copyMap, metadataSetName, metadataSetName, fallbackMetadataSetName, null, initActionId, editRefActionId); 354 } 355 356 /** 357 * Copy a content by creating a new content and copying the value of the 358 * metadata of a source content into the new one. 359 * 360 * @param baseContent The base content. 361 * @param title Desired title for the new content. 362 * @param copyMap The map of properties as described in 363 * {@link CopyContentMetadataComponent}. Can be null in which 364 * case the map will be constructed from a metadataSet. 365 * @param metadataSetName The name of the metadata set to be used to 366 * construct to copyMap if not provided. This will also be the 367 * default name for possible inner copies (if not provided as a 368 * copyMap parameter). 369 * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist. 370 * @param metadataSetType The type of the metadata set to be used to 371 * construct to copyMap if not provided. This will also be the 372 * default type for possible inner copies. 373 * @param targetContentType The type of content to create. If null the type(s) of created content will be those of base content. 374 * @param initActionId The init workflow action id for main content only 375 * @param editRefActionId The workflow action for editing references 376 * @return The copy report containing valuable information about the copy 377 * and the possible encountered errors. 378 */ 379 public CopyReport copyContent(Content baseContent, String title, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, String targetContentType, int initActionId, int editRefActionId) 380 { 381 return copyContent(baseContent, title, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, targetContentType, null, null, initActionId, editRefActionId); 382 } 383 384 /** 385 * Copy a content by creating a new content and copying the value of the 386 * metadata of a source content into the new one. 387 * 388 * @param baseContent The base content. 389 * @param title Desired title for the new content. 390 * @param copyMap The map of properties as described in 391 * {@link CopyContentMetadataComponent}. Can be null in which 392 * case the map will be constructed from a metadataSet. 393 * @param metadataSetName The name of the metadata set to be used to 394 * construct to copyMap if not provided. This will also be the 395 * default name for possible inner copies (if not provided as a 396 * copyMap parameter). 397 * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist. 398 * @param metadataSetType The type of the metadata set to be used to 399 * construct to copyMap if not provided. This will also be the 400 * default type for possible inner copies. 401 * @param parentContentId The target content ID. 402 * @param parentMetadataPath the parent metadata path, if a sub-content is being created. 403 * @param initActionId The init workflow action id for main content only 404 * @param editActionId The workflow action for editing main content only 405 * @return The copy report containing valuable information about the copy 406 * and the possible encountered errors. 407 */ 408 public CopyReport copyContent(Content baseContent, String title, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, String parentContentId, String parentMetadataPath, int initActionId, int editActionId) 409 { 410 return copyContent(baseContent, title, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, null, parentContentId, parentMetadataPath, initActionId, editActionId); 411 } 412 413 /** 414 * Copy a content by creating a new content and copying the value of the 415 * metadata of a source content into the new one. 416 * 417 * @param baseContent The base content. 418 * @param title Desired title for the new content. 419 * @param copyMap The map of properties as described in 420 * {@link CopyContentMetadataComponent}. Can be null in which 421 * case the map will be constructed from a metadataSet. 422 * @param metadataSetName The name of the metadata set to be used to 423 * construct to copyMap if not provided. This will also be the 424 * default name for possible inner copies (if not provided as a 425 * copyMap parameter). 426 * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist. 427 * @param metadataSetType The type of the metadata set to be used to 428 * construct to copyMap if not provided. This will also be the 429 * default type for possible inner copies. 430 * @param targetContentType The type of content to create. If null the type(s) of created content will be those of base content. 431 * @param parentContentId The target content ID. 432 * @param parentMetadataPath the parent metadata path, if a sub-content is being created. 433 * @param initActionId The init workflow action id for main content only 434 * @param editRefActionId The workflow action for editing references 435 * @return The copy report containing valuable information about the copy 436 * and the possible encountered errors. 437 */ 438 public CopyReport copyContent(Content baseContent, String title, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, String targetContentType, String parentContentId, String parentMetadataPath, int initActionId, int editRefActionId) 439 { 440 String baseContentId = baseContent.getId(); 441 String auxMetadataSetName = StringUtils.defaultIfEmpty(metadataSetName, "default-edition"); 442 String auxFallbackMetadataSetName = StringUtils.defaultIfEmpty(fallbackMetadataSetName, "main"); 443 String auxMetadataSetType = StringUtils.defaultIfEmpty(metadataSetType, "edition"); 444 CopyReport report = new CopyReport(baseContentId, _contentHelper.getTitle(baseContent), true, auxMetadataSetName, auxFallbackMetadataSetName, auxMetadataSetType, CopyMode.CREATION); 445 446 try 447 { 448 report.setReferenceTable(_contentHelper.isReferenceTable(baseContent)); 449 450 Map<String, Object> internalCopyMap = copyMap; 451 if (internalCopyMap == null || BooleanUtils.isTrue((Boolean) internalCopyMap.get("$forceBuild"))) 452 { 453 internalCopyMap = _buildCopyMap(baseContent, auxMetadataSetName, fallbackMetadataSetName, auxMetadataSetType, targetContentType, internalCopyMap); 454 } 455 456 // Title metadata must never be copied in case of a content copy, the title is set during the content creation. 457 internalCopyMap.remove("title"); 458 459 Map<String, Object> inputs = _getInputsForCopy(baseContent, title, internalCopyMap, targetContentType, parentContentId, parentMetadataPath, report); 460 String workflowName = _getWorklowName(baseContent, inputs); 461 462 // Create the content and copy metadata from the base content. 463 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(); 464 workflow.initialize(workflowName, initActionId, inputs); 465 466 // Manual call to the edit content function to edit the content 467 // references stored by the copy report through the duplication process 468 _runContentReferencesEdition(baseContent, workflow, editRefActionId, true, report); 469 470 report.notifyContentCopySuccess(); 471 } 472 catch (Exception e) 473 { 474 getLogger().error("An error has been encountered during the content copy, or the copy is not allowed (base content identifier : " + baseContentId + ").", e); 475 476 report.notifyContentCopyError(); 477 } 478 479 return report; 480 } 481 482 /** 483 * Retrieve the inputs for the copy workflow function. 484 * @param baseContent The content to copy 485 * @param title The title to set 486 * @param copyMap The map with properties to copy 487 * @param targetContentType The type of content to create. If null the type(s) of created content will be those of base content. 488 * @param parentContentId The parent content ID, when copying a sub-content. 489 * @param parentMetadataPath The parent metadata path, when copying a sub-content. 490 * @param copyReport The report of the copy 491 * @return The map of inputs. 492 */ 493 @SuppressWarnings("unchecked") 494 protected Map<String, Object> _getInputsForCopy(Content baseContent, String title, Map<String, Object> copyMap, String targetContentType, String parentContentId, String parentMetadataPath, CopyReport copyReport) 495 { 496 Map<String, Object> inputs = new HashMap<>(); 497 498 // Add copy map inputs 499 Map<String, Object> copyMapInputs = (Map<String, Object>) copyMap.get("$inputs"); 500 if (copyMapInputs != null) 501 { 502 inputs.putAll(copyMapInputs); 503 } 504 505 inputs.put(CreateContentByCopyFunction.BASE_CONTENT_KEY, baseContent); 506 inputs.put(CreateContentByCopyFunction.COPY_MAP_KEY, copyMap); 507 inputs.put(CreateContentByCopyFunction.COPY_REPORT_KEY, copyReport); 508 509 if (StringUtils.isNotBlank(parentContentId) && StringUtils.isNotBlank(parentMetadataPath)) 510 { 511 // Provide the parent content ID and metadata path to the CreateContentFunction (removing the leading slash). 512 inputs.put(CreateContentFunction.PARENT_CONTENT_ID_KEY, parentContentId); 513 inputs.put(CreateContentFunction.PARENT_CONTENT_METADATA_PATH_KEY, StringUtils.stripStart(parentMetadataPath, "/")); 514 } 515 516 if (StringUtils.isNoneBlank(title)) 517 { 518 inputs.put(CreateContentFunction.CONTENT_TITLE_KEY, title); 519 } 520 521 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>()); 522 inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>()); 523 524 if (targetContentType != null) 525 { 526 inputs.put(CreateContentFunction.CONTENT_TYPES_KEY, new String[] {targetContentType}); 527 } 528 529 return inputs; 530 } 531 532 /** 533 * Retrieve the workflow name of a content. 534 * @param content The content to consider 535 * @param inputs The inputs that will be provided to the workflow function 536 * @return The name of the workflow. 537 * @throws IllegalArgumentException if the content is not workflow aware. 538 */ 539 protected String _getWorklowName(Content content, Map<String, Object> inputs) throws IllegalArgumentException 540 { 541 String workflowName = null; 542 543 if (content instanceof WorkflowAwareContent) 544 { 545 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 546 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 547 workflowName = workflow.getWorkflowName(waContent.getWorkflowId()); 548 } 549 550 if (workflowName == null) 551 { 552 String errorMsg = "Unable to retrieve the workflow name for the content with identifier '" + content.getId() + "'."; 553 554 getLogger().error(errorMsg); 555 throw new IllegalArgumentException(errorMsg); 556 } 557 558 return workflowName; 559 } 560 561 /** 562 * Build the copy map from given content and metadata set. 563 * @param baseContent the content to copy 564 * @param metadataSetName Name of the metadata set. 565 * @param fallbackMetadataSetName The fallback metadata set name if 'metadataSetName' is not found 566 * @param metadataSetType Type of the metadata set. 567 * @return A map with properties to copy. 568 */ 569 public Map<String, Object> buildCopyMap (Content baseContent, String metadataSetName, String fallbackMetadataSetName, String metadataSetType) 570 { 571 return _buildCopyMap(baseContent, metadataSetName, fallbackMetadataSetName, metadataSetType, new HashMap<String, Object>()); 572 } 573 574 /** 575 * Build the copy map from a metadata set. 576 * @param baseContent the content to copy 577 * @param metadataSetName Name of the metadata set. 578 * @param fallbackMetadataSetName The fallback metadata set name if 'metadataSetName' is not found 579 * @param metadataSetType Type of the metadata set. 580 * @param baseCopyMap The copy map being constructed 581 * @return The copy map. 582 */ 583 protected Map<String, Object> _buildCopyMap(Content baseContent, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, Map<String, Object> baseCopyMap) 584 { 585 return _buildCopyMap(baseContent, metadataSetName, fallbackMetadataSetName, metadataSetType, null, baseCopyMap); 586 } 587 588 /** 589 * Build the copy map from a metadata set. 590 * @param baseContent The content to be copied 591 * @param metadataSetName Name of the metadata set to be copied. 592 * @param fallbackMetadataSetName The fallback metadata set name if 'metadataSetName' is not found 593 * @param metadataSetType Type of the metadata set to be copied (view or edition). 594 * @param targetContentType The content type of content to create. Can be null. 595 * @param baseCopyMap The copy map being constructed 596 * @return The copy map. 597 */ 598 protected Map<String, Object> _buildCopyMap(Content baseContent, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, String targetContentType, Map<String, Object> baseCopyMap) 599 { 600 boolean isEditionMetadataSet = !"view".equals(metadataSetType); 601 602 MetadataSet metadataSet = null; 603 if (targetContentType != null) 604 { 605 Set<String> types = new HashSet<>(); 606 Collections.addAll(types, baseContent.getTypes()); 607 types.add(targetContentType); 608 Set<String> commonAncestors = _contentTypesHelper.getCommonAncestors(types); 609 610 metadataSet = _contentTypesHelper.getMetadataSetWithFallback(metadataSetName, fallbackMetadataSetName, commonAncestors.toArray(new String[commonAncestors.size()]), new String[0], isEditionMetadataSet); 611 } 612 else 613 { 614 metadataSet = _contentTypesHelper.getMetadataSetWithFallback(metadataSetName, fallbackMetadataSetName, baseContent.getTypes(), baseContent.getMixinTypes(), isEditionMetadataSet); 615 } 616 617 return _buildCopyMap(metadataSet, baseCopyMap); 618 } 619 620 /** 621 * Recursive auxiliary function used to build the copy map. 622 * @param metadataSetElement The current metadata set element 623 * @param copyMap The copy map being constructed or null 624 * @return The copy map corresponding to this metadata set element. 625 */ 626 protected Map<String, Object> _buildCopyMap(AbstractMetadataSetElement metadataSetElement, Map<String, Object> copyMap) 627 { 628 Map<String, Object> map = copyMap; 629 630 for (AbstractMetadataSetElement metadataElement : metadataSetElement.getElements()) 631 { 632 if (metadataElement instanceof MetadataDefinitionReference) 633 { 634 MetadataDefinitionReference metadataDefRef = (MetadataDefinitionReference) metadataElement; 635 String name = metadataDefRef.getMetadataName(); 636 637 map = map != null ? map : new HashMap<>(); 638 map.put(name, _buildCopyMap(metadataDefRef, null)); 639 } 640 else 641 { 642 map = map != null ? map : new HashMap<>(); 643 map.putAll(_buildCopyMap(metadataElement, map)); 644 } 645 } 646 647 return map; 648 } 649 650 /** 651 * Edit a content by copying the value of the 652 * metadata of a source content into a target content. 653 * 654 * @param baseContentId The identifier of the base content 655 * @param targetContentId The identifier of the target content 656 * @param copyMap The map of properties as described in 657 * {@link CopyContentMetadataComponent}. Can be null in which 658 * case the map will be constructed from a metadataSet. 659 * @param metadataSetName The name of the metadata set to be used to 660 * construct to copyMap if not provided. This will also be the 661 * default name for possible inner copies (if not provided as a 662 * copyMap parameter). 663 * @param fallbackMetadataSetName The fallback metadata set name if 'metadataSetName' is not found 664 * @param metadataSetType The type of the metadata set to be used to 665 * construct to copyMap if not provided. This will also be the 666 * default type for possible inner copies. 667 * @return The copy report containing valuable information about the copy 668 * and the possible encountered errors. 669 */ 670 public CopyReport editContent(String baseContentId, String targetContentId, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType) 671 { 672 return editContent(baseContentId, targetContentId, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, getDefaultActionIdForContentEdition(), getDefaultActionIdForEditingContentReferences()); 673 } 674 675 /** 676 * Edit a content by copying the value of the 677 * metadata of a source content into a target content. 678 * 679 * @param baseContentId The identifier of the base content 680 * @param targetContentId The identifier of the target content 681 * @param copyMap The map of properties as described in 682 * {@link CopyContentMetadataComponent}. Can be null in which 683 * case the map will be constructed from a metadataSet. 684 * @param metadataSetName The name of the metadata set to be used to 685 * construct to copyMap if not provided. This will also be the 686 * default name for possible inner copies (if not provided as a 687 * copyMap parameter). 688 * @param fallbackMetadataSetName The fallback metadata set name if 'metadataSetName' is not found 689 * @param metadataSetType The type of the metadata set to be used to 690 * construct to copyMap if not provided. This will also be the 691 * default type for possible inner copies. 692 * @param actionId the edit workflow action id 693 * @param editRefActionId the workflow action id for editing references 694 * @return The copy report containing valuable information about the copy 695 * and the possible encountered errors. 696 */ 697 public CopyReport editContent(String baseContentId, String targetContentId, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, int actionId, int editRefActionId) 698 { 699 Content baseContent = null; 700 CopyReport report = null; 701 702 String auxMetadataSetName = StringUtils.defaultIfEmpty(metadataSetName, "default-edition"); 703 String auxFallbackMetadataSetName = StringUtils.defaultIfEmpty(fallbackMetadataSetName, "main"); 704 String auxMetadataSetType = StringUtils.defaultIfEmpty(metadataSetType, "edition"); 705 706 try 707 { 708 baseContent = _resolver.resolveById(baseContentId); 709 report = new CopyReport(baseContentId, _contentHelper.getTitle(baseContent), _contentHelper.isReferenceTable(baseContent), auxMetadataSetName, fallbackMetadataSetName, auxMetadataSetType, CopyMode.EDITION); 710 711 Map<String, Object> internalCopyMap = copyMap; 712 if (internalCopyMap == null || BooleanUtils.isTrue((Boolean) internalCopyMap.get("$forceBuild"))) 713 { 714 internalCopyMap = _buildCopyMap(baseContent, auxMetadataSetName, auxFallbackMetadataSetName, auxMetadataSetType, internalCopyMap); 715 } 716 717 WorkflowAwareContent targetContent = _retrieveTargetContent(targetContentId); 718 Map<String, Object> inputs = _getInputsForEdition(baseContent, targetContent, internalCopyMap, report); 719 720 // Edit the content by copying metadata from the base content. 721 // This is done in a workflow function. 722 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(targetContent); 723 workflow.doAction(targetContent.getWorkflowId(), actionId, inputs); 724 725 // Manual call to the edit content function to edit the content 726 // references stored by the copy report through the duplication process 727 _runContentReferencesEdition(baseContent, workflow, editRefActionId, false, report); 728 729 report.notifyContentCopySuccess(); 730 } 731 catch (Exception e) 732 { 733 getLogger().error( 734 "An error has been encountered during the content edition, or the edition is not allowed (base content identifier : " + baseContentId 735 + ", target content identifier : " + targetContentId + ").", e); 736 737 if (report != null) 738 { 739 report.notifyContentCopyError(); 740 } 741 else 742 { 743 report = new CopyReport(baseContentId, _contentHelper.isReferenceTable(baseContent), auxMetadataSetName, fallbackMetadataSetName, auxMetadataSetType, CopyMode.EDITION); 744 report.notifyContentCopyError(); 745 } 746 } 747 748 return report; 749 } 750 751 /** 752 * Retrieve the target content from its id. 753 * Also ensure that it is a workflow aware content. 754 * @param targetContentId The target content identifer. 755 * @return the retrieved workflow aware content 756 * @throws IllegalArgumentException if the content is not workflow aware. 757 */ 758 protected WorkflowAwareContent _retrieveTargetContent(String targetContentId) throws IllegalArgumentException 759 { 760 Content content = _resolver.resolveById(targetContentId); 761 762 if (!(content instanceof WorkflowAwareContent)) 763 { 764 throw new IllegalArgumentException("Content with identifier '" + targetContentId + "' is not workflow aware."); 765 } 766 767 return (WorkflowAwareContent) content; 768 } 769 770 /** 771 * Retrieve the inputs for the edition workflow function. 772 * @param baseContent The content to copy 773 * @param targetContent The target of the copy 774 * @param copyMap The properties to copy 775 * @param copyReport The report of the copy 776 * @return The map of inputs. 777 */ 778 protected Map<String, Object> _getInputsForEdition(Content baseContent, WorkflowAwareContent targetContent, Map<String, Object> copyMap, CopyReport copyReport) 779 { 780 Map<String, Object> inputs = new HashMap<>(); 781 782 // Add copy map inputs 783 @SuppressWarnings("unchecked") 784 Map<String, Object> copyMapInputs = (Map<String, Object>) copyMap.get("$inputs"); 785 if (copyMapInputs != null) 786 { 787 inputs.putAll(copyMapInputs); 788 } 789 790 inputs.put(EditContentByCopyFunction.BASE_CONTENT_KEY, baseContent); 791 inputs.put(EditContentByCopyFunction.COPY_MAP_KEY, copyMap); 792 inputs.put(EditContentByCopyFunction.COPY_REPORT_KEY, copyReport); 793 794 inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, targetContent); 795 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>()); 796 inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>()); 797 798 return inputs; 799 } 800 801 802 /* ******************************* 803 * * 804 * Copy of the metadata * 805 * * 806 * ******************************/ 807 808 /** 809 * Copy the specified set of metadata from a base content to a target content by iterating over the copyMap. 810 * 811 * @param baseContent The original content 812 * @param targetContent The copy content 813 * @param copyMap The properties to copy 814 * @param copyReport The copy report being populated during the copy. 815 */ 816 @SuppressWarnings("unchecked") 817 public void copyMetadataMap(Content baseContent, ModifiableContent targetContent, Map<String, Object> copyMap, CopyReport copyReport) 818 { 819 copyReport.notifyContentCreation(targetContent.getId(), _contentHelper.getTitle(targetContent), _contentHelper.isReferenceTable(baseContent)); 820 821 Map<String, Object> innerCopyMapInputs = copyMap != null ? (Map<String, Object>) copyMap.get("$inputs") : null; 822 _copyMetadataMap(baseContent, baseContent.getMetadataHolder(), targetContent.getMetadataHolder(), "", null, copyMap, innerCopyMapInputs, copyReport); 823 824 copyReport.setTargetContentTitle(_contentHelper.getTitle(targetContent)); 825 826 _updateRichTextMetadata(baseContent, targetContent, copyReport); 827 _extractOutgoingReferences(targetContent); 828 } 829 830 /** 831 * Copy the specified set of metadata from a base composite metadata to a target composite metadata by iterating over the copyMap. 832 * @param baseContent The original content 833 * @param baseMetaHolder The metadata holder of the baseContent 834 * @param targetMetaHolder The metadata holder of the target content 835 * @param metaPrefix The metadata prefix. 836 * @param parentMetadataDefinition The parent metadata definition. Can be null 837 * @param copyMap The properties to copy 838 * @param innerCopyMapInputs The properties to copy for sub objects 839 * @param copyReport The copy report being populated during the copy. 840 */ 841 @SuppressWarnings("unchecked") 842 protected void _copyMetadataMap(Content baseContent, CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metaPrefix, MetadataDefinition parentMetadataDefinition, Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs, CopyReport copyReport) 843 { 844 if (copyMap == null) 845 { 846 return; 847 } 848 849 for (String metadataName : copyMap.keySet()) 850 { 851 // Ignore key starting with the $ (denotes a parameter) 852 if (StringUtils.startsWith(metadataName, "$")) 853 { 854 continue; 855 } 856 857 MetadataDefinition metadataDefinition = null; 858 try 859 { 860 metadataDefinition = _getMetadataDefinition(baseContent, parentMetadataDefinition, metadataName); 861 if (metadataDefinition != null) 862 { 863 copyMetadata(baseContent, baseMetaHolder, targetMetaHolder, metadataDefinition, metaPrefix, (Map<String, Object>) copyMap.get(metadataName), innerCopyMapInputs, copyReport); 864 } 865 } 866 catch (Exception e) 867 { 868 _reportMetadataException(baseMetaHolder, targetMetaHolder, metadataName, metadataDefinition, copyReport, e); 869 } 870 } 871 } 872 873 /** 874 * Retrieves a {@link MetadataDefinition} through its 875 * parent definition or the {@link ContentType} of the 876 * current content if at the root level of the metadataset. 877 * @param content The content 878 * @param metadataDefinition The parent metadata definition. Can be null. 879 * @param metadataName The metadata name 880 * @return The retrieved metadata defintion or null if not found 881 */ 882 protected MetadataDefinition _getMetadataDefinition(Content content, MetadataDefinition metadataDefinition, String metadataName) 883 { 884 if (metadataDefinition != null) 885 { 886 return metadataDefinition.getMetadataDefinition(metadataName); 887 } 888 else 889 { 890 return _contentTypesHelper.getMetadataDefinition(metadataName, content.getTypes(), content.getMixinTypes()); 891 } 892 } 893 894 /** 895 * Add a metadata exception to the report. 896 * @param baseMetaHolder The metadata holder to copy 897 * @param targetMetaHolder The metadata holder where to copy 898 * @param metadataName The metadata to copy 899 * @param metadataDefinition The associated definition 900 * @param copyReport The report of the copy 901 * @param e The raised exception. 902 */ 903 protected void _reportMetadataException(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName, MetadataDefinition metadataDefinition, CopyReport copyReport, Exception e) 904 { 905 String type = metadataDefinition != null ? metadataDefinition.getType().toString() : null; 906 907 String errorMsg = "Copy of the metadata '" + metadataName + "' of type '" + (type != null ? type : "unknown type") + "' has failed."; 908 getLogger().error(errorMsg, e); 909 910 copyReport.notifyMetadataCopyError(metadataDefinition != null ? metadataDefinition.getLabel() : new I18nizableText(metadataName)); 911 } 912 913 /** 914 * Copy the specified metadata from a base composite metadata to a target composite metadata. 915 * @param baseContent The original content 916 * @param baseMetaHolder The metadata holder to copy 917 * @param targetMetaHolder The metadata holder where to copy 918 * @param metadataDefinition The associated definition 919 * @param metaPrefix The metadata prefix. 920 * @param copyMap The properties to copy 921 * @param innerCopyMapInputs The properties to copy for sub objects 922 * @param copyReport The report of the copy 923 */ 924 public void copyMetadata(Content baseContent, CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDefinition, String metaPrefix, Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs, CopyReport copyReport) 925 { 926 String metadataName = metadataDefinition.getName(); 927 String metadataPath = metaPrefix + "/" + metadataName; 928 929 switch (metadataDefinition.getType()) 930 { 931 case GEOCODE: 932 copyGeocodeMetadata(baseMetaHolder, targetMetaHolder, metadataName); 933 break; 934 case USER: 935 copyUserMetadata(baseMetaHolder, targetMetaHolder, metadataName); 936 break; 937 case MULTILINGUAL_STRING: 938 copyMultilingualStringMetadata(baseMetaHolder, targetMetaHolder, metadataName); 939 break; 940 case REFERENCE: 941 copyReferenceMetadata(baseMetaHolder, targetMetaHolder, metadataDefinition, metadataName); 942 break; 943 case BINARY: 944 copyBinaryMetadata(baseMetaHolder, targetMetaHolder, metadataName); 945 break; 946 case FILE: 947 copyFileMetadata(baseMetaHolder, targetMetaHolder, metadataDefinition, metadataName); 948 break; 949 case RICH_TEXT: 950 copyRichTextMetadata(baseMetaHolder, targetMetaHolder, metadataDefinition, metadataName, copyReport); 951 break; 952 case CONTENT: 953 copyContentReferenceMetadata(baseMetaHolder, targetMetaHolder, metadataDefinition, metadataPath, copyMap, innerCopyMapInputs, copyReport); 954 break; 955 case SUB_CONTENT: 956 copySubContentMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataPath, copyMap, innerCopyMapInputs, copyReport); 957 break; 958 case COMPOSITE: 959 copyCompositeMetadata(baseContent, baseMetaHolder, targetMetaHolder, metadataPath, metadataDefinition, copyMap, innerCopyMapInputs, copyReport); 960 break; 961 default: 962 copyBasicMetadata(baseMetaHolder, targetMetaHolder, metadataDefinition, metadataName); 963 break; 964 } 965 } 966 967 /** 968 * Copy a 'basic' metadata. 969 * This is used to copy metadata of type : 970 * {@link MetadataType#STRING}, {@link MetadataType#DATE}, {@link MetadataType#DATETIME}, 971 * {@link MetadataType#DOUBLE}, {@link MetadataType#LONG}, {@link MetadataType#BOOLEAN}, 972 * {@link MetadataType#USER} 973 * 974 * @param baseMetaHolder The metadata holder to copy 975 * @param targetMetaHolder The metadata holder where to copy 976 * @param metadataDef The metadata definition 977 * @param metadataName The metadata name 978 */ 979 public void copyBasicMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDef, String metadataName) 980 { 981 if (baseMetaHolder.hasMetadata(metadataName)) 982 { 983 switch (metadataDef.getType()) 984 { 985 case DATE: 986 case DATETIME: 987 _setDateMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataDef.isMultiple()); 988 break; 989 case DOUBLE: 990 _setDoubleMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataDef.isMultiple()); 991 break; 992 case LONG: 993 _setLongMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataDef.isMultiple()); 994 break; 995 case BOOLEAN: 996 _setBooleanMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataDef.isMultiple()); 997 break; 998 case STRING: 999 case USER: 1000 default: 1001 _setStringMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataDef.isMultiple()); 1002 break; 1003 } 1004 } 1005 else if (targetMetaHolder.hasMetadata(metadataName)) 1006 { 1007 targetMetaHolder.removeMetadata(metadataName); 1008 } 1009 } 1010 1011 /** 1012 * Set a metadata of type string from a base composite metadata to a target composite metadata. 1013 * @param baseMetaHolder The metadata holder to copy 1014 * @param targetMetaHolder The metadata holder where to copy 1015 * @param metadataName The metadata to consider 1016 * @param isMultiple Is the metadata multiple 1017 */ 1018 protected void _setStringMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName, Boolean isMultiple) 1019 { 1020 if (isMultiple) 1021 { 1022 targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getStringArray(metadataName)); 1023 } 1024 else 1025 { 1026 targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getString(metadataName)); 1027 } 1028 } 1029 1030 /** 1031 * Set a metadata of type date/datetime from a base composite metadata to a target composite metadata. 1032 * @param baseMetaHolder The metadata holder to copy 1033 * @param targetMetaHolder The metadata holder where to copy 1034 * @param metadataName The metadata to consider 1035 * @param isMultiple Is the metadata multiple 1036 */ 1037 protected void _setDateMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName, Boolean isMultiple) 1038 { 1039 if (isMultiple) 1040 { 1041 targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getDateArray(metadataName)); 1042 } 1043 else 1044 { 1045 targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getDate(metadataName)); 1046 } 1047 } 1048 1049 /** 1050 * Set a metadata of type double from a base composite metadata to a target composite metadata. 1051 * @param baseMetaHolder The metadata holder to copy 1052 * @param targetMetaHolder The metadata holder where to copy 1053 * @param metadataName The metadata to consider 1054 * @param isMultiple Is the metadata multiple 1055 */ 1056 protected void _setDoubleMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName, Boolean isMultiple) 1057 { 1058 if (isMultiple) 1059 { 1060 targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getDoubleArray(metadataName)); 1061 } 1062 else 1063 { 1064 targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getDouble(metadataName)); 1065 } 1066 } 1067 1068 /** 1069 * Set a metadata of type long from a base composite metadata to a target composite metadata. 1070 * @param baseMetaHolder The metadata holder to copy 1071 * @param targetMetaHolder The metadata holder where to copy 1072 * @param metadataName The metadata to consider 1073 * @param isMultiple Is the metadata multiple 1074 */ 1075 protected void _setLongMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName, Boolean isMultiple) 1076 { 1077 if (isMultiple) 1078 { 1079 targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getLongArray(metadataName)); 1080 } 1081 else 1082 { 1083 targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getLong(metadataName)); 1084 } 1085 } 1086 1087 /** 1088 * Set a metadata of type boolean from a base composite metadata to a target composite metadata. 1089 * @param baseMetaHolder The metadata holder to copy 1090 * @param targetMetaHolder The metadata holder where to copy 1091 * @param metadataName The metadata to consider 1092 * @param isMultiple Is the metadata multiple 1093 */ 1094 protected void _setBooleanMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName, Boolean isMultiple) 1095 { 1096 if (isMultiple) 1097 { 1098 targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getBooleanArray(metadataName)); 1099 } 1100 else 1101 { 1102 targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getBoolean(metadataName)); 1103 } 1104 } 1105 1106 /** 1107 * Duplicate a metadata of type {@link MetadataType#GEOCODE}. 1108 * @param baseMetaHolder The metadata holder to copy 1109 * @param targetMetaHolder The metadata holder where to copy 1110 * @param metadataName The metadata to consider 1111 */ 1112 public void copyGeocodeMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName) 1113 { 1114 if (baseMetaHolder.hasMetadata(metadataName)) 1115 { 1116 CompositeMetadata baseGeoCode = baseMetaHolder.getCompositeMetadata(metadataName); 1117 ModifiableCompositeMetadata targetGeoCode = targetMetaHolder.getCompositeMetadata(metadataName, true); 1118 1119 targetGeoCode.setMetadata("longitude", baseGeoCode.getDouble("longitude")); 1120 targetGeoCode.setMetadata("latitude", baseGeoCode.getDouble("latitude")); 1121 } 1122 else if (targetMetaHolder.hasMetadata(metadataName)) 1123 { 1124 targetMetaHolder.removeMetadata(metadataName); 1125 } 1126 } 1127 1128 /** 1129 * Duplicate a metadata of type {@link MetadataType#MULTILINGUAL_STRING}. 1130 * @param baseMetaHolder The metadata holder to copy 1131 * @param targetMetaHolder The metadata holder where to copy 1132 * @param metadataName The metadata to consider 1133 */ 1134 public void copyMultilingualStringMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName) 1135 { 1136 if (baseMetaHolder.hasMetadata(metadataName)) 1137 { 1138 MultilingualString baseMultilingualString = baseMetaHolder.getMultilingualString(metadataName); 1139 for (Locale locale : baseMultilingualString.getLocales()) 1140 { 1141 targetMetaHolder.setMetadata(metadataName, baseMultilingualString.getValue(locale), locale); 1142 } 1143 } 1144 else if (targetMetaHolder.hasMetadata(metadataName)) 1145 { 1146 targetMetaHolder.removeMetadata(metadataName); 1147 } 1148 } 1149 1150 /** 1151 * Duplicate a metadata of type {@link MetadataType#USER}. 1152 * @param baseMetaHolder The metadata holder to copy 1153 * @param targetMetaHolder The metadata holder where to copy 1154 * @param metadataName The metadata to consider 1155 */ 1156 public void copyUserMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName) 1157 { 1158 if (baseMetaHolder.hasMetadata(metadataName)) 1159 { 1160 CompositeMetadata baseUser = baseMetaHolder.getCompositeMetadata(metadataName); 1161 ModifiableCompositeMetadata targetUser = targetMetaHolder.getCompositeMetadata(metadataName, true); 1162 1163 targetUser.setMetadata("login", baseUser.getString("login")); 1164 targetUser.setMetadata("population", baseUser.getString("population")); 1165 } 1166 else if (targetMetaHolder.hasMetadata(metadataName)) 1167 { 1168 targetMetaHolder.removeMetadata(metadataName); 1169 } 1170 } 1171 1172 /** 1173 * Duplicate a metadata of type {@link MetadataType#REFERENCE}. 1174 * @param baseMetaHolder The metadata holder to copy 1175 * @param targetMetaHolder The metadata holder where to copy 1176 * @param metadataName The metadata to consider 1177 * @param metadataDefinition The associated definition 1178 */ 1179 public void copyReferenceMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDefinition, String metadataName) 1180 { 1181 if (baseMetaHolder.hasMetadata(metadataName)) 1182 { 1183 CompositeMetadata baseReferenceMeta = baseMetaHolder.getCompositeMetadata(metadataName); 1184 ModifiableCompositeMetadata targetReferenceMeta = targetMetaHolder.getCompositeMetadata(metadataName, true); 1185 1186 targetReferenceMeta.setMetadata("type", baseReferenceMeta.getString("type")); 1187 targetReferenceMeta.setMetadata("value", baseReferenceMeta.getString("value")); 1188 } 1189 else if (targetMetaHolder.hasMetadata(metadataName)) 1190 { 1191 targetMetaHolder.removeMetadata(metadataName); 1192 } 1193 } 1194 1195 /** 1196 * Duplicate a metadata of type {@link MetadataType#BINARY}. 1197 * @param baseMetaHolder The metadata holder to copy 1198 * @param targetMetaHolder The metadata holder where to copy 1199 * @param metadataName The metadata to consider 1200 */ 1201 public void copyBinaryMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName) 1202 { 1203 if (baseMetaHolder.hasMetadata(metadataName)) 1204 { 1205 BinaryMetadata baseBinaryMetadata = baseMetaHolder.getBinaryMetadata(metadataName); 1206 ModifiableBinaryMetadata targetBinaryMetadata = targetMetaHolder.getBinaryMetadata(metadataName, true); 1207 1208 targetBinaryMetadata.setFilename(baseBinaryMetadata.getFilename()); 1209 targetBinaryMetadata.setMimeType(baseBinaryMetadata.getMimeType()); 1210 targetBinaryMetadata.setLastModified(baseBinaryMetadata.getLastModified()); 1211 targetBinaryMetadata.setEncoding(baseBinaryMetadata.getEncoding()); 1212 targetBinaryMetadata.setInputStream(baseBinaryMetadata.getInputStream()); 1213 } 1214 else if (targetMetaHolder.hasMetadata(metadataName)) 1215 { 1216 targetMetaHolder.removeMetadata(metadataName); 1217 } 1218 } 1219 1220 /** 1221 * Copy a file metadata 1222 * @param baseMetaHolder The copied composite metadata 1223 * @param targetMetaHolder The target composite metadata 1224 * @param metadataDef The metadata definition 1225 * @param metadataName The metadata name 1226 */ 1227 public void copyFileMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDef, String metadataName) 1228 { 1229 if (baseMetaHolder.hasMetadata(metadataName)) 1230 { 1231 // Could be a binary or a string. 1232 if (CompositeMetadata.MetadataType.BINARY.equals(baseMetaHolder.getType(metadataName))) 1233 { 1234 copyBinaryMetadata(baseMetaHolder, targetMetaHolder, metadataName); 1235 } 1236 else 1237 { 1238 targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getString(metadataName)); 1239 copyBasicMetadata(baseMetaHolder, targetMetaHolder, metadataDef, metadataName); 1240 } 1241 } 1242 else if (targetMetaHolder.hasMetadata(metadataName)) 1243 { 1244 targetMetaHolder.removeMetadata(metadataName); 1245 } 1246 } 1247 1248 /** 1249 * Copy a rich-text metadata 1250 * @param baseMetaHolder The copied composite metadata 1251 * @param targetMetaHolder The target composite metadata 1252 * @param metadataName The metadata name 1253 * @param metadataDef The metadata definition 1254 * @param copyReport The copy report 1255 */ 1256 protected void copyRichTextMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDef, String metadataName, CopyReport copyReport) 1257 { 1258 if (baseMetaHolder.hasMetadata(metadataName)) 1259 { 1260 RichText baseRichText = baseMetaHolder.getRichText(metadataName); 1261 ModifiableRichText targetRichText = targetMetaHolder.getRichText(metadataName, true); 1262 1263 // Notify the report that a rich text has been copied. 1264 copyReport.addRichText(targetRichText, metadataDef); 1265 1266 targetRichText.setEncoding(baseRichText.getEncoding()); 1267 targetRichText.setMimeType(baseRichText.getMimeType()); 1268 targetRichText.setLastModified(baseRichText.getLastModified()); 1269 targetRichText.setInputStream(baseRichText.getInputStream()); 1270 1271 // Copy additional data 1272 _copyFolder(baseRichText.getAdditionalDataFolder(), targetRichText.getAdditionalDataFolder()); 1273 } 1274 else if (targetMetaHolder.hasMetadata(metadataName)) 1275 { 1276 targetMetaHolder.removeMetadata(metadataName); 1277 } 1278 } 1279 1280 /** 1281 * Folder copy during the copy of a rich text metadata. 1282 * @param baseFolder The folder to copy 1283 * @param targetFolder The folder where to copy 1284 */ 1285 protected void _copyFolder(Folder baseFolder, ModifiableFolder targetFolder) 1286 { 1287 // Files and folders removal 1288 targetFolder.removeAll(); 1289 1290 // Copy folders 1291 Collection< ? extends Folder> baseSubFolders = baseFolder.getFolders(); 1292 for (Folder baseSubFolder : baseSubFolders) 1293 { 1294 ModifiableFolder targetSubFolder = targetFolder.addFolder(baseSubFolder.getName()); 1295 _copyFolder(baseSubFolder, targetSubFolder); 1296 } 1297 1298 // Copy files 1299 Collection< ? extends File> baseFiles = baseFolder.getFiles(); 1300 for (File baseFile : baseFiles) 1301 { 1302 _copyFile(baseFile, targetFolder); 1303 } 1304 } 1305 1306 /** 1307 * File copy during the copy of a folder. 1308 * @param baseFile The file to copy 1309 * @param targetFolder The folder where to copy 1310 */ 1311 protected void _copyFile(File baseFile, ModifiableFolder targetFolder) 1312 { 1313 ModifiableFile file = targetFolder.addFile(baseFile.getName()); 1314 1315 Resource baseResource = baseFile.getResource(); 1316 ModifiableResource targetResource = file.getResource(); 1317 1318 targetResource.setLastModified(baseResource.getLastModified()); 1319 targetResource.setMimeType(baseResource.getMimeType()); 1320 targetResource.setEncoding(baseResource.getEncoding()); 1321 targetResource.setInputStream(baseResource.getInputStream()); 1322 } 1323 1324 /** 1325 * Duplicate a metadata of type {@link MetadataType#CONTENT}. 1326 * If the copy map has a '$mode' parameter set to 'create', a new content will be created for each referenced content in the base metadata. 1327 * The referenced contents are created if needed but content references are not set here. 1328 * It should be done manually as done in {@link #_runContentReferencesEdition} 1329 * @param baseMetaHolder The metadata holder of the content to copy 1330 * @param targetMetaHolder The metadata holder of the target content 1331 * @param metadataDefinition The definition of the metadata 1332 * @param metadataPath The content metadata path. 1333 * @param copyMap The properties to copy 1334 * @param innerCopyMapInputs The properties of sub objects 1335 * @param copyReport The report of the copy 1336 */ 1337 public void copyContentReferenceMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDefinition, String metadataPath, Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs, CopyReport copyReport) 1338 { 1339 String metadataName = metadataDefinition.getName(); 1340 1341 if (baseMetaHolder.hasMetadata(metadataName)) 1342 { 1343 String[] baseContentIds = baseMetaHolder.getStringArray(metadataName); 1344 String[] targetContentIds = baseContentIds; 1345 1346 if (copyMap != null && StringUtils.equals((String) copyMap.get("$mode"), "create")) 1347 { 1348 targetContentIds = _copyReferencedContents(baseContentIds, copyMap, innerCopyMapInputs, copyReport); 1349 } 1350 1351 if (targetContentIds.length > 0) 1352 { 1353 Object values = metadataDefinition.isMultiple() ? Arrays.asList(targetContentIds) : targetContentIds[0]; 1354 copyReport.addContentReferenceValues(metadataPath, values); 1355 } 1356 else if (targetMetaHolder.hasMetadata(metadataName)) 1357 { 1358 Object values = metadataDefinition.isMultiple() ? Collections.EMPTY_LIST : null; 1359 copyReport.addContentReferenceValues(metadataPath, values); 1360 } 1361 } 1362 else if (targetMetaHolder.hasMetadata(metadataName)) 1363 { 1364 Object values = metadataDefinition.isMultiple() ? Collections.EMPTY_LIST : null; 1365 copyReport.addContentReferenceValues(metadataPath, values); 1366 } 1367 } 1368 1369 /** 1370 * Duplicate base contents by creating new contents. If a specific metadata 1371 * set must be used, a '$metadataSetName' parameter must be specified. If 1372 * the copyMap for each inner duplication is already present in the current 1373 * copyMap, a '$loaded' parameter must be set to true, if not the copyMap 1374 * will be constructed from the requested metadata set. 1375 * @param baseContentIds The ids of contents to copy 1376 * @param copyMap The properties to copy 1377 * @param innerCopyMapInputs The properties of sub objects 1378 * @param copyReport The report of the copy 1379 * @return The array of identifiers of the new contents. 1380 */ 1381 protected String[] _copyReferencedContents(String[] baseContentIds, Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs, CopyReport copyReport) 1382 { 1383 String defaultMetadataSetName = StringUtils.defaultString((String) copyMap.get("$metadataSetName"), copyReport._metadataSetName); 1384 String defaultFallbackMetadataSetName = StringUtils.defaultString((String) copyMap.get("$fallbackMetadataSetName"), copyReport._fallbackMetadataSetName); 1385 String defaultMetadataSetType = copyReport._metadataSetType; 1386 1387 Map<String, Object> innerCopyMap = _handleInnerCopyMap(copyMap, innerCopyMapInputs); 1388 List<String> targetContentIds = new ArrayList<>(); 1389 1390 for (String baseContentId : baseContentIds) 1391 { 1392 CopyReport innerReport = copyContent(baseContentId, innerCopyMap, defaultMetadataSetName, defaultFallbackMetadataSetName, defaultMetadataSetType, getDefaultInitActionId(), getDefaultActionIdForEditingContentReferences()); 1393 1394 if (innerReport._state == CopyState.SUCCESS) 1395 { 1396 targetContentIds.add(innerReport._targetContentId); 1397 } 1398 1399 copyReport.addReport(innerReport); 1400 } 1401 1402 return targetContentIds.toArray(new String[targetContentIds.size()]); 1403 } 1404 1405 /** 1406 * Handle the creation of the inner copy map given the current context. 1407 * The inner copy map will act as the copy map for underlying content copies. 1408 * @param copyMap The properties to copy 1409 * @param innerCopyMapInputs The properties of sub objects 1410 * @return The inner copy map 1411 */ 1412 protected Map<String, Object> _handleInnerCopyMap(Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs) 1413 { 1414 // If the copy map does not contain the data for the inner duplications, 1415 // a null copy map should be passed to force the copy process to build 1416 // it. 1417 // If inner inputs for the copy map are supplied, they will be added to 1418 // the copy map. 1419 // In case of a null copy map, the copy map will no be null anymore but 1420 // a special parameters $forceBuild is added. 1421 1422 Map<String, Object> innerCopyMap = null; 1423 if (copyMap != null && BooleanUtils.isTrue((Boolean) copyMap.get("$loaded"))) 1424 { 1425 innerCopyMap = copyMap; 1426 } 1427 1428 if (innerCopyMapInputs != null) 1429 { 1430 if (innerCopyMap == null) 1431 { 1432 innerCopyMap = new HashMap<>(); 1433 innerCopyMap.put("$forceBuild", true); 1434 } 1435 1436 innerCopyMap.put("$inputs", innerCopyMapInputs); 1437 } 1438 1439 return innerCopyMap; 1440 } 1441 1442 /** 1443 * Duplicate a metadata of type {@link MetadataType#SUB_CONTENT}. 1444 * If the copy map has a '$mode' parameter set to 'create', a new content will be created for each referenced content in the base metadata. 1445 * @param baseMetaHolder The metadata holder of the content to copy 1446 * @param targetMetaHolder The metadata holder of the target content 1447 * @param metadataName The name of the metadata 1448 * @param metadataPath The content metadata path. 1449 * @param copyMap The properties to copy 1450 * @param innerCopyMapInputs The properties of sub objects 1451 * @param copyReport The report of the copy 1452 */ 1453 public void copySubContentMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName, String metadataPath, Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs, CopyReport copyReport) 1454 { 1455 if (baseMetaHolder.hasMetadata(metadataName)) 1456 { 1457 String defaultMetadataSetType = copyReport._metadataSetType; 1458 String defaultMetadataSetName = copyReport._metadataSetName; 1459 String defaultFallbackMetadataSetName = copyReport._fallbackMetadataSetName; 1460 if (copyMap != null && copyMap.containsKey("$metadataSetName")) 1461 { 1462 defaultMetadataSetName = (String) copyMap.get("$metadataSetName"); 1463 } 1464 1465 Map<String, Object> innerCopyMap = _handleInnerCopyMap(copyMap, innerCopyMapInputs); 1466 TraversableAmetysObject objectCollection = baseMetaHolder.getObjectCollection(metadataName); 1467 AmetysObjectIterable<Content> subContents = objectCollection.getChildren(); 1468 1469 for (Content content : subContents) 1470 { 1471 CopyReport innerReport = copyContent(content, null, innerCopyMap, defaultMetadataSetName, defaultFallbackMetadataSetName, defaultMetadataSetType, copyReport.getTargetContentId(), metadataPath, getDefaultInitActionId(), getDefaultActionIdForEditingContentReferences()); 1472 1473 copyReport.addReport(innerReport); 1474 } 1475 } 1476 else if (targetMetaHolder.hasMetadata(metadataName)) 1477 { 1478 targetMetaHolder.removeMetadata(metadataName); 1479 } 1480 } 1481 1482 /** 1483 * Copy composite metadata 1484 * @param baseContent The copied content 1485 * @param baseMetaHolder The original composite metadata 1486 * @param targetMetaHolder The target composite metadata 1487 * @param metadataPath The metadata path 1488 * @param metadataDefinition The metadata definition 1489 * @param copyMap The map for copy 1490 * @param innerCopyMapInputs The properties of sub objects 1491 * @param copyReport The copy report 1492 */ 1493 public void copyCompositeMetadata(Content baseContent, CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataPath, MetadataDefinition metadataDefinition, Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs, CopyReport copyReport) 1494 { 1495 if (metadataDefinition instanceof RepeaterDefinition) 1496 { 1497 _copyRepeater(baseContent, baseMetaHolder, targetMetaHolder, metadataPath, metadataDefinition, copyMap, innerCopyMapInputs, copyReport); 1498 } 1499 else 1500 { 1501 String metadataName = metadataDefinition.getName(); 1502 1503 if (baseMetaHolder.hasMetadata(metadataName)) 1504 { 1505 CompositeMetadata baseCompositeMetadata = baseMetaHolder.getCompositeMetadata(metadataName); 1506 ModifiableCompositeMetadata targetCompositeMetadata = targetMetaHolder.getCompositeMetadata(metadataName, true); 1507 1508 _copyMetadataMap(baseContent, baseCompositeMetadata, targetCompositeMetadata, metadataPath, metadataDefinition, copyMap, innerCopyMapInputs, copyReport); 1509 } 1510 else if (targetMetaHolder.hasMetadata(metadataName)) 1511 { 1512 targetMetaHolder.removeMetadata(metadataName); 1513 } 1514 } 1515 } 1516 1517 /** 1518 * Copy a repeater 1519 * @param baseContent The copied content 1520 * @param baseMetaHolder The original composite metadata 1521 * @param targetMetaHolder The target composite metadata 1522 * @param metadataPath The metadata path 1523 * @param metadataDefinition The metadata definition 1524 * @param copyMap The map for copy 1525 * @param innerCopyMapInputs The properties of sub objects 1526 * @param copyReport The copy report 1527 */ 1528 protected void _copyRepeater(Content baseContent, CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataPath, MetadataDefinition metadataDefinition, Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs, CopyReport copyReport) 1529 { 1530 String metadataName = metadataDefinition.getName(); 1531 1532 if (baseMetaHolder.hasMetadata(metadataName)) 1533 { 1534 CompositeMetadata baseRepeaterMetadata = baseMetaHolder.getCompositeMetadata(metadataName); 1535 ModifiableCompositeMetadata targetRepeaterMetadata = targetMetaHolder.getCompositeMetadata(metadataName, true); 1536 1537 // Find the higher index for the entries of the base repeater 1538 String[] baseEntryNames = baseRepeaterMetadata.getMetadataNames(); 1539 Arrays.sort(baseEntryNames, MetadataManager.REPEATER_ENTRY_COMPARATOR); 1540 1541 int maxEntryName = 0; 1542 for (String entryName : baseEntryNames) 1543 { 1544 if (baseRepeaterMetadata.getType(entryName) == CompositeMetadata.MetadataType.COMPOSITE) 1545 { 1546 maxEntryName = Math.max(maxEntryName, Integer.parseInt(entryName)); 1547 } 1548 } 1549 1550 // Rename every repeater entry to ensure their removal later because index of the entry will be higher that the repeater size. 1551 // Sorted by reverse name order to avoid same name conflicts while renaming entries. 1552 if (maxEntryName > 0) 1553 { 1554 String[] targetEntryNames = targetRepeaterMetadata.getMetadataNames(); 1555 Arrays.sort(targetEntryNames, Collections.reverseOrder(MetadataManager.REPEATER_ENTRY_COMPARATOR)); 1556 1557 for (String entryName : targetEntryNames) 1558 { 1559 if (targetRepeaterMetadata.getType(entryName) == CompositeMetadata.MetadataType.COMPOSITE) 1560 { 1561 // Warning! Rename expects the full jcr name ('ametys:name'), not the metadata name as usual. 1562 // FIXME REPOSITORY-277 1563 targetRepeaterMetadata.getCompositeMetadata(entryName).rename("ametys:" + String.valueOf(maxEntryName + Integer.parseInt(entryName))); 1564 } 1565 } 1566 } 1567 1568 // Duplication base repeater entries 1569 // Fix target entry names if necessary (1,3,4 -> 1,2,3) 1570 int targetEntryName = 1; 1571 for (String entryName : baseEntryNames) 1572 { 1573 if (baseRepeaterMetadata.getType(entryName) == CompositeMetadata.MetadataType.COMPOSITE) 1574 { 1575 CompositeMetadata baseRepeaterEntry = baseRepeaterMetadata.getCompositeMetadata(entryName); 1576 ModifiableCompositeMetadata targetRepeaterEntry = targetRepeaterMetadata.getCompositeMetadata(String.valueOf(targetEntryName), true); 1577 1578 String metaPrefix = metadataPath + "/" + targetEntryName; 1579 1580 _copyMetadataMap(baseContent, baseRepeaterEntry, targetRepeaterEntry, metaPrefix, metadataDefinition, copyMap, innerCopyMapInputs, copyReport); 1581 1582 targetEntryName++; 1583 } 1584 } 1585 1586 // Sync repeater by storing base entry names. Remaining entries of 1587 // the target repeater metadata will be deleted with this (the job 1588 // is done via _runContentReferencesEdition) 1589 copyReport.addRepeaterInfo(metadataPath, baseEntryNames.length); 1590 } 1591 else /* if (targetMetaHolder.hasMetadata(metadataName)) */ 1592 { 1593 // Info are mandatory even if repeater does not currently exists. Otherwise, the EditContentFunction called via _runContentReferencesEdition will fails. 1594 copyReport.addRepeaterInfo(metadataPath, 0); 1595 } 1596 } 1597 1598 /** 1599 * Analyse the content to find outgoing references and store them 1600 * @param content The content to analyze 1601 */ 1602 protected void _extractOutgoingReferences(ModifiableContent content) 1603 { 1604 Map<String, OutgoingReferences> outgoingReferencesByPath = _outgoingReferencesExtractor.getOutgoingReferences(content); 1605 content.setOutgoingReferences(outgoingReferencesByPath); 1606 } 1607 1608 1609 /** 1610 * This method analyzes content rich texts and update their content to 1611 * ensure consistency. (link to image, attachments...) 1612 * @param baseContent The copied content 1613 * @param targetContent The target content 1614 * @param copyReport The copy report 1615 */ 1616 protected void _updateRichTextMetadata(final Content baseContent, final ModifiableContent targetContent, CopyReport copyReport) 1617 { 1618 SAXParser saxParser = null; 1619 try 1620 { 1621 Map<ModifiableRichText, MetadataDefinition> copiedRichTexts = copyReport.getCopiedRichTexts(); 1622 saxParser = (SAXParser) _manager.lookup(SAXParser.ROLE); 1623 1624 for (Entry<ModifiableRichText, MetadataDefinition> entry : copiedRichTexts.entrySet()) 1625 { 1626 ModifiableRichText richText = entry.getKey(); 1627 MetadataDefinition metadataDef = entry.getValue(); 1628 1629 String referenceContentType = metadataDef.getReferenceContentType(); 1630 ContentType contentType = _contentTypeExtensionPoint.getExtension(referenceContentType); 1631 1632 RichTextUpdater richTextUpdater = contentType.getRichTextUpdater(); 1633 if (richTextUpdater != null) 1634 { 1635 Map<String, Object> params = new HashMap<>(); 1636 params.put("initialContent", baseContent); 1637 params.put("createdContent", targetContent); 1638 params.put("initialAO", baseContent); 1639 params.put("createdAO", targetContent); 1640 1641 // create the transformer instance 1642 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 1643 1644 // create the format of result 1645 Properties format = new Properties(); 1646 format.put(OutputKeys.METHOD, "xml"); 1647 format.put(OutputKeys.INDENT, "yes"); 1648 format.put(OutputKeys.ENCODING, "UTF-8"); 1649 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2"); 1650 th.getTransformer().setOutputProperties(format); 1651 1652 // Update rich text contents 1653 // Copy the needed original attachments. 1654 try (InputStream is = richText.getInputStream(); OutputStream os = richText.getOutputStream()) 1655 { 1656 StreamResult result = new StreamResult(os); 1657 th.setResult(result); 1658 1659 ContentHandler richTextHandler = richTextUpdater.getContentHandler(th, th, params); 1660 1661 // Copy attachments handler. 1662 ContentHandlerProxy copyAttachmentsHandler = new CopyAttachmentsHandler(richTextHandler, baseContent, targetContent, copyReport, _resolver, getLogger()); 1663 1664 // Rich text update. 1665 saxParser.parse(new InputSource(is), copyAttachmentsHandler); 1666 } 1667 finally 1668 { 1669 th.getTransformer().reset(); 1670 } 1671 } 1672 } 1673 } 1674 catch (Exception e) 1675 { 1676 getLogger().error( 1677 "An error occurred while updating rich text metadata for content '" + targetContent.getId() + " after copy from initial content '" + baseContent.getId() + "'", 1678 e); 1679 } 1680 finally 1681 { 1682 _manager.release(saxParser); 1683 } 1684 } 1685 1686 /** 1687 * Run the edition of the content references that has been stored during the copy of the metadata. 1688 * It is done via the EditContentFunction at the end of the process for technical purposes. 1689 * @param baseContent The base content to copy 1690 * @param targetContentWorkflow the target content local workflow 1691 * @param editActionId The edit action id to edit references 1692 * @param forceEdition true to force edition regardless user's rights 1693 * @param copyReport the copy report 1694 * @throws Exception If an error occured 1695 */ 1696 protected void _runContentReferencesEdition(Content baseContent, AmetysObjectWorkflow targetContentWorkflow, int editActionId, boolean forceEdition, CopyReport copyReport) throws Exception 1697 { 1698 Map<String, Object> editionValues = new HashMap<>(); 1699 1700 Map<String, Object> contentReferenceValues = copyReport.getContentReferenceValuesMap(); 1701 for (Entry<String, Object> entry : contentReferenceValues.entrySet()) 1702 { 1703 String valueKey = StringUtils.replaceChars(StringUtils.stripStart(entry.getKey(), "/"), '/', '.'); 1704 editionValues.put(EditContentFunction.FORM_ELEMENTS_PREFIX + valueKey, entry.getValue()); 1705 } 1706 1707 Map<String, Integer> repeatersInfo = copyReport.getRepeatersInfo(); 1708 for (Entry<String, Integer> entry : repeatersInfo.entrySet()) 1709 { 1710 String valueKey = StringUtils.replaceChars(StringUtils.stripStart(entry.getKey(), "/"), '/', '.'); 1711 Integer entryCount = entry.getValue(); 1712 1713 editionValues.put(EditContentFunction.FORM_ELEMENTS_PREFIX + valueKey, null); 1714 editionValues.put('_' + EditContentFunction.FORM_ELEMENTS_PREFIX + valueKey + ".size", String.valueOf(entryCount)); 1715 1716 for (int i = 1; i <= entryCount; i++) 1717 { 1718 String pos = String.valueOf(i); 1719 editionValues.put('_' + EditContentFunction.FORM_ELEMENTS_PREFIX + valueKey + "." + i + ".previous-position", pos); 1720 editionValues.put('_' + EditContentFunction.FORM_ELEMENTS_PREFIX + valueKey + "." + i + ".position", pos); 1721 } 1722 } 1723 1724 if (editionValues.size() > 0) 1725 { 1726 WorkflowAwareContent targetContent = targetContentWorkflow.getAmetysObject(); 1727 Map<String, Object> inputs = _getInputsForContentReferencesEdition(targetContent, targetContent, editionValues, copyReport); 1728 if (forceEdition) 1729 { 1730 inputs.put(CheckRightsCondition.FORCE, forceEdition); 1731 } 1732 1733 try 1734 { 1735 targetContentWorkflow.doAction(targetContent.getWorkflowId(), editActionId, inputs); 1736 } 1737 catch (Exception e) 1738 { 1739 throw new ProcessingException("Error during content references edition. (base content identifier : " + baseContent.getId() 1740 + ", target content identifier : " + targetContent.getId() + "). ", e); 1741 } 1742 } 1743 } 1744 1745 /** 1746 * Retrieve the inputs for the content references edition workflow function. 1747 * @param baseContent The content to copy 1748 * @param targetContent The content where to copy 1749 * @param editionValues The values representing the content references to be edited (also contains special repeater size values) 1750 * @param copyReport The report of the copy 1751 * @return The map of inputs. 1752 */ 1753 protected Map<String, Object> _getInputsForContentReferencesEdition(Content baseContent, WorkflowAwareContent targetContent, Map<String, Object> editionValues, CopyReport copyReport) 1754 { 1755 Map<String, Object> inputs = new HashMap<>(); 1756 1757 inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, targetContent); 1758 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>()); 1759 inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>()); 1760 1761 Map<String, Object> contextParameters = new HashMap<>(); 1762 contextParameters.put(EditContentFunction.QUIT, Boolean.TRUE); 1763 contextParameters.put(EditContentFunction.FORM_RAW_VALUES, editionValues); 1764 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters); 1765 1766 return inputs; 1767 } 1768 1769 /** 1770 * Get the default workflow action id for initialization of main content 1771 * @return the default action id 1772 */ 1773 public int getDefaultInitActionId () 1774 { 1775 return 111; 1776 } 1777 1778 1779 /** 1780 * Get the default workflow action id for editing content metadata of main content 1781 * @return the default action id 1782 */ 1783 public int getDefaultActionIdForEditingContentReferences () 1784 { 1785 return 2; 1786 } 1787 1788 /** 1789 * Get the default workflow action id for editing content by copy 1790 * @return the default action id 1791 */ 1792 public int getDefaultActionIdForContentEdition() 1793 { 1794 return 222; 1795 } 1796 1797 /** 1798 * Retrieve the action id to execute for the content copy. 1799 * @return The action id 1800 */ 1801 public int getActionIdForCopy2() 1802 { 1803 return 111; 1804 } 1805 1806 /** 1807 * A copy attachments content handler. 1808 * To be used to copy the attachments linked in a rich text metadata. 1809 */ 1810 protected static class CopyAttachmentsHandler extends ContentHandlerProxy 1811 { 1812 /** base content */ 1813 protected Content _baseContent; 1814 /** target content */ 1815 protected ModifiableContent _targetContent; 1816 /** copy report */ 1817 protected CopyReport _copyReport; 1818 /** Ametys object resolver */ 1819 protected AmetysObjectResolver _resolver; 1820 /** logger */ 1821 protected Logger _logger; 1822 1823 1824 /** 1825 * Ctor 1826 * @param contentHandler The content handler to delegate to. 1827 * @param baseContent The content to copy 1828 * @param targetContent The content where to copy 1829 * @param copyReport The report of the copy 1830 * @param resolver The ametys object resolver 1831 * @param logger A logger to log informations 1832 */ 1833 protected CopyAttachmentsHandler(ContentHandler contentHandler, Content baseContent, ModifiableContent targetContent, CopyReport copyReport, AmetysObjectResolver resolver, Logger logger) 1834 { 1835 super(contentHandler); 1836 _baseContent = baseContent; 1837 _targetContent = targetContent; 1838 _copyReport = copyReport; 1839 _resolver = resolver; 1840 _logger = logger; 1841 } 1842 1843 @Override 1844 public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException 1845 { 1846 if ("link".equals(loc)) 1847 { 1848 // Copy attachment 1849 _copyIfAttachment(attrs.getValue("xlink:href")); 1850 } 1851 1852 super.startElement(uri, loc, raw, attrs); 1853 } 1854 1855 /** 1856 * Copy the linked resource to the new content if it is an attachment. 1857 * @param href link href attribute 1858 */ 1859 protected void _copyIfAttachment(String href) 1860 { 1861 try 1862 { 1863 if (_baseContent.getId().equals(href) || _targetContent.getId().equals(href)) 1864 { 1865 // nothing to do 1866 return; 1867 } 1868 else if (_resolver.hasAmetysObjectForId(href)) 1869 { 1870 AmetysObject ametysObject = _resolver.resolveById(href); 1871 1872 ResourceCollection baseRootAttachments = _baseContent.getRootAttachments(); 1873 if (!(ametysObject instanceof org.ametys.plugins.explorer.resources.Resource) || baseRootAttachments == null) 1874 { 1875 // nothing to do 1876 return; 1877 } 1878 1879 String baseAttachmentsPath = _baseContent.getRootAttachments().getPath(); 1880 String resourcePath = ametysObject.getPath(); 1881 1882 if (resourcePath.startsWith(baseAttachmentsPath + '/')) 1883 { 1884 // Is in attachments path 1885 String relPath = StringUtils.removeStart(resourcePath, baseAttachmentsPath + '/'); 1886 _copyAttachment(ametysObject, relPath); 1887 } 1888 } 1889 } 1890 catch (AmetysRepositoryException e) 1891 { 1892 // the reference was not <protocol>://<protocol-specific-part> (for example : mailto:mymail@example.com ) 1893 _logger.debug("The link '{}' is not recognized as Ametys object. It will be ignored", href); 1894 return; 1895 } 1896 } 1897 1898 /** 1899 * Copy an attachment 1900 * @param baseResource The resource to copy 1901 * @param relPath The path where to copy 1902 */ 1903 protected void _copyAttachment(AmetysObject baseResource, String relPath) 1904 { 1905 boolean success = false; 1906 Exception exception = null; 1907 1908 try 1909 { 1910 if (_targetContent instanceof ModifiableTraversableAmetysObject) 1911 { 1912 ModifiableTraversableAmetysObject mtaoTargetContent = (ModifiableTraversableAmetysObject) _targetContent; 1913 ModifiableResourceCollection targetParentCollection = mtaoTargetContent.getChild(DefaultContent.ATTACHMENTS_NODE_NAME); 1914 1915 String[] parts = StringUtils.split(relPath, '/'); 1916 if (parts.length > 0) 1917 { 1918 // Traverse the path and create necessary resources collections 1919 for (int i = 0; i < parts.length - 1; i++) 1920 { 1921 String childName = parts[i]; 1922 if (!targetParentCollection.hasChild(childName)) 1923 { 1924 targetParentCollection = targetParentCollection.createChild(childName, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE); 1925 } 1926 else 1927 { 1928 targetParentCollection = targetParentCollection.getChild(childName); 1929 } 1930 } 1931 1932 // Copy the attachment resource. 1933 String resourceName = parts[parts.length - 1]; 1934 if (baseResource instanceof CopiableAmetysObject) 1935 { 1936 ((CopiableAmetysObject) baseResource).copyTo(targetParentCollection, resourceName); 1937 success = true; 1938 _copyReport.addAttachment(relPath); 1939 } 1940 } 1941 } 1942 } 1943 catch (Exception e) 1944 { 1945 exception = e; 1946 } 1947 1948 if (!success) 1949 { 1950 String warnMsg = "Unable to copy attachment from base path '" + baseResource.getPath() + "' to the content at path : '" + _targetContent.getPath() + "'."; 1951 1952 if (_logger.isWarnEnabled()) 1953 { 1954 if (exception != null) 1955 { 1956 _logger.warn(warnMsg, exception); 1957 } 1958 else 1959 { 1960 _logger.warn(warnMsg); 1961 } 1962 } 1963 } 1964 } 1965 } 1966}