001/* 002 * Copyright 2019 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.plugins.contentio.archive; 017 018import java.io.IOException; 019import java.net.URI; 020import java.nio.file.Path; 021import java.nio.file.attribute.BasicFileAttributes; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Date; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Objects; 029import java.util.Optional; 030import java.util.function.Predicate; 031import java.util.stream.Collectors; 032import java.util.stream.Stream; 033import java.util.zip.ZipEntry; 034import java.util.zip.ZipOutputStream; 035 036import javax.jcr.AccessDeniedException; 037import javax.jcr.ItemNotFoundException; 038import javax.jcr.Node; 039import javax.jcr.RepositoryException; 040import javax.xml.parsers.DocumentBuilder; 041import javax.xml.parsers.DocumentBuilderFactory; 042import javax.xml.parsers.ParserConfigurationException; 043import javax.xml.transform.TransformerConfigurationException; 044import javax.xml.transform.TransformerException; 045import javax.xml.transform.sax.TransformerHandler; 046import javax.xml.transform.stream.StreamResult; 047 048import org.apache.avalon.framework.component.Component; 049import org.apache.avalon.framework.service.ServiceException; 050import org.apache.avalon.framework.service.ServiceManager; 051import org.apache.avalon.framework.service.Serviceable; 052import org.apache.commons.lang3.ArrayUtils; 053import org.apache.commons.lang3.StringUtils; 054import org.slf4j.Logger; 055import org.w3c.dom.Document; 056import org.xml.sax.SAXException; 057 058import org.ametys.cms.CmsConstants; 059import org.ametys.cms.content.references.OutgoingReferences; 060import org.ametys.cms.content.references.OutgoingReferencesExtractor; 061import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 062import org.ametys.cms.repository.Content; 063import org.ametys.cms.repository.DefaultContent; 064import org.ametys.cms.repository.ModifiableContent; 065import org.ametys.cms.repository.ModifiableContentHelper; 066import org.ametys.cms.repository.WorkflowAwareContent; 067import org.ametys.cms.repository.WorkflowAwareContentHelper; 068import org.ametys.core.user.UserIdentity; 069import org.ametys.core.util.DateUtils; 070import org.ametys.plugins.contentio.archive.Archivers.AmetysObjectNotImportedException; 071import org.ametys.plugins.repository.AmetysObjectIterable; 072import org.ametys.plugins.repository.AmetysObjectResolver; 073import org.ametys.plugins.repository.TraversableAmetysObject; 074import org.ametys.plugins.repository.collection.AmetysObjectCollection; 075import org.ametys.plugins.repository.data.extractor.xml.XMLValuesExtractorAdditionalDataGetter; 076import org.ametys.plugins.repository.jcr.JCRAmetysObject; 077import org.ametys.plugins.repository.jcr.JCRTraversableAmetysObject; 078import org.ametys.plugins.repository.jcr.NodeHelper; 079import org.ametys.plugins.repository.version.VersionableAmetysObject; 080import org.ametys.plugins.workflow.support.WorkflowProvider; 081import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 082import org.ametys.runtime.plugin.component.AbstractLogEnabled; 083 084import com.opensymphony.workflow.WorkflowException; 085import com.opensymphony.workflow.spi.Step; 086 087/** 088 * Export a contents collection as individual XML files. 089 */ 090public class ContentsArchiverHelper extends AbstractLogEnabled implements Component, Serviceable 091{ 092 /** Avalon role. */ 093 public static final String ROLE = ContentsArchiverHelper.class.getName(); 094 095 private static final String __CONTENT_ZIP_ENTRY_FILENAME = "content.xml"; 096 private static final String __ACL_ZIP_ENTRY_FILENAME = "_acl.xml"; 097 098 private AmetysObjectResolver _resolver; 099 private ResourcesArchiverHelper _resourcesArchiverHelper; 100 private WorkflowProvider _workflowProvider; 101 private ModifiableContentHelper _modifiableContentHelper; 102 private OutgoingReferencesExtractor _outgoingReferencesExtractor; 103 private ContentTypeExtensionPoint _contentTypeEP; 104 105 @Override 106 public void service(ServiceManager manager) throws ServiceException 107 { 108 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 109 _resourcesArchiverHelper = (ResourcesArchiverHelper) manager.lookup(ResourcesArchiverHelper.ROLE); 110 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 111 _modifiableContentHelper = (ModifiableContentHelper) manager.lookup(ModifiableContentHelper.ROLE); 112 _outgoingReferencesExtractor = (OutgoingReferencesExtractor) manager.lookup(OutgoingReferencesExtractor.ROLE); 113 _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 114 } 115 116 /** 117 * Exports contents from a root Node. 118 * @param prefix the prefix for the ZIP archive. 119 * @param rootNode the root JCR Node holding the contents collection. 120 * @param zos the ZIP OutputStream. 121 * @throws RepositoryException if an error occurs while resolving Node. 122 * @throws IOException if an error occurs while archiving 123 */ 124 public void exportContents(String prefix, Node rootNode, ZipOutputStream zos) throws RepositoryException, IOException 125 { 126 TraversableAmetysObject rootContents = _resolver.resolve(rootNode, false); 127 exportContents(prefix, rootContents, zos); 128 } 129 130 /** 131 * Exports contents from a root AmetysObject. 132 * @param prefix the prefix for the ZIP archive. 133 * @param rootContents the root JCR Node holding the contents collection. 134 * @param zos the ZIP OutputStream. 135 * @throws IOException if an error occurs while archiving 136 */ 137 public void exportContents(String prefix, TraversableAmetysObject rootContents, ZipOutputStream zos) throws IOException 138 { 139 zos.putNextEntry(new ZipEntry(StringUtils.appendIfMissing(prefix, "/"))); // even if there is no child, at least export the root of contents 140 141 AmetysObjectIterable<Content> contents = rootContents.getChildren(); 142 for (Content content : contents) 143 { 144 try 145 { 146 _exportContent(prefix, content, zos); 147 } 148 catch (Exception e) 149 { 150 throw new RuntimeException("An error occured while exporting content '" + content.getId() + "'.", e); 151 } 152 } 153 154 // finally process ACL for the contents root 155 try 156 { 157 Node contentNode = ((JCRAmetysObject) rootContents).getNode(); 158 Archivers.exportAcl(contentNode, zos, prefix + __ACL_ZIP_ENTRY_FILENAME); 159 } 160 catch (RepositoryException e) 161 { 162 throw new RuntimeException("Unable to SAX ACL for root contents at '" + rootContents.getPath() + "' for archiving", e); 163 } 164 } 165 166 private void _exportContent(String prefix, Content content, ZipOutputStream zos) throws IOException 167 { 168 List<String> unexistingContentTypesAndMixins = Stream.of(content.getTypes(), content.getMixinTypes()) 169 .flatMap(Stream::of) 170 .filter(Predicate.not(_contentTypeEP::hasExtension)) 171 .collect(Collectors.toList()); 172 if (!unexistingContentTypesAndMixins.isEmpty()) 173 { 174 getLogger().error("Content \"{}\" will not be exported as at least one of its types or mixins does not exist: {}", content, unexistingContentTypesAndMixins); 175 return; 176 } 177 178 // for each content, first an XML file with attributes, comments, tags, ... 179 String name = content.getName(); 180 String path = prefix + NodeHelper.getFullHashPath(name) + "/"; 181 ZipEntry contentEntry = new ZipEntry(path + __CONTENT_ZIP_ENTRY_FILENAME); 182 zos.putNextEntry(contentEntry); 183 184 try 185 { 186 TransformerHandler contentHandler = Archivers.newTransformerHandler(); 187 contentHandler.setResult(new StreamResult(zos)); 188 189 contentHandler.startDocument(); 190 content.toSAX(contentHandler, null, null, true); 191 contentHandler.endDocument(); 192 } 193 catch (SAXException | TransformerConfigurationException e) 194 { 195 throw new RuntimeException("Unable to SAX content '" + content.getPath() + "' for archiving", e); 196 } 197 198 // then all attachments 199 _resourcesArchiverHelper.exportCollection(content.getRootAttachments(), zos, path + "_attachments/"); 200 201 // then all files local to rich texts (images) 202 Archivers.exportRichTexts(content, zos, path); 203 204 // then all binary attributes 205 Archivers.exportBinaries(content, zos, path); 206 207 // then all file attributes 208 Archivers.exportFiles(content, zos, path); 209 210 // then ACL 211 try 212 { 213 Node contentNode = ((JCRAmetysObject) content).getNode(); 214 Archivers.exportAcl(contentNode, zos, path + __ACL_ZIP_ENTRY_FILENAME); 215 } 216 catch (RepositoryException e) 217 { 218 throw new RuntimeException("Unable to SAX ACL for content '" + content.getPath() + "' for archiving", e); 219 } 220 } 221 222 /** 223 * Imports contents from the given ZIP archive and path, under the given root of contents 224 * @param commonPrefix The common prefix in the ZIP archive 225 * @param rootContents the root {@link JCRTraversableAmetysObject} holding the contents collection. 226 * @param zipPath the input zip path 227 * @param merger The {@link Merger} 228 * @param contentFillers The fillers in order to fill additional attributes on imported contents 229 * @return The {@link ImportReport} 230 * @throws IOException if an error occurs while importing archive 231 */ 232 public ImportReport importContents(String commonPrefix, AmetysObjectCollection rootContents, Path zipPath, Merger merger, Collection<ContentFiller> contentFillers) throws IOException 233 { 234 Importer importer; 235 List<DefaultContent> createdContents; 236 try 237 { 238 importer = new Importer(commonPrefix, rootContents, zipPath, merger, contentFillers, getLogger()); 239 createdContents = importer.importRoot(); 240 } 241 catch (ParserConfigurationException e) 242 { 243 throw new IOException(e); 244 } 245 _saveContents(rootContents); 246 _checkoutContents(createdContents); 247 return importer._report; 248 } 249 250 private void _saveContents(AmetysObjectCollection rootContents) 251 { 252 if (rootContents.needsSave()) 253 { 254 getLogger().warn(Archivers.WARN_MESSAGE_ROOT_HAS_PENDING_CHANGES, rootContents); 255 rootContents.saveChanges(); 256 } 257 } 258 259 private void _checkoutContents(List<DefaultContent> createdContents) 260 { 261 for (DefaultContent createdContent : createdContents) 262 { 263 createdContent.checkpoint(); 264 } 265 } 266 267 /** 268 * A filler in order to fill additional attributes on imported contents 269 */ 270 @FunctionalInterface 271 public static interface ContentFiller 272 { 273 /** 274 * Fill the content with additional attributes 275 * @param content The imported content 276 */ 277 void fillContent(DefaultContent content); 278 } 279 280 private class Importer 281 { 282 final ImportReport _report = new ImportReport(); 283 private final String _commonPrefix; 284 private final AmetysObjectCollection _root; 285 private final Path _zipArchivePath; 286 private final Merger _merger; 287 private final Collection<ContentFiller> _contentFillers; 288 private final Logger _logger; 289 private final DocumentBuilder _builder; 290 private final UnitaryContentImporter _unitaryImporter = new UnitaryContentImporter(); 291 292 Importer(String commonPrefix, AmetysObjectCollection root, Path zipArchivePath, Merger merger, Collection<ContentFiller> contentFillers, Logger logger) throws ParserConfigurationException 293 { 294 _commonPrefix = commonPrefix; 295 _root = root; 296 _zipArchivePath = zipArchivePath; 297 _merger = merger; 298 _contentFillers = contentFillers; 299 _logger = logger; 300 _builder = DocumentBuilderFactory.newInstance() 301 .newDocumentBuilder(); 302 } 303 304 List<DefaultContent> importRoot() throws IOException 305 { 306 _fillRoot(); 307 308 try (Stream<Path> zippedFiles = _matchingZippedFiles()) 309 { 310 // no stream pipeline here because exception flow is important 311 List<DefaultContent> createdContents = new ArrayList<>(); 312 for (Path zipEntryPath : zippedFiles.toArray(Path[]::new)) 313 { 314 Optional<DefaultContent> createdContent = _importContent(zipEntryPath); 315 createdContent.ifPresent(createdContents::add); 316 } 317 return createdContents; 318 } 319 } 320 321 private void _fillRoot() throws IOException 322 { 323 _createRootContentAcl(); 324 try 325 { 326 Archivers.unitarySave(_root.getNode(), _logger); 327 } 328 catch (AmetysObjectNotImportedException e) 329 { 330 // ACL were not exported => it was already logged in error level, and it does not affect the future import of contents => continue 331 } 332 } 333 334 private void _createRootContentAcl() throws IOException 335 { 336 Node rootNode = _root.getNode(); 337 String zipEntryPath = new StringBuilder() 338 .append(StringUtils.strip(_commonPrefix, "/")) 339 .append("/") 340 .append(__ACL_ZIP_ENTRY_FILENAME) 341 .toString(); 342 _createAcl(rootNode, zipEntryPath); 343 } 344 345 private void _createContentAcl(Node contentNode, Path contentZipEntryPath) throws IOException 346 { 347 String zipEntryPath = contentZipEntryPath 348 .getParent() 349 .resolve(__ACL_ZIP_ENTRY_FILENAME) 350 .toString(); 351 _createAcl(contentNode, zipEntryPath); 352 } 353 354 private void _createAcl(Node node, String zipAclEntryPath) throws IOException 355 { 356 try 357 { 358 _logger.debug("Trying to import ACL node for Content (or root of contents) '{}', from ACL XML file '{}', if it exists", node, zipAclEntryPath); 359 Archivers.importAcl(node, _zipArchivePath, _merger, zipAclEntryPath, _logger); 360 } 361 catch (RepositoryException e) 362 { 363 throw new IOException(e); 364 } 365 } 366 367 private Stream<Path> _matchingZippedFiles() throws IOException 368 { 369 return ZipEntryHelper.zipFileTree( 370 _zipArchivePath, 371 Optional.of(_commonPrefix), 372 (Path p, BasicFileAttributes attrs) -> 373 !attrs.isDirectory() 374 && __CONTENT_ZIP_ENTRY_FILENAME.equals(p.getFileName().toString())); 375 } 376 377 private Optional<DefaultContent> _importContent(Path zipEntryPath) throws ImportGlobalFailException 378 { 379 return _unitaryImporter.unitaryImport(_zipArchivePath, zipEntryPath, _merger, _logger); 380 } 381 382 private Document _getContentPropertiesXml(Path zipEntryPath) throws SAXException, IOException 383 { 384 URI zipEntryUri = zipEntryPath.toUri(); 385 return _builder.parse(zipEntryUri.toString()); 386 } 387 388 private DefaultContent _createContent(Path contentZipEntry, Document propertiesXml) throws AmetysObjectNotImportedException, Exception 389 { 390 // At first, check the content types and mixins exist in the current application, otherwise do not import the content ASAP 391 String[] contentTypes = _retrieveContentTypes(contentZipEntry, propertiesXml, "content/contentTypes/contentType"); 392 String[] mixins = _retrieveContentTypes(contentZipEntry, propertiesXml, "content/mixins/mixin"); 393 394 // Create the JCR Node 395 String uuid = Archivers.xpathEvalNonEmpty("content/@uuid", propertiesXml); 396 String contentDesiredName = Archivers.xpathEvalNonEmpty("content/@name", propertiesXml); 397 String type = Archivers.xpathEvalNonEmpty("content/@primaryType", propertiesXml); 398 _logger.info("Creating a Content object for '{}' file (uuid={}, type={}, desiredName={})", contentZipEntry, uuid, type, contentDesiredName); 399 400 DefaultContent createdContent = _createChild(uuid, contentDesiredName, type); 401 402 // Set mandatory properties 403 _setContentMandatoryProperties(createdContent, contentTypes, mixins, propertiesXml); 404 // Set other properties 405 _fillContentNode(createdContent, propertiesXml, contentZipEntry); 406 // Set content attachments 407 ImportReport importAttachmentReport = _fillContentAttachments(createdContent, contentZipEntry); 408 _report.addFrom(importAttachmentReport); 409 // Fill other attributes 410 _fillAdditionalContentAttributes(createdContent); 411 // Outgoing references 412 _setOutgoingReferences(createdContent); 413 414 // Initialize workflow 415 if (createdContent instanceof WorkflowAwareContent) 416 { 417 String workflowName = Archivers.xpathEvalNonEmpty("content/workflow-step/@workflowName", propertiesXml); 418 _handleWorkflow(workflowName, (WorkflowAwareContent) createdContent); 419 } 420 421 return createdContent; 422 } 423 424 private String[] _retrieveContentTypes(Path contentZipEntry, Document propertiesXml, String xPath) throws TransformerException, AmetysObjectNotImportedException 425 { 426 String[] contentTypes = DomNodeHelper.stringValues(propertiesXml, xPath); 427 List<String> unexistingTypes = Stream.of(contentTypes) 428 .filter(Predicate.not(_contentTypeEP::hasExtension)) 429 .collect(Collectors.toList()); 430 if (!unexistingTypes.isEmpty()) 431 { 432 String message = String.format("Content defined in '%s' has at least one of its types or mixins which does not exist: %s", contentZipEntry, unexistingTypes); 433 throw new AmetysObjectNotImportedException(message); 434 } 435 return contentTypes; 436 } 437 438 private DefaultContent _createChild(String uuid, String contentDesiredName, String type) throws AccessDeniedException, ItemNotFoundException, RepositoryException 439 { 440 // Create a content with AmetysObjectCollection.createChild 441 String unusedContentName = _getUnusedContentName(contentDesiredName); 442 JCRAmetysObject srcContent = (JCRAmetysObject) _root.createChild(unusedContentName, type); 443 Node srcNode = srcContent.getNode(); 444 // But then call 'replaceNodeWithDesiredUuid' to have it with the desired UUID (srcNode will be removed) 445 Node nodeWithDesiredUuid = Archivers.replaceNodeWithDesiredUuid(srcNode, uuid); 446 447 // Then resolve and return a Content 448 String parentPath = _root.getPath(); 449 DefaultContent createdContent = _resolver.resolve(parentPath, nodeWithDesiredUuid, null, false); 450 return createdContent; 451 } 452 453 // ~ same algorithm than org.ametys.cms.workflow.CreateContentFunction._createContent 454 // no use of org.ametys.cms.FilterNameHelper.filterName because it was already filtered during the export (taken from the existing content name) 455 private String _getUnusedContentName(String desiredName) 456 { 457 String contentName = desiredName; 458 for (int errorCount = 0; true; errorCount++) 459 { 460 if (errorCount != 0) 461 { 462 _logger.debug("Name '{}' from Content is already used. Trying another one...", contentName); 463 contentName = desiredName + "-" + (errorCount + 1); 464 } 465 if (!_root.hasChild(contentName)) 466 { 467 _logger.debug("Content will be created with unused name '{}'. The desired name was '{}'", contentName, desiredName); 468 return contentName; 469 } 470 } 471 } 472 473 private void _setContentMandatoryProperties(DefaultContent content, String[] contentTypes, String[] mixins, Document propertiesXml) throws TransformerException, AmetysObjectNotImportedException 474 { 475 content.setTypes(contentTypes); 476 content.setMixinTypes(mixins); 477 478 Date creationDate = Objects.requireNonNull(DomNodeHelper.nullableDatetimeValue(propertiesXml, "content/@createdAt")); 479 _modifiableContentHelper.setCreationDate(content, DateUtils.asZonedDateTime(creationDate)); 480 481 String creator = Archivers.xpathEvalNonEmpty("content/@creator", propertiesXml); 482 _modifiableContentHelper.setCreator(content, UserIdentity.stringToUserIdentity(creator)); 483 484 Date lastModifiedAt = Objects.requireNonNull(DomNodeHelper.nullableDatetimeValue(propertiesXml, "content/@lastModifiedAt")); 485 _modifiableContentHelper.setLastModified(content, DateUtils.asZonedDateTime(lastModifiedAt)); 486 487 String lastContributor = Archivers.xpathEvalNonEmpty("content/@lastContributor", propertiesXml); 488 _modifiableContentHelper.setLastContributor(content, UserIdentity.stringToUserIdentity(lastContributor)); 489 } 490 491 private void _fillContentNode(DefaultContent content, Document propertiesXml, Path contentZipEntry) throws TransformerException, Exception 492 { 493 String language = DomNodeHelper.nullableStringValue(propertiesXml, "content/@language"); 494 if (language != null) 495 { 496 content.setLanguage(language); 497 } 498 499 Date lastValidatedAt = DomNodeHelper.nullableDatetimeValue(propertiesXml, "content/@lastValidatedAt"); 500 if (lastValidatedAt != null) 501 { 502 _modifiableContentHelper.setLastValidationDate(content, DateUtils.asZonedDateTime(lastValidatedAt)); 503 } 504 505 if (content instanceof ModifiableContent) 506 { 507 Path contentPath = contentZipEntry.getParent(); 508 _fillContent((ModifiableContent) content, propertiesXml, contentPath); 509 } 510 } 511 512 private void _fillContent(ModifiableContent content, Document propertiesXml, Path contentZipEntry) throws Exception 513 { 514 XMLValuesExtractorAdditionalDataGetter additionalDataGetter = new ResourcesAdditionalDataGetter(_zipArchivePath, contentZipEntry); 515 content.fillContent(propertiesXml, additionalDataGetter); 516 } 517 518 private ImportReport _fillContentAttachments(DefaultContent createdContent, Path contentZipEntry) throws IOException, RepositoryException 519 { 520 Node contentNode = createdContent.getNode(); 521 522 // ametys-internal:attachments is created automatically 523 if (contentNode.hasNode(DefaultContent.ATTACHMENTS_NODE_NAME)) 524 { 525 contentNode.getNode(DefaultContent.ATTACHMENTS_NODE_NAME).remove(); 526 } 527 528 Path contentAttachmentsZipEntryFolder = contentZipEntry.resolveSibling("_attachments/"); 529 String commonPrefix = StringUtils.appendIfMissing(contentAttachmentsZipEntryFolder.toString(), "/"); 530 return _resourcesArchiverHelper.importCollection(commonPrefix, contentNode, _zipArchivePath, _merger); 531 } 532 533 private void _fillAdditionalContentAttributes(DefaultContent content) 534 { 535 for (ContentFiller contentFiller : _contentFillers) 536 { 537 contentFiller.fillContent(content); 538 } 539 } 540 541 private void _setOutgoingReferences(DefaultContent content) 542 { 543 if (content instanceof ModifiableContent) 544 { 545 Map<String, OutgoingReferences> outgoingReferencesByPath = _outgoingReferencesExtractor.getOutgoingReferences(content); 546 ((ModifiableContent) content).setOutgoingReferences(outgoingReferencesByPath); 547 } 548 } 549 550 private void _handleWorkflow(String workflowName, WorkflowAwareContent createdContent) throws WorkflowException 551 { 552 // In the case of import on the original instance, the version history is still present. 553 // As we are about to reset the workflow and potentially change the workflowId 554 // We must unpublish any prior version to make sure that we won't publish a version 555 // With an incoherent workflowId 556 if (createdContent instanceof VersionableAmetysObject vao 557 && ArrayUtils.contains(vao.getAllLabels(), CmsConstants.LIVE_LABEL)) 558 { 559 vao.removeLabel(CmsConstants.LIVE_LABEL); 560 } 561 562 // Then initialize the workflow 563 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(createdContent); 564 565 int initialAction = 0; 566 Map<String, Object> inputs = new HashMap<>(Map.of()); 567 long workflowId = workflow.initialize(workflowName, initialAction, inputs); 568 WorkflowAwareContentHelper.setWorkflowId(createdContent, workflowId); 569 570 Step currentStep = (Step) workflow.getCurrentSteps(workflowId).iterator().next(); 571 createdContent.setCurrentStepId(currentStep.getStepId()); 572 } 573 574 private final class UnitaryContentImporter implements UnitaryImporter<DefaultContent> 575 { 576 @Override 577 public String objectNameForLogs() 578 { 579 return "Content"; 580 } 581 582 @Override 583 public Document getPropertiesXml(Path zipEntryPath) throws Exception 584 { 585 return _getContentPropertiesXml(zipEntryPath); 586 } 587 588 @Override 589 public String retrieveId(Document propertiesXml) throws Exception 590 { 591 return Archivers.xpathEvalNonEmpty("content/@id", propertiesXml); 592 } 593 594 @Override 595 public DefaultContent create(Path zipEntryPath, String id, Document propertiesXml) throws AmetysObjectNotImportedException, Exception 596 { 597 DefaultContent createdContent = _createContent(zipEntryPath, propertiesXml); 598 Node contentNode = createdContent.getNode(); 599 _createContentAcl(contentNode, zipEntryPath); 600 Archivers.unitarySave(contentNode, _logger); 601 return createdContent; 602 } 603 604 @Override 605 public ImportReport getReport() 606 { 607 return _report; 608 } 609 } 610 } 611}