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