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.io.InputStream; 020import java.nio.file.DirectoryStream; 021import java.nio.file.Files; 022import java.nio.file.Path; 023import java.time.LocalDate; 024import java.time.ZoneId; 025import java.time.ZonedDateTime; 026import java.time.format.DateTimeFormatter; 027import java.util.ArrayList; 028import java.util.Calendar; 029import java.util.Date; 030import java.util.GregorianCalendar; 031import java.util.List; 032import java.util.Objects; 033import java.util.Optional; 034import java.util.zip.ZipEntry; 035import java.util.zip.ZipOutputStream; 036 037import javax.jcr.Binary; 038import javax.jcr.Node; 039import javax.jcr.RepositoryException; 040import javax.jcr.Session; 041import javax.xml.parsers.DocumentBuilder; 042import javax.xml.parsers.DocumentBuilderFactory; 043import javax.xml.parsers.ParserConfigurationException; 044import javax.xml.transform.TransformerConfigurationException; 045import javax.xml.transform.TransformerException; 046import javax.xml.transform.sax.TransformerHandler; 047import javax.xml.transform.stream.StreamResult; 048 049import org.apache.avalon.framework.component.Component; 050import org.apache.avalon.framework.context.ContextException; 051import org.apache.avalon.framework.context.Contextualizable; 052import org.apache.avalon.framework.service.ServiceException; 053import org.apache.avalon.framework.service.ServiceManager; 054import org.apache.avalon.framework.service.Serviceable; 055import org.apache.cocoon.Constants; 056import org.apache.cocoon.environment.Context; 057import org.apache.cocoon.xml.XMLUtils; 058import org.apache.commons.io.IOUtils; 059import org.apache.commons.lang3.StringUtils; 060import org.apache.xpath.XPathAPI; 061import org.slf4j.Logger; 062import org.w3c.dom.Document; 063import org.xml.sax.ContentHandler; 064import org.xml.sax.SAXException; 065 066import org.ametys.core.user.UserIdentity; 067import org.ametys.core.util.DateUtils; 068import org.ametys.plugins.contentio.archive.Archivers.AmetysObjectNotImportedException; 069import org.ametys.plugins.explorer.resources.Resource; 070import org.ametys.plugins.explorer.resources.ResourceCollection; 071import org.ametys.plugins.explorer.resources.jcr.JCRResource; 072import org.ametys.plugins.explorer.resources.jcr.JCRResourceFactory; 073import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollection; 074import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory; 075import org.ametys.plugins.repository.AmetysObject; 076import org.ametys.plugins.repository.AmetysObjectFactoryExtensionPoint; 077import org.ametys.plugins.repository.AmetysObjectIterable; 078import org.ametys.plugins.repository.AmetysRepositoryException; 079import org.ametys.plugins.repository.RepositoryConstants; 080import org.ametys.plugins.repository.dublincore.DublinCoreAwareAmetysObject; 081import org.ametys.plugins.repository.dublincore.ModifiableDublinCoreAwareAmetysObject; 082import org.ametys.plugins.repository.jcr.JCRAmetysObject; 083import org.ametys.runtime.plugin.component.AbstractLogEnabled; 084 085/** 086 * Export a resources collection as individual files. 087 */ 088public class ResourcesArchiverHelper extends AbstractLogEnabled implements Component, Serviceable, Contextualizable 089{ 090 /** Avalon role. */ 091 public static final String ROLE = ResourcesArchiverHelper.class.getName(); 092 093 private static final String __PROPERTIES_METADATA_XML_FILE_NAME_SUFFIX = "properties.xml"; 094 private static final String __DC_METADATA_XML_FILE_NAME_SUFFIX = "dc.xml"; 095 096 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_ROOT = "dublin-core-metadata"; 097 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_TITLE = "title"; 098 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_CREATOR = "creator"; 099 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_SUBJECT = "subject"; 100 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_DESCRIPTION = "description"; 101 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_PUBLISHER = "publisher"; 102 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_CONTRIBUTOR = "contributor"; 103 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_DATE = "date"; 104 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_TYPE = "type"; 105 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_FORMAT = "format"; 106 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_IDENTIFIER = "identifier"; 107 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_SOURCE = "source"; 108 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_LANGUAGE = "language"; 109 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_RELATION = "relation"; 110 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_COVERAGE = "coverage"; 111 private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_RIGHTS = "rights"; 112 113 private JCRResourcesCollectionFactory _jcrResourcesCollectionFactory; 114 private JCRResourceFactory _jcrResourceFactory; 115 116 private Context _cocoonContext; 117 118 @Override 119 public void service(ServiceManager manager) throws ServiceException 120 { 121 AmetysObjectFactoryExtensionPoint ametysObjectFactoryEP = (AmetysObjectFactoryExtensionPoint) manager.lookup(AmetysObjectFactoryExtensionPoint.ROLE); 122 _jcrResourcesCollectionFactory = (JCRResourcesCollectionFactory) ametysObjectFactoryEP.getExtension(JCRResourcesCollectionFactory.class.getName()); 123 _jcrResourceFactory = (JCRResourceFactory) ametysObjectFactoryEP.getExtension(JCRResourceFactory.class.getName()); 124 } 125 126 @Override 127 public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException 128 { 129 _cocoonContext = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 130 } 131 132 /** 133 * Exports a {@link ResourceCollection} as folders and files inside the ZIP archive. 134 * @param collection the root {@link ResourceCollection} 135 * @param zos the ZIP OutputStream. 136 * @param prefix the prefix for the ZIP archive. 137 * @throws IOException if an error occurs while archiving 138 */ 139 public void exportCollection(ResourceCollection collection, ZipOutputStream zos, String prefix) throws IOException 140 { 141 if (collection == null) 142 { 143 return; 144 } 145 146 zos.putNextEntry(new ZipEntry(StringUtils.appendIfMissing(prefix, "/"))); // even if there is no child, at least export the collection folder 147 148 try (AmetysObjectIterable<AmetysObject> objects = collection.getChildren();) 149 { 150 for (AmetysObject object : objects) 151 { 152 _exportChild(object, zos, prefix); 153 } 154 } 155 156 try 157 { 158 // process metadata for the collection 159 _exportCollectionMetadataEntry(collection, zos, prefix); 160 // process ACL for the collection 161 Node collectionNode = ((JCRAmetysObject) collection).getNode(); 162 Archivers.exportAcl(collectionNode, zos, ArchiveHandler.METADATA_PREFIX + prefix + "acl.xml"); 163 } 164 catch (RepositoryException e) 165 { 166 throw new RuntimeException("Unable to SAX some information for collection '" + collection.getPath() + "' for archiving", e); 167 } 168 } 169 170 private void _exportCollectionMetadataEntry(ResourceCollection collection, ZipOutputStream zos, String prefix) throws IOException 171 { 172 String metadataFullPrefix = ArchiveHandler.METADATA_PREFIX + prefix; 173 ZipEntry contentEntry = new ZipEntry(metadataFullPrefix + __PROPERTIES_METADATA_XML_FILE_NAME_SUFFIX); 174 zos.putNextEntry(contentEntry); 175 176 try 177 { 178 TransformerHandler contentHandler = Archivers.newTransformerHandler(); 179 contentHandler.setResult(new StreamResult(zos)); 180 181 contentHandler.startDocument(); 182 _saxSystemMetadata(collection, contentHandler); 183 contentHandler.endDocument(); 184 } 185 catch (SAXException | TransformerConfigurationException e) 186 { 187 throw new RuntimeException("Unable to SAX properties for collection '" + collection.getPath() + "' for archiving", e); 188 } 189 } 190 191 private void _exportChild(AmetysObject child, ZipOutputStream zos, String prefix) throws IOException 192 { 193 if (child instanceof ResourceCollection) 194 { 195 String newPrefix = prefix + child.getName() + "/"; 196 exportCollection((ResourceCollection) child, zos, newPrefix); 197 } 198 else if (child instanceof Resource) 199 { 200 exportResource((Resource) child, zos, prefix); 201 } 202 } 203 204 /** 205 * Exports a {@link Resource} as file inside the ZIP archive. 206 * @param resource the {@link Resource}. 207 * @param zos the ZIP OutputStream. 208 * @param prefix the prefix for the ZIP archive. 209 * @throws IOException if an error occurs while archiving 210 */ 211 public void exportResource(Resource resource, ZipOutputStream zos, String prefix) throws IOException 212 { 213 ZipEntry resourceEntry = new ZipEntry(prefix + resource.getName()); 214 zos.putNextEntry(resourceEntry); 215 216 try (InputStream is = resource.getInputStream()) 217 { 218 IOUtils.copy(is, zos); 219 } 220 221 // dublin core properties 222 _exportResourceMetadataEntry(resource, zos, prefix, __DC_METADATA_XML_FILE_NAME_SUFFIX, this::_saxDublinCoreMetadata, "Dublin Core metadata"); 223 224 // other properties (id, creator...) 225 _exportResourceMetadataEntry(resource, zos, prefix, __PROPERTIES_METADATA_XML_FILE_NAME_SUFFIX, this::_saxSystemMetadata, "properties"); 226 } 227 228 private void _exportResourceMetadataEntry(Resource resource, ZipOutputStream zos, String prefix, String metadataFileNameSuffix, ResourceMetadataSaxer metadataSaxer, String debugName) throws IOException 229 { 230 String metadataFullPrefix = ArchiveHandler.METADATA_PREFIX + prefix + resource.getName() + "_"; 231 ZipEntry contentEntry = new ZipEntry(metadataFullPrefix + metadataFileNameSuffix); 232 zos.putNextEntry(contentEntry); 233 234 try 235 { 236 TransformerHandler contentHandler = Archivers.newTransformerHandler(); 237 contentHandler.setResult(new StreamResult(zos)); 238 239 contentHandler.startDocument(); 240 metadataSaxer.sax(resource, contentHandler); 241 contentHandler.endDocument(); 242 } 243 catch (SAXException | TransformerConfigurationException e) 244 { 245 throw new RuntimeException("Unable to SAX " + debugName + " for resource '" + resource.getPath() + "' for archiving", e); 246 } 247 } 248 249 @FunctionalInterface 250 private static interface ResourceMetadataSaxer 251 { 252 void sax(Resource resource, ContentHandler contentHandler) throws SAXException; 253 } 254 255 private void _saxDublinCoreMetadata(DublinCoreAwareAmetysObject dcObject, ContentHandler contentHandler) throws SAXException 256 { 257 XMLUtils.startElement(contentHandler, __DC_METADATA_XML_EXPORT_TAG_NAME_ROOT); 258 _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_TITLE, dcObject.getDCTitle(), contentHandler); 259 _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_CREATOR, dcObject.getDCCreator(), contentHandler); 260 _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_SUBJECT, dcObject.getDCSubject(), contentHandler); 261 _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_DESCRIPTION, dcObject.getDCDescription(), contentHandler); 262 _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_PUBLISHER, dcObject.getDCPublisher(), contentHandler); 263 _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_CONTRIBUTOR, dcObject.getDCContributor(), contentHandler); 264 _saxLocalDateIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_DATE, dcObject.getDCDate(), contentHandler); 265 _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_TYPE, dcObject.getDCType(), contentHandler); 266 _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_FORMAT, dcObject.getDCFormat(), contentHandler); 267 _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_IDENTIFIER, dcObject.getDCIdentifier(), contentHandler); 268 _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_SOURCE, dcObject.getDCSource(), contentHandler); 269 _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_LANGUAGE, dcObject.getDCLanguage(), contentHandler); 270 _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_RELATION, dcObject.getDCRelation(), contentHandler); 271 _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_COVERAGE, dcObject.getDCCoverage(), contentHandler); 272 _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_RIGHTS, dcObject.getDCRights(), contentHandler); 273 XMLUtils.endElement(contentHandler, __DC_METADATA_XML_EXPORT_TAG_NAME_ROOT); 274 } 275 276 private void _saxSystemMetadata(Resource resource, ContentHandler contentHandler) throws SAXException 277 { 278 XMLUtils.startElement(contentHandler, "resource"); 279 _saxIfNotNull("id", resource.getId(), contentHandler); 280 _saxIfNotNull("name", resource.getName(), contentHandler); 281 _saxIfNotNull("creator", resource.getCreator(), contentHandler); 282 _saxZonedDateTimeIfNotNull("creationDate", resource.getCreationDate(), contentHandler); 283 _saxIfNotNull("contributor", resource.getLastContributor(), contentHandler); 284 _saxZonedDateTimeIfNotNull("lastModified", resource.getLastModified(), contentHandler); 285 XMLUtils.endElement(contentHandler, "resource"); 286 } 287 288 private void _saxSystemMetadata(ResourceCollection collection, ContentHandler contentHandler) throws SAXException 289 { 290 XMLUtils.startElement(contentHandler, "resource-collection"); 291 _saxIfNotNull("id", collection.getId(), contentHandler); 292 _saxIfNotNull("name", collection.getName(), contentHandler); 293 XMLUtils.endElement(contentHandler, "resource-collection"); 294 } 295 296 private void _saxIfNotNull(String name, String value, ContentHandler contentHandler) throws SAXException 297 { 298 if (value != null) 299 { 300 XMLUtils.createElement(contentHandler, name, value); 301 } 302 } 303 304 private void _saxIfNotNull(String name, UserIdentity value, ContentHandler contentHandler) throws SAXException 305 { 306 if (value != null) 307 { 308 XMLUtils.createElement(contentHandler, name, UserIdentity.userIdentityToString(value)); 309 } 310 } 311 312 private void _saxIfNotNull(String name, String[] values, ContentHandler contentHandler) throws SAXException 313 { 314 if (values != null) 315 { 316 for (String value : values) 317 { 318 XMLUtils.createElement(contentHandler, name, value); 319 } 320 } 321 } 322 323 private void _saxLocalDateIfNotNull(String name, Date value, ContentHandler contentHandler) throws SAXException 324 { 325 if (value != null) 326 { 327 LocalDate ld = DateUtils.asLocalDate(value); 328 XMLUtils.createElement(contentHandler, name, ld.format(DateTimeFormatter.ISO_LOCAL_DATE)); 329 } 330 } 331 332 private void _saxZonedDateTimeIfNotNull(String name, Date value, ContentHandler contentHandler) throws SAXException 333 { 334 if (value != null) 335 { 336 ZonedDateTime zdt = DateUtils.asZonedDateTime(value, ZoneId.systemDefault()); 337 XMLUtils.createElement(contentHandler, name, zdt.format(DateUtils.getISODateTimeFormatter())); 338 } 339 } 340 341 /** 342 * Imports folders and files from the given ZIP archive and path, under the given {@link ResourceCollection} 343 * @param commonPrefix The common prefix in the ZIP archive 344 * @param parentOfRootResources the parent of the root {@link ResourceCollection} (the root will also be created) 345 * @param zipPath the input zip path 346 * @param merger The {@link Merger} 347 * @return The {@link ImportReport} 348 * @throws IOException if an error occurs while importing archive 349 */ 350 public ImportReport importCollection(String commonPrefix, Node parentOfRootResources, Path zipPath, Merger merger) throws IOException 351 { 352 Importer importer; 353 List<JCRResource> importedResources; 354 try 355 { 356 importer = new Importer(commonPrefix, parentOfRootResources, zipPath, merger, getLogger()); 357 importer.importRoot(); 358 importedResources = importer.getImportedResource(); 359 } 360 catch (ParserConfigurationException e) 361 { 362 throw new IOException(e); 363 } 364 _saveImported(parentOfRootResources); 365 _checkoutImported(importedResources); 366 return importer._report; 367 } 368 369 private void _saveImported(Node parentOfRoot) 370 { 371 try 372 { 373 Session session = parentOfRoot.getSession(); 374 if (session.hasPendingChanges()) 375 { 376 getLogger().warn(Archivers.WARN_MESSAGE_ROOT_HAS_PENDING_CHANGES, parentOfRoot); 377 session.save(); 378 } 379 } 380 catch (RepositoryException e) 381 { 382 throw new AmetysRepositoryException("Unable to save changes", e); 383 } 384 } 385 386 private void _checkoutImported(List<JCRResource> importedResources) 387 { 388 for (JCRResource resource : importedResources) 389 { 390 resource.checkpoint(); 391 } 392 } 393 394 private class Importer 395 { 396 final ImportReport _report = new ImportReport(); 397 private final String _commonPrefix; 398 private final Node _parentOfRoot; 399 private final Path _zipArchivePath; 400 private final Merger _merger; 401 private final Logger _logger; 402 private final DocumentBuilder _builder; 403 private final List<JCRResource> _importedResources = new ArrayList<>(); 404 private JCRResourcesCollection _root; 405 private final UnitaryCollectionImporter _unitaryCollectionImporter = new UnitaryCollectionImporter(); 406 private final UnitaryResourceImporter _unitaryResourceImporter = new UnitaryResourceImporter(); 407 408 Importer(String commonPrefix, Node parentOfRoot, Path zipArchivePath, Merger merger, Logger logger) throws ParserConfigurationException 409 { 410 _commonPrefix = commonPrefix; 411 _parentOfRoot = parentOfRoot; 412 _zipArchivePath = zipArchivePath; 413 _merger = merger; 414 _logger = logger; 415 _builder = DocumentBuilderFactory.newInstance() 416 .newDocumentBuilder(); 417 } 418 419 private class UnitaryCollectionImporter implements UnitaryImporter<Node> 420 { 421 @Override 422 public String objectNameForLogs() 423 { 424 return "Resource collection"; 425 } 426 427 @Override 428 public Document getPropertiesXml(Path zipEntryPath) throws Exception 429 { 430 return _getFolderPropertiesXml(zipEntryPath); 431 } 432 433 @Override 434 public String retrieveId(Document propertiesXml) throws Exception 435 { 436 return Archivers.xpathEvalNonEmpty("resource-collection/id", propertiesXml); 437 } 438 439 @Override 440 public Node create(Path zipEntryPath, String id, Document propertiesXml) throws AmetysObjectNotImportedException, Exception 441 { 442 Node node = _createResourceCollection(zipEntryPath, id, propertiesXml); 443 Archivers.unitarySave(node, _logger); 444 return node; 445 } 446 447 @Override 448 public ImportReport getReport() 449 { 450 return _report; 451 } 452 } 453 454 private class UnitaryResourceImporter implements UnitaryImporter<JCRResource> 455 { 456 @Override 457 public String objectNameForLogs() 458 { 459 return "Resource"; 460 } 461 462 @Override 463 public Document getPropertiesXml(Path zipEntryPath) throws Exception 464 { 465 return _getFilePropertiesXml(zipEntryPath); 466 } 467 468 @Override 469 public String retrieveId(Document propertiesXml) throws Exception 470 { 471 return Archivers.xpathEvalNonEmpty("resource/id", propertiesXml); 472 } 473 474 @Override 475 public JCRResource create(Path zipEntryPath, String id, Document propertiesXml) throws AmetysObjectNotImportedException, Exception 476 { 477 JCRResource createdResource = _createdResource(zipEntryPath, id, propertiesXml); 478 Archivers.unitarySave(createdResource.getNode(), _logger); 479 return createdResource; 480 } 481 482 @Override 483 public ImportReport getReport() 484 { 485 return _report; 486 } 487 } 488 489 void importRoot() throws IOException 490 { 491 if (ZipEntryHelper.zipEntryFolderExists(_zipArchivePath, _commonPrefix)) 492 { 493 Path rootFolderToImport = ZipEntryHelper.zipFileRoot(_zipArchivePath) 494 .resolve(_commonPrefix); 495 496 _importResourceCollectionAndChildren(rootFolderToImport); 497 } 498 } 499 500 List<JCRResource> getImportedResource() 501 { 502 return _importedResources; 503 } 504 505 private void _createResourceCollectionAcl(Node collectionNode, String folderPath) throws IOException 506 { 507 String zipEntryPath = new StringBuilder() 508 .append(ArchiveHandler.METADATA_PREFIX) 509 .append(StringUtils.strip(folderPath, "/")) 510 .append("/acl.xml") 511 .toString(); 512 try 513 { 514 _logger.debug("Trying to import ACL node for ResourcesCollection '{}', from ACL XML file '{}', if it exists", collectionNode, zipEntryPath); 515 Archivers.importAcl(collectionNode, _zipArchivePath, _merger, zipEntryPath, _logger); 516 } 517 catch (RepositoryException e) 518 { 519 throw new IOException(e); 520 } 521 } 522 523 private void _importChildren(Path folder) throws IOException 524 { 525 String pathPrefix = _relativePath(folder); 526 527 DirectoryStream<Path> childFiles = _getDirectFileChildren(pathPrefix); 528 try (childFiles) 529 { 530 for (Path childFile : childFiles) 531 { 532 _importResource(childFile) 533 .ifPresent(_importedResources::add); 534 } 535 } 536 537 DirectoryStream<Path> childFolders = _getDirectFolderChildren(pathPrefix); 538 try (childFolders) 539 { 540 for (Path childFolder : childFolders) 541 { 542 _importResourceCollectionAndChildren(childFolder); 543 } 544 } 545 } 546 547 private DirectoryStream<Path> _getDirectFolderChildren(String pathPrefix) throws IOException 548 { 549 return ZipEntryHelper.children( 550 _zipArchivePath, 551 Optional.of(_commonPrefix + pathPrefix), 552 p -> Files.isDirectory(p)); 553 } 554 555 private DirectoryStream<Path> _getDirectFileChildren(String pathPrefix) throws IOException 556 { 557 return ZipEntryHelper.children( 558 _zipArchivePath, 559 Optional.of(_commonPrefix + pathPrefix), 560 p -> !Files.isDirectory(p)); 561 } 562 563 private void _importResourceCollectionAndChildren(Path folder) throws IOException 564 { 565 Optional<Node> optionalCollectionNode = _importResourceCollection(folder); 566 if (optionalCollectionNode.isPresent()) 567 { 568 Node collectionNode = optionalCollectionNode.get(); 569 _createResourceCollectionAcl(collectionNode, folder.toString()); 570 _importChildren(folder); 571 } 572 } 573 574 private Optional<Node> _importResourceCollection(Path folder) throws ImportGlobalFailException 575 { 576 return _unitaryCollectionImporter.unitaryImport(_zipArchivePath, folder, _merger, _logger); 577 } 578 579 private Document _getFolderPropertiesXml(Path folder) throws IOException 580 { 581 String zipEntryPath = new StringBuilder() 582 .append(ArchiveHandler.METADATA_PREFIX) 583 .append(StringUtils.strip(folder.toString(), "/")) 584 .append("/") 585 .append(__PROPERTIES_METADATA_XML_FILE_NAME_SUFFIX) 586 .toString(); 587 try (InputStream stream = ZipEntryHelper.zipEntryFileInputStream(_zipArchivePath, zipEntryPath)) 588 { 589 Document doc = _builder.parse(stream); 590 return doc; 591 } 592 catch (SAXException e) 593 { 594 throw new IOException(e); 595 } 596 } 597 598 private Node _createResourceCollection(Path folder, String id, Document propertiesXml) throws IOException, AmetysObjectNotImportedException, TransformerException 599 { 600 boolean isRoot = _isRoot(folder); 601 Node parentNode = _retrieveParentJcrNode(folder, isRoot); 602 String uuid = StringUtils.substringAfter(id, "://"); 603 String collectionName = Archivers.xpathEvalNonEmpty("resource-collection/name", propertiesXml); 604 _logger.info("Creating a ResourcesCollection object for '{}' folder (id={})", folder, id); 605 606 try 607 { 608 Node resourceCollection = _createChildResourceCollection(parentNode, uuid, collectionName); 609 if (isRoot) 610 { 611 _root = _jcrResourcesCollectionFactory.getAmetysObject(resourceCollection, null); 612 } 613 return resourceCollection; 614 } 615 catch (RepositoryException e) 616 { 617 throw new IOException(e); 618 } 619 } 620 621 private boolean _isRoot(Path folder) 622 { 623 String folderPath = StringUtils.strip(folder.toString(), "/"); 624 String commonPrefixToCompare = StringUtils.strip(_commonPrefix, "/"); 625 return commonPrefixToCompare.equals(folderPath); 626 } 627 628 private Node _createChildResourceCollection(Node parentNode, String uuid, String collectionName) throws RepositoryException 629 { 630 // Create a Node with JCR primary type "ametys:resources-collection" 631 // But then call 'replaceNodeWithDesiredUuid' to have it with the desired UUID (srcNode will be removed) 632 Node srcNode = parentNode.addNode(collectionName, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE); 633 Node nodeWithDesiredUuid = Archivers.replaceNodeWithDesiredUuid(srcNode, uuid); 634 return nodeWithDesiredUuid; 635 } 636 637 private String _relativePath(Path folderOrFile) 638 { 639 // for instance, _commonPrefix="resources/" 640 // relPath=folderOrFile.toString()="/resources/foo/bar" 641 // it should return "foo/bar" 642 643 // for instance, _commonPrefix="resources/" 644 // relPath=folderOrFile.toString()="/resources" 645 // it should return "" 646 647 String commonPrefixToRemove = "/" + StringUtils.strip(_commonPrefix, "/"); 648 String relPath = folderOrFile.toString(); 649 relPath = relPath.startsWith(commonPrefixToRemove) 650 ? StringUtils.substringAfter(relPath, commonPrefixToRemove) 651 : relPath; 652 relPath = StringUtils.strip(relPath, "/"); 653 return relPath; 654 } 655 656 private Node _retrieveParentJcrNode(Path fileOrFolder, boolean isRoot) 657 { 658 if (isRoot) 659 { 660 // is the root, thus return the parent of the root 661 return _parentOfRoot; 662 } 663 664 if (_root == null) 665 { 666 throw new IllegalStateException("Unexpected error, the root must have been created before."); 667 } 668 669 Path parent = fileOrFolder.getParent(); 670 String parentRelPath = _relativePath(parent); 671 return parentRelPath.isEmpty() 672 ? _root.getNode() 673 : _jcrResourcesCollectionFactory.<JCRResourcesCollection>getChild(_root, parentRelPath).getNode(); 674 } 675 676 private Optional<JCRResource> _importResource(Path file) throws ImportGlobalFailException 677 { 678 return _unitaryResourceImporter.unitaryImport(_zipArchivePath, file, _merger, _logger); 679 } 680 681 private Document _getFilePropertiesXml(Path file) throws IOException 682 { 683 String zipEntryPath = new StringBuilder() 684 .append(ArchiveHandler.METADATA_PREFIX) 685 .append(StringUtils.strip(file.toString(), "/")) 686 .append("_") 687 .append(__PROPERTIES_METADATA_XML_FILE_NAME_SUFFIX) 688 .toString(); 689 try (InputStream stream = ZipEntryHelper.zipEntryFileInputStream(_zipArchivePath, zipEntryPath)) 690 { 691 Document doc = _builder.parse(stream); 692 return doc; 693 } 694 catch (SAXException e) 695 { 696 throw new IOException(e); 697 } 698 } 699 700 private JCRResource _createdResource(Path file, String id, Document propertiesXml) throws IOException, AmetysObjectNotImportedException 701 { 702 Node parentNode = _retrieveParentJcrNode(file, false); 703 String uuid = StringUtils.substringAfter(id, "://"); 704 String resourceName = file.getFileName().toString(); 705 _logger.info("Creating a Resource object for '{}' file (id={})", file, id); 706 707 try 708 { 709 Node resourceNode = _createChildResource(parentNode, uuid, resourceName); 710 _setResourceData(resourceNode, file, propertiesXml); 711 _setResourceProperties(resourceNode, propertiesXml); 712 _setResourceMetadata(resourceNode, file); 713 714 JCRResource createdResource = _resolveResource(resourceNode); 715 return createdResource; 716 } 717 catch (TransformerException | RepositoryException e) 718 { 719 throw new IOException(e); 720 } 721 } 722 723 private Node _createChildResource(Node parentNode, String uuid, String resourceName) throws RepositoryException 724 { 725 // Create a Node with JCR primary type "ametys:resource" 726 // But then call 'replaceNodeWithDesiredUuid' to have it with the desired UUID (srcNode will be removed) 727 Node srcNode = parentNode.addNode(resourceName, "ametys:resource"); 728 Node nodeWithDesiredUuid = Archivers.replaceNodeWithDesiredUuid(srcNode, uuid); 729 return nodeWithDesiredUuid; 730 } 731 732 private JCRResource _resolveResource(Node resourceNode) 733 { 734 return _jcrResourceFactory.getAmetysObject(resourceNode, null); 735 } 736 737 private void _setResourceData(Node resourceNode, Path file, Document propertiesXml) throws RepositoryException, IOException, TransformerException 738 { 739 Node resourceContentNode = resourceNode.addNode("jcr:content", "nt:resource"); 740 741 String mimeType = _getMimeType(file); 742 resourceContentNode.setProperty("jcr:mimeType", mimeType); 743 744 try (InputStream stream = ZipEntryHelper.zipEntryFileInputStream(_zipArchivePath, file.toString())) 745 { 746 Binary binary = resourceNode.getSession() 747 .getValueFactory() 748 .createBinary(stream); 749 resourceContentNode.setProperty("jcr:data", binary); 750 } 751 752 Date lastModified = Objects.requireNonNull(DomNodeHelper.nullableDatetimeValue(propertiesXml, "resource/lastModified")); 753 Calendar lastModifiedCal = new GregorianCalendar(); 754 lastModifiedCal.setTime(lastModified); 755 resourceContentNode.setProperty("jcr:lastModified", lastModifiedCal); 756 } 757 758 private void _setResourceProperties(Node resourceNode, Document propertiesXml) throws TransformerException, AmetysObjectNotImportedException, RepositoryException 759 { 760 UserIdentity contributor = UserIdentity.stringToUserIdentity(Archivers.xpathEvalNonEmpty("resource/contributor", propertiesXml)); 761 Node lastContributorNode = resourceNode.addNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + JCRResource.CONTRIBUTOR_NODE_NAME, RepositoryConstants.USER_NODETYPE); 762 lastContributorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", contributor.getLogin()); 763 lastContributorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", contributor.getPopulationId()); 764 765 UserIdentity creator = UserIdentity.stringToUserIdentity(Archivers.xpathEvalNonEmpty("resource/creator", propertiesXml)); 766 Node creatorNode = resourceNode.addNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + JCRResource.CREATOR_NODE_NAME, RepositoryConstants.USER_NODETYPE); 767 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", creator.getLogin()); 768 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", creator.getPopulationId()); 769 770 Date creationDate = Objects.requireNonNull(DomNodeHelper.nullableDatetimeValue(propertiesXml, "resource/creationDate")); 771 Calendar creationDateCal = new GregorianCalendar(); 772 creationDateCal.setTime(creationDate); 773 resourceNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + JCRResource.CREATION_DATE, creationDateCal); 774 } 775 776 private void _setResourceMetadata(Node resourceNode, Path file) throws IOException 777 { 778 ModifiableDublinCoreAwareAmetysObject dcObject = _jcrResourceFactory.getAmetysObject(resourceNode, null); 779 _setDublinCoreMetadata(dcObject, file); 780 } 781 782 private void _setDublinCoreMetadata(ModifiableDublinCoreAwareAmetysObject dcObject, Path file) throws IOException 783 { 784 String zipEntryPath = new StringBuilder() 785 .append(ArchiveHandler.METADATA_PREFIX) 786 .append(StringUtils.strip(file.toString(), "/")) 787 .append("_") 788 .append(__DC_METADATA_XML_FILE_NAME_SUFFIX) 789 .toString(); 790 try (InputStream stream = ZipEntryHelper.zipEntryFileInputStream(_zipArchivePath, zipEntryPath)) 791 { 792 Document doc = _builder.parse(stream); 793 _setDublinCoreMetadata(dcObject, doc); 794 } 795 catch (SAXException | TransformerException e) 796 { 797 throw new IOException(e); 798 } 799 } 800 801 private void _setDublinCoreMetadata(ModifiableDublinCoreAwareAmetysObject dcObject, Document doc) throws TransformerException 802 { 803 org.w3c.dom.Node dcNode = XPathAPI.selectSingleNode(doc, __DC_METADATA_XML_EXPORT_TAG_NAME_ROOT); 804 dcObject.setDCTitle(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_TITLE)); 805 dcObject.setDCCreator(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_CREATOR)); 806 dcObject.setDCSubject(DomNodeHelper.stringValues(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_SUBJECT)); 807 dcObject.setDCDescription(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_DESCRIPTION)); 808 dcObject.setDCPublisher(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_PUBLISHER)); 809 dcObject.setDCContributor(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_CONTRIBUTOR)); 810 dcObject.setDCDate(DomNodeHelper.nullableDateValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_DATE)); 811 dcObject.setDCType(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_TYPE)); 812 dcObject.setDCFormat(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_FORMAT)); 813 dcObject.setDCIdentifier(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_IDENTIFIER)); 814 dcObject.setDCSource(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_SOURCE)); 815 dcObject.setDCLanguage(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_LANGUAGE)); 816 dcObject.setDCRelation(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_RELATION)); 817 dcObject.setDCCoverage(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_COVERAGE)); 818 dcObject.setDCRights(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_RIGHTS)); 819 } 820 821 private String _getMimeType(Path file) 822 { 823 return Optional.of(file) 824 .map(Path::getFileName) 825 .map(Path::toString) 826 .map(String::toLowerCase) 827 .map(_cocoonContext::getMimeType) 828 .orElse("application/unknown"); 829 } 830 } 831}