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.HashMap; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Map.Entry; 027import java.util.Optional; 028import java.util.Properties; 029import java.util.Set; 030 031import javax.xml.transform.OutputKeys; 032import javax.xml.transform.TransformerFactory; 033import javax.xml.transform.sax.SAXTransformerFactory; 034import javax.xml.transform.sax.TransformerHandler; 035import javax.xml.transform.stream.StreamResult; 036 037import org.apache.avalon.framework.component.Component; 038import org.apache.avalon.framework.service.ServiceException; 039import org.apache.avalon.framework.service.ServiceManager; 040import org.apache.avalon.framework.service.Serviceable; 041import org.apache.cocoon.ProcessingException; 042import org.apache.commons.lang3.StringUtils; 043import org.apache.commons.lang3.tuple.Pair; 044import org.apache.excalibur.xml.sax.ContentHandlerProxy; 045import org.apache.excalibur.xml.sax.SAXParser; 046import org.apache.xml.serializer.OutputPropertiesFactory; 047import org.slf4j.Logger; 048import org.xml.sax.Attributes; 049import org.xml.sax.ContentHandler; 050import org.xml.sax.InputSource; 051import org.xml.sax.SAXException; 052 053import org.ametys.cms.content.CopyReport.CopyMode; 054import org.ametys.cms.content.CopyReport.CopyState; 055import org.ametys.cms.contenttype.ContentType; 056import org.ametys.cms.contenttype.ContentTypesHelper; 057import org.ametys.cms.contenttype.RichTextUpdater; 058import org.ametys.cms.data.ContentValue; 059import org.ametys.cms.data.RichText; 060import org.ametys.cms.data.type.ModelItemTypeConstants; 061import org.ametys.cms.repository.Content; 062import org.ametys.cms.repository.DefaultContent; 063import org.ametys.cms.repository.ModifiableContent; 064import org.ametys.cms.repository.WorkflowAwareContent; 065import org.ametys.cms.workflow.AllErrors; 066import org.ametys.cms.workflow.ContentWorkflowHelper; 067import org.ametys.cms.workflow.CreateContentFunction; 068import org.ametys.cms.workflow.EditContentFunction; 069import org.ametys.cms.workflow.InvalidInputWorkflowException; 070import org.ametys.cms.workflow.copy.CreateContentByCopyFunction; 071import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 072import org.ametys.plugins.explorer.resources.ResourceCollection; 073import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory; 074import org.ametys.plugins.repository.AmetysObject; 075import org.ametys.plugins.repository.AmetysObjectResolver; 076import org.ametys.plugins.repository.AmetysRepositoryException; 077import org.ametys.plugins.repository.CopiableAmetysObject; 078import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 079import org.ametys.plugins.repository.model.ViewHelper; 080import org.ametys.plugins.workflow.AbstractWorkflowComponent; 081import org.ametys.plugins.workflow.support.WorkflowProvider; 082import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 083import org.ametys.runtime.i18n.I18nizableText; 084import org.ametys.runtime.i18n.I18nizableTextParameter; 085import org.ametys.runtime.model.ElementDefinition; 086import org.ametys.runtime.model.View; 087import org.ametys.runtime.model.ViewItemContainer; 088import org.ametys.runtime.parameter.Errors; 089import org.ametys.runtime.plugin.component.AbstractLogEnabled; 090 091/** 092 * <p> 093 * This component is used to copy a content (either totally or partially). 094 * </p><p> 095 * In this whole file a Map named <em>copyMap</em> is regularly used. This map 096 * provide the name of the attribute to copy as well as some optional parameters. 097 * It has the following form (JSON) : 098 * </p> 099 * <pre> 100 * { 101 * "$param1": value, 102 * "attributeA": null, 103 * "attributeB": { 104 * "subattributeB1": null, 105 * "subattributeB2": { 106 * "$param1": value, 107 * "$param2": value, 108 * "subSubattributeB21": {...} 109 * }, 110 * ... 111 * } 112 * } 113 * </pre> 114 * <p> 115 * Each attribute that should be copied must be present as a key in the map. 116 * Composite attribute can contains child attributes but as seen on the example the 117 * map must be well structured, it is not a flat map. Parameters in the map must 118 * always start with the reserved character '$', in order to be differentiated 119 * from attribute name. 120 * </p><p> 121 * The entry points are the copyContent and editContent methods, which run a dedicated workflow 122 * function (createByCopy or edit).<br> 123 * Actual write of values is made through the EditContentFunction, with the values computed by this component. 124 */ 125public class CopyContentComponent extends AbstractLogEnabled implements Serviceable, Component 126{ 127 /** Avalon ROLE. */ 128 public static final String ROLE = CopyContentComponent.class.getName(); 129 130 /** Workflow provider. */ 131 protected WorkflowProvider _workflowProvider; 132 133 /** Ametys object resolver available to subclasses. */ 134 protected AmetysObjectResolver _resolver; 135 136 /** Helper for content types */ 137 protected ContentTypesHelper _contentTypesHelper; 138 139 /** The content helper */ 140 protected ContentHelper _contentHelper; 141 142 /** The content workflow helper */ 143 protected ContentWorkflowHelper _contentWorkflowHelper; 144 145 /** Avalon service manager */ 146 protected ServiceManager _manager; 147 148 @Override 149 public void service(ServiceManager manager) throws ServiceException 150 { 151 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 152 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 153 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 154 _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE); 155 _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 156 _manager = manager; 157 } 158 159 /** 160 * Copy a content by creating a new content and copying the attributes value a source content into the new one. 161 * @param contentId The source content id 162 * @param title Desired title for the new content or null if computed from the source's title 163 * @param copyMap The map of properties as described in {@link CopyContentComponent}. 164 * Can be null in which case the map will be constructed from the provided view. 165 * @param viewName The name of the view to be used to construct to copyMap if not provided. This will also be the 166 * default name for possible inner copies (if not provided as a copyMap parameter). 167 * @param fallbackViewName The fallback view name if 'viewName' is not found 168 * @param targetContentType The type of content to create. If null the type(s) of created content will be those of base content. 169 * @param initActionId The init workflow action id for main content only 170 * @return The copy report containing valuable information about the copy and the possible encountered errors. 171 */ 172 public CopyReport copyContent(String contentId, String title, Map<String, Object> copyMap, String viewName, String fallbackViewName, String targetContentType, int initActionId) 173 { 174 Content content = _resolver.resolveById(contentId); 175 CopyReport report = new CopyReport(contentId, _contentHelper.getTitle(content), _contentHelper.isReferenceTable(content), viewName, fallbackViewName, CopyMode.CREATION); 176 177 try 178 { 179 Map<String, Object> inputs = getInputsForCopy(content, title, copyMap, targetContentType, report); 180 String workflowName = getWorkflowName(content, inputs); 181 182 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(); 183 workflow.initialize(workflowName, initActionId, inputs); 184 185 ModifiableContent targetContent = workflow.getAmetysObject(); 186 187 report.notifyContentCreation(targetContent.getId(), _contentHelper.getTitle(targetContent), _contentHelper.isReferenceTable(content)); 188 report.notifyContentCopySuccess(); 189 } 190 catch (Exception e) 191 { 192 getLogger().error("An error has been encountered during the content copy, or the copy is not allowed (base content identifier : {}).", contentId, e); 193 194 if (e instanceof InvalidInputWorkflowException) 195 { 196 I18nizableText rootError = null; 197 198 AllErrors allErrors = ((InvalidInputWorkflowException) e).getErrors(); 199 Map<String, Errors> allErrorsMap = allErrors.getAllErrors(); 200 for (String errorMetadataPath : allErrorsMap.keySet()) 201 { 202 Errors errors = allErrorsMap.get(errorMetadataPath); 203 204 I18nizableText insideError = null; 205 206 List<I18nizableText> errorsAsList = errors.getErrors(); 207 for (I18nizableText error : errorsAsList) 208 { 209 Map<String, I18nizableTextParameter> i18nparameters = new HashMap<>(); 210 i18nparameters.put("0", error); 211 212 I18nizableText localError = new I18nizableText("plugin.cms", "CONTENT_COPY_ACTIONS_REPORT_ERROR_VALIDATION_ATTRIBUTE_CHAIN", i18nparameters); 213 214 if (insideError == null) 215 { 216 insideError = localError; 217 } 218 else 219 { 220 insideError.getParameterMap().put("1", localError); 221 } 222 } 223 224 Map<String, I18nizableTextParameter> i18ngeneralparameters = new HashMap<>(); 225 226 String i18ngeneralkey = null; 227 if (EditContentFunction.GLOBAL_ERROR_KEY.equals(errorMetadataPath)) 228 { 229 i18ngeneralkey = "CONTENT_COPY_ACTIONS_REPORT_ERROR_GLOBAL_VALIDATION"; 230 i18ngeneralparameters.put("error", insideError); 231 } 232 else 233 { 234 i18ngeneralkey = "CONTENT_COPY_ACTIONS_REPORT_ERROR_VALIDATION_ATTRIBUTE"; 235 i18ngeneralparameters.put("path", new I18nizableText(errorMetadataPath)); 236 i18ngeneralparameters.put("error", insideError); 237 } 238 239 I18nizableText generalError = new I18nizableText("plugin.cms", i18ngeneralkey, i18ngeneralparameters); 240 if (rootError == null) 241 { 242 rootError = generalError; 243 } 244 else 245 { 246 rootError.getParameterMap().put("2", generalError); 247 } 248 } 249 250 Map<String, I18nizableTextParameter> parameters = Map.of("title", new I18nizableText(_contentHelper.getTitle(content)), "error", rootError); 251 I18nizableText errorMsg = new I18nizableText("plugin.cms", "CONTENT_COPY_ACTIONS_REPORT_ERROR_COPY", parameters); 252 report.setErrorMessage(errorMsg); 253 } 254 report.notifyContentCopyError(); 255 } 256 257 return report; 258 } 259 260 /** 261 * Retrieve the inputs for the copy workflow function. 262 * @param baseContent The content to copy 263 * @param title The title to set 264 * @param copyMap The map with properties to copy 265 * @param targetContentType The type of content to create. If null the type(s) of created content will be those of base content. 266 * @param copyReport The report of the copy 267 * @return The map of inputs. 268 */ 269 protected Map<String, Object> getInputsForCopy(Content baseContent, String title, Map<String, Object> copyMap, String targetContentType, CopyReport copyReport) 270 { 271 Map<String, Object> inputs = new HashMap<>(); 272 273 inputs.put(CreateContentByCopyFunction.BASE_CONTENT_KEY, baseContent); 274 inputs.put(CreateContentByCopyFunction.COPY_MAP_KEY, copyMap); 275 inputs.put(CreateContentByCopyFunction.COPY_REPORT_KEY, copyReport); 276 inputs.put(CreateContentByCopyFunction.COPY_VIEW_NAME, copyReport.getViewName()); 277 inputs.put(CreateContentByCopyFunction.COPY_FALLBACK_VIEW_NAME, copyReport.getFallbackViewName()); 278 279 if (StringUtils.isNoneBlank(title)) 280 { 281 inputs.put(CreateContentFunction.CONTENT_TITLE_KEY, title); 282 } 283 284 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>()); 285 inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>()); 286 287 if (targetContentType != null) 288 { 289 inputs.put(CreateContentFunction.CONTENT_TYPES_KEY, new String[] {targetContentType}); 290 } 291 292 return inputs; 293 } 294 295 /** 296 * Retrieve the workflow name of a content. 297 * @param content The content to consider 298 * @param inputs The inputs that will be provided to the workflow function 299 * @return The name of the workflow. 300 * @throws IllegalArgumentException if the content is not workflow aware. 301 */ 302 protected String getWorkflowName(Content content, Map<String, Object> inputs) throws IllegalArgumentException 303 { 304 String workflowName = null; 305 306 if (content instanceof WorkflowAwareContent) 307 { 308 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 309 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 310 workflowName = workflow.getWorkflowName(waContent.getWorkflowId()); 311 } 312 313 if (workflowName == null) 314 { 315 String errorMsg = String.format("Unable to retrieve the workflow name for the content with identifier '%s'.", content.getId()); 316 317 getLogger().error(errorMsg); 318 throw new IllegalArgumentException(errorMsg); 319 } 320 321 return workflowName; 322 } 323 324 /** 325 * Edit a content by copying attribute values a source content into a target content. 326 * @param contentId The identifier of the source content 327 * @param targetContentId The identifier of the target content 328 * @param copyMap The map of properties as described in {@link CopyContentComponent}. 329 * Can be null in which case the map will be constructed from the provided view. 330 * @param viewName The name of the view to be used to construct to copyMap if not provided. This will also be the 331 * default name for possible inner copies (if not provided as a copyMap parameter). 332 * @param fallbackViewName The fallback view name if 'viewName' is not found 333 * @return The copy report containing valuable information about the copy and the possible encountered errors. 334 */ 335 public CopyReport editContent(String contentId, String targetContentId, Map<String, Object> copyMap, String viewName, String fallbackViewName) 336 { 337 return editContent(contentId, targetContentId, copyMap, viewName, fallbackViewName, getDefaultActionIdForContentEdition()); 338 } 339 340 /** 341 * Edit a content by copying attribute values a source content into a target content. 342 * @param contentId The identifier of the source content 343 * @param targetContentId The identifier of the target content 344 * @param copyMap The map of properties as described in {@link CopyContentComponent}. 345 * Can be null in which case the map will be constructed from the provided view. 346 * @param viewName The name of the view to be used to construct to copyMap if not provided. This will also be the 347 * default name for possible inner copies (if not provided as a copyMap parameter). 348 * @param fallbackViewName The fallback view name if 'viewName' is not found 349 * @param actionId the edit workflow action id 350 * @return The copy report containing valuable information about the copy and the possible encountered errors. 351 */ 352 public CopyReport editContent(String contentId, String targetContentId, Map<String, Object> copyMap, String viewName, String fallbackViewName, int actionId) 353 { 354 Content content = _resolver.resolveById(contentId); 355 356 String auxViewName = StringUtils.defaultIfEmpty(viewName, "default-edition"); 357 String auxFallbackViewName = StringUtils.defaultIfEmpty(fallbackViewName, "main"); 358 359 CopyReport report = new CopyReport(contentId, _contentHelper.getTitle(content), _contentHelper.isReferenceTable(content), auxViewName, auxFallbackViewName, CopyMode.EDITION); 360 try 361 { 362 WorkflowAwareContent targetContent = _resolver.resolveById(targetContentId); 363 364 Map<String, Object> values = computeValues(content, (ModifiableContent) targetContent, copyMap, null, auxViewName, auxFallbackViewName, report); 365 366 _contentWorkflowHelper.editContent(targetContent, values, actionId); 367 368 report.notifyContentCreation(targetContent.getId(), _contentHelper.getTitle(targetContent), _contentHelper.isReferenceTable(content)); 369 report.notifyContentCopySuccess(); 370 } 371 catch (Exception e) 372 { 373 getLogger().error("An error has been encountered during the content edition, or the edition is not allowed (base content identifier : {}, target content identifier : {}).", 374 contentId, targetContentId, e); 375 376 report.notifyContentCopyError(); 377 } 378 379 return report; 380 } 381 382 /** 383 * Extract values to copy from the given parameters. 384 * @param content the source content 385 * @param targetContent the target content 386 * @param copyMap the map of properties as described in {@link CopyContentComponent}. 387 * @param additionalCopyMap an additional map of properties if needed. Can be null. Often used in case of recursive copies. 388 * @param viewName The name of the view to be used to construct to copyMap if not provided. This will also be the 389 * default name for possible inner copies (if not provided as a copyMap parameter). 390 * @param fallbackViewName The fallback view name if 'viewName' is not found 391 * @param copyReport The copy report containing valuable information about the copy and the possible encountered errors. 392 * @return the computed values, ready to be synchronized to the target content. 393 */ 394 public Map<String, Object> computeValues(Content content, ModifiableContent targetContent, Map<String, Object> copyMap, Map<String, Object> additionalCopyMap, String viewName, String fallbackViewName, CopyReport copyReport) 395 { 396 Pair<View, Map<String, Object>> viewAndValues = _getViewAndValues(content, copyMap, additionalCopyMap, viewName, fallbackViewName, copyReport); 397 View view = viewAndValues.getLeft(); 398 Map<String, Object> values = viewAndValues.getRight(); 399 400 _updateRichTexts(content, targetContent, view, values, copyReport); 401 402 return values; 403 } 404 405 private Pair<View, Map<String, Object>> _getViewAndValues(Content content, Map<String, Object> copyMap, Map<String, Object> additionalCopyMap, String viewName, String fallbackViewName, CopyReport copyReport) 406 { 407 Set<String> viewItems; 408 Map<String, Map<String, Object>> contentToCopy = new HashMap<>(); 409 410 Map<String, Object> finalCopyMap = null; 411 if (copyMap != null) 412 { 413 finalCopyMap = new HashMap<>(); 414 for (Entry<String, Object> entry : copyMap.entrySet()) 415 { 416 if (!entry.getKey().startsWith("$")) 417 { 418 // cannot use stream here as entry values are often null and Collectors.toMap don't like that... 419 finalCopyMap.put(entry.getKey(), entry.getValue()); 420 } 421 } 422 } 423 424 if (finalCopyMap != null && !finalCopyMap.isEmpty()) 425 { 426 viewItems = new HashSet<>(); 427 _fillViewItemsFromCopyMap(content, viewItems, contentToCopy, finalCopyMap, ""); 428 } 429 else 430 { 431 View view = _contentTypesHelper.getViewWithFallback(viewName, fallbackViewName, content.getTypes(), content.getMixinTypes()); 432 viewItems = new HashSet<>(org.ametys.runtime.model.ViewHelper.getModelItemsPathsFromView(view)); 433 } 434 435 if (additionalCopyMap != null) 436 { 437 _fillViewItemsFromCopyMap(content, viewItems, contentToCopy, additionalCopyMap, ""); 438 } 439 440 View view = View.of(content.getModel(), viewItems.toArray(String[]::new)); 441 Map<String, Object> values = content.dataToMap(view); 442 443 _processLinkedContents(content, view, values, contentToCopy, copyReport); 444 445 return Pair.of(view, values); 446 } 447 448 @SuppressWarnings("unchecked") 449 private void _fillViewItemsFromCopyMap(Content content, Set<String> items, Map<String, Map<String, Object>> contentToCopy, Map<String, Object> copyMap, String prefix) 450 { 451 for (String name : copyMap.keySet()) 452 { 453 if (!name.startsWith("$")) 454 { 455 Object value = copyMap.get(name); 456 if (value == null) 457 { 458 items.add(prefix + name); 459 } 460 else 461 { 462 Map<String, Object> subCopyMap = (Map<String, Object>) value; 463 if (subCopyMap.containsKey("$mode")) 464 { 465 // content attribute 466 items.add(prefix + name); 467 contentToCopy.put(prefix + name, subCopyMap); 468 } 469 else 470 { 471 // composite or repeater 472 _fillViewItemsFromCopyMap(content, items, contentToCopy, subCopyMap, prefix + name + "/"); 473 } 474 } 475 } 476 } 477 } 478 479 @SuppressWarnings("unchecked") 480 private void _processLinkedContents(Content content, ViewItemContainer viewItemContainer, Map<String, Object> values, Map<String, Map<String, Object>> contentToCopy, CopyReport copyReport) 481 { 482 ViewHelper.visitView(viewItemContainer, 483 (element, definition) -> { 484 // simple element 485 String name = definition.getName(); 486 String path = definition.getPath(); 487 Object value = values.get(name); 488 if (value != null && definition.getType().getId().equals(ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID)) 489 { 490 // create the content and replace the old value by the new one 491 Map<String, Object> copyMap = contentToCopy.get(path); 492 boolean referenceMode = copyMap == null || !"create".equals(copyMap.get("$mode")); 493 494 if (definition.isMultiple()) 495 { 496 ContentValue[] contentValues = (ContentValue[]) value; 497 List<ContentValue> targets = new ArrayList<>(); 498 499 Arrays.stream(contentValues) 500 .map(contentValue -> contentValue.getContentIfExists()) 501 .flatMap(Optional::stream) 502 .forEach(subContent -> { 503 ContentValue contentValue = handleLinkedContent(definition, subContent, referenceMode, copyMap, copyReport); 504 if (contentValue != null) 505 { 506 targets.add(contentValue); 507 } 508 }); 509 510 values.put(name, targets.toArray(ContentValue[]::new)); 511 } 512 else 513 { 514 ContentValue contentValue = (ContentValue) value; 515 ModifiableContent subContent = contentValue.getContentIfExists().orElse(null); 516 if (subContent != null) 517 { 518 ContentValue linkedValue = handleLinkedContent(definition, subContent, referenceMode, copyMap, copyReport); 519 values.put(name, linkedValue); 520 } 521 } 522 } 523 }, 524 (group, definition) -> { 525 // composite 526 String name = definition.getName(); 527 Map<String, Object> composite = (Map<String, Object>) values.get(name); 528 if (composite != null) 529 { 530 _processLinkedContents(content, group, composite, contentToCopy, copyReport); 531 } 532 }, 533 (group, definition) -> { 534 // repeater 535 String name = definition.getName(); 536 List<Map<String, Object>> entries = (List<Map<String, Object>>) values.get(name); 537 if (entries != null) 538 { 539 for (Map<String, Object> entry : entries) 540 { 541 _processLinkedContents(content, group, entry, contentToCopy, copyReport); 542 } 543 } 544 }, 545 group -> _processLinkedContents(content, group, values, contentToCopy, copyReport)); 546 } 547 548 /** 549 * Handle a single value of a content attribute 550 * @param definition the attribute definition 551 * @param value the linked value on the source content 552 * @param referenceMode true if a reference was initially requested, false if it was a copy 553 * @param copyMap the current copy map 554 * @param copyReport the copy report 555 * @return the {@link ContentValue} (copied or not) to insert in the current Content. 556 */ 557 protected ContentValue handleLinkedContent(ElementDefinition definition, ModifiableContent value, boolean referenceMode, Map<String, Object> copyMap, CopyReport copyReport) 558 { 559 if (!referenceMode) 560 { 561 String targetContentId = copyLinkedContent(value, copyMap, copyReport); 562 if (targetContentId != null) 563 { 564 return new ContentValue(_resolver, targetContentId); 565 } 566 } 567 else 568 { 569 return new ContentValue(value); 570 } 571 572 return null; 573 } 574 575 /** 576 * Copy a value of a content attribute. 577 * @param content the initial content value. 578 * @param copyMap the current copy map 579 * @param copyReport the current copy report 580 * @return the id of the copied Content. 581 */ 582 protected String copyLinkedContent(Content content, Map<String, Object> copyMap, CopyReport copyReport) 583 { 584 String defaultViewName = copyMap != null ? (String) copyMap.getOrDefault("$viewName", copyReport.getViewName()) : copyReport.getViewName(); 585 String defaultFallbackViewName = copyMap != null ? (String) copyMap.getOrDefault("$fallbackViewName", copyReport.getFallbackViewName()) : copyReport.getFallbackViewName(); 586 587 CopyReport innerReport = copyContent(content.getId(), content.getTitle(), copyMap, defaultViewName, defaultFallbackViewName, null, getDefaultInitActionId()); 588 589 String targetContentId = null; 590 if (innerReport.getStatus() == CopyState.SUCCESS) 591 { 592 targetContentId = innerReport.getTargetContentId(); 593 } 594 595 copyReport.addReport(innerReport); 596 597 return targetContentId; 598 } 599 600 @SuppressWarnings("unchecked") 601 private void _updateRichTexts(Content content, ModifiableContent targetContent, ViewItemContainer viewItemContainer, Map<String, Object> values, CopyReport copyReport) 602 { 603 ViewHelper.visitView(viewItemContainer, 604 (element, definition) -> { 605 // simple element 606 String name = definition.getName(); 607 Object value = values.get(name); 608 if (value != null && definition.getType().getId().equals(ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID)) 609 { 610 // update richTexts 611 RichTextUpdater richTextUpdater = ((ContentType) definition.getModel()).getRichTextUpdater(); 612 if (richTextUpdater != null) 613 { 614 if (definition.isMultiple()) 615 { 616 RichText[] richTexts = (RichText[]) value; 617 for (RichText richText : richTexts) 618 { 619 _updateRichText(richText, richTextUpdater, content, targetContent, copyReport); 620 } 621 } 622 else 623 { 624 RichText richText = (RichText) value; 625 _updateRichText(richText, richTextUpdater, content, targetContent, copyReport); 626 } 627 } 628 } 629 }, 630 (group, definition) -> { 631 // composite 632 String name = definition.getName(); 633 Map<String, Object> composite = (Map<String, Object>) values.get(name); 634 if (composite != null) 635 { 636 _updateRichTexts(content, targetContent, group, composite, copyReport); 637 } 638 }, 639 (group, definition) -> { 640 // repeater 641 String name = definition.getName(); 642 List<Map<String, Object>> entries = (List<Map<String, Object>>) values.get(name); 643 if (entries != null) 644 { 645 for (Map<String, Object> entry : entries) 646 { 647 _updateRichTexts(content, targetContent, group, entry, copyReport); 648 } 649 } 650 }, 651 group -> _updateRichTexts(content, targetContent, group, values, copyReport)); 652 } 653 654 private void _updateRichText(RichText richText, RichTextUpdater richTextUpdater, Content initialContent, ModifiableContent targetContent, CopyReport copyReport) 655 { 656 try 657 { 658 Map<String, Object> params = new HashMap<>(); 659 params.put("initialContent", initialContent); 660 params.put("createdContent", targetContent); 661 params.put("initialAO", initialContent); 662 params.put("createdAO", targetContent); 663 664 // create the transformer instance 665 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 666 667 // create the format of result 668 Properties format = new Properties(); 669 format.put(OutputKeys.METHOD, "xml"); 670 format.put(OutputKeys.INDENT, "yes"); 671 format.put(OutputKeys.ENCODING, "UTF-8"); 672 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2"); 673 th.getTransformer().setOutputProperties(format); 674 675 // Update rich text contents 676 // Copy the needed original attachments. 677 try (InputStream is = richText.getInputStream(); OutputStream os = richText.getOutputStream()) 678 { 679 StreamResult result = new StreamResult(os); 680 th.setResult(result); 681 682 ContentHandler richTextHandler = richTextUpdater.getContentHandler(th, th, params); 683 684 // Copy attachments handler. 685 ContentHandlerProxy copyAttachmentsHandler = new CopyAttachmentsHandler(richTextHandler, initialContent, targetContent, copyReport, _resolver, getLogger()); 686 687 // Rich text update. 688 SAXParser saxParser = null; 689 try 690 { 691 saxParser = (SAXParser) _manager.lookup(SAXParser.ROLE); 692 saxParser.parse(new InputSource(is), copyAttachmentsHandler); 693 } 694 catch (ServiceException e) 695 { 696 throw new ProcessingException("Unable to get a SAX parser", e); 697 } 698 finally 699 { 700 _manager.release(saxParser); 701 } 702 } 703 } 704 catch (Exception e) 705 { 706 getLogger().error("An error occurred while updating rich text attribute for content '{}' after copy from initial content '{}'", targetContent.getId(), initialContent.getId(), e); 707 } 708 } 709 710 /** 711 * Get the default workflow action id for initialization of main content 712 * @return the default action id 713 */ 714 public int getDefaultInitActionId () 715 { 716 return 111; 717 } 718 719 /** 720 * Get the default workflow action id for editing content by copy 721 * @return the default action id 722 */ 723 public int getDefaultActionIdForContentEdition() 724 { 725 return 222; 726 } 727 728 /** 729 * A copy attachments content handler. 730 * To be used to copy the attachments linked in a rich text attribute. 731 */ 732 protected static class CopyAttachmentsHandler extends ContentHandlerProxy 733 { 734 /** base content */ 735 protected Content _baseContent; 736 /** target content */ 737 protected ModifiableContent _targetContent; 738 /** copy report */ 739 protected CopyReport _copyReport; 740 /** Ametys object resolver */ 741 protected AmetysObjectResolver _resolver; 742 /** logger */ 743 protected Logger _logger; 744 745 /** 746 * Ctor 747 * @param contentHandler The content handler to delegate to. 748 * @param baseContent The content to copy 749 * @param targetContent The content where to copy 750 * @param copyReport The report of the copy 751 * @param resolver The ametys object resolver 752 * @param logger A logger to log informations 753 */ 754 protected CopyAttachmentsHandler(ContentHandler contentHandler, Content baseContent, ModifiableContent targetContent, CopyReport copyReport, AmetysObjectResolver resolver, Logger logger) 755 { 756 super(contentHandler); 757 _baseContent = baseContent; 758 _targetContent = targetContent; 759 _copyReport = copyReport; 760 _resolver = resolver; 761 _logger = logger; 762 } 763 764 @Override 765 public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException 766 { 767 if ("link".equals(loc)) 768 { 769 // Copy attachment 770 _copyIfAttachment(attrs.getValue("xlink:href")); 771 } 772 773 super.startElement(uri, loc, raw, attrs); 774 } 775 776 /** 777 * Copy the linked resource to the new content if it is an attachment. 778 * @param href link href attribute 779 */ 780 protected void _copyIfAttachment(String href) 781 { 782 try 783 { 784 if (_baseContent.getId().equals(href) || _targetContent.getId().equals(href)) 785 { 786 // nothing to do 787 return; 788 } 789 else if (_resolver.hasAmetysObjectForId(href)) 790 { 791 AmetysObject ametysObject = _resolver.resolveById(href); 792 793 ResourceCollection baseRootAttachments = _baseContent.getRootAttachments(); 794 if (!(ametysObject instanceof org.ametys.plugins.explorer.resources.Resource) || baseRootAttachments == null) 795 { 796 // nothing to do 797 return; 798 } 799 800 String baseAttachmentsPath = _baseContent.getRootAttachments().getPath(); 801 String resourcePath = ametysObject.getPath(); 802 803 if (resourcePath.startsWith(baseAttachmentsPath + '/')) 804 { 805 // Is in attachments path 806 String relPath = StringUtils.removeStart(resourcePath, baseAttachmentsPath + '/'); 807 _copyAttachment(ametysObject, relPath); 808 } 809 } 810 } 811 catch (AmetysRepositoryException e) 812 { 813 // the reference was not <protocol>://<protocol-specific-part> (for example : mailto:mymail@example.com ) 814 _logger.debug("The link '{}' is not recognized as Ametys object. It will be ignored", href); 815 return; 816 } 817 } 818 819 /** 820 * Copy an attachment 821 * @param baseResource The resource to copy 822 * @param relPath The path where to copy 823 */ 824 protected void _copyAttachment(AmetysObject baseResource, String relPath) 825 { 826 boolean success = false; 827 Exception exception = null; 828 829 try 830 { 831 if (_targetContent instanceof ModifiableTraversableAmetysObject) 832 { 833 ModifiableTraversableAmetysObject mtaoTargetContent = (ModifiableTraversableAmetysObject) _targetContent; 834 ModifiableResourceCollection targetParentCollection = mtaoTargetContent.getChild(DefaultContent.ATTACHMENTS_NODE_NAME); 835 836 String[] parts = StringUtils.split(relPath, '/'); 837 if (parts.length > 0) 838 { 839 // Traverse the path and create necessary resources collections 840 for (int i = 0; i < parts.length - 1; i++) 841 { 842 String childName = parts[i]; 843 if (!targetParentCollection.hasChild(childName)) 844 { 845 targetParentCollection = targetParentCollection.createChild(childName, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE); 846 } 847 else 848 { 849 targetParentCollection = targetParentCollection.getChild(childName); 850 } 851 } 852 853 // Copy the attachment resource. 854 String resourceName = parts[parts.length - 1]; 855 if (baseResource instanceof CopiableAmetysObject) 856 { 857 ((CopiableAmetysObject) baseResource).copyTo(targetParentCollection, resourceName); 858 success = true; 859 _copyReport.addAttachment(relPath); 860 } 861 } 862 } 863 } 864 catch (Exception e) 865 { 866 exception = e; 867 } 868 869 if (!success) 870 { 871 String warnMsg = "Unable to copy attachment from base path '" + baseResource.getPath() + "' to the content at path : '" + _targetContent.getPath() + "'."; 872 873 if (_logger.isWarnEnabled()) 874 { 875 if (exception != null) 876 { 877 _logger.warn(warnMsg, exception); 878 } 879 else 880 { 881 _logger.warn(warnMsg); 882 } 883 } 884 } 885 } 886 } 887}