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