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.cms.data.type; 017 018import java.io.ByteArrayInputStream; 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.StringReader; 022import java.net.URL; 023import java.nio.charset.StandardCharsets; 024import java.time.ZoneId; 025import java.time.ZonedDateTime; 026import java.time.format.DateTimeFormatter; 027import java.util.Calendar; 028import java.util.GregorianCalendar; 029import java.util.LinkedHashMap; 030import java.util.Map; 031import java.util.Optional; 032import java.util.function.Function; 033import java.util.stream.Stream; 034 035import org.apache.cocoon.environment.Context; 036import org.apache.cocoon.xml.AttributesImpl; 037import org.apache.cocoon.xml.XMLUtils; 038import org.apache.commons.io.IOUtils; 039import org.apache.commons.lang3.StringUtils; 040import org.apache.commons.lang3.tuple.ImmutableTriple; 041import org.apache.commons.lang3.tuple.Triple; 042import org.apache.tika.Tika; 043import org.apache.tika.mime.MimeType; 044import org.apache.tika.mime.MimeTypeException; 045import org.apache.tika.mime.MimeTypes; 046import org.w3c.dom.Element; 047import org.xml.sax.ContentHandler; 048import org.xml.sax.SAXException; 049 050import org.ametys.cms.data.Binary; 051import org.ametys.cms.data.File; 052import org.ametys.cms.data.NamedResource; 053import org.ametys.cms.data.Resource; 054import org.ametys.cms.data.RichText; 055import org.ametys.cms.transformation.xslt.ResolveURIComponent; 056import org.ametys.core.model.type.ModelItemTypeHelper; 057import org.ametys.core.upload.Upload; 058import org.ametys.core.util.DateUtils; 059import org.ametys.plugins.repository.AmetysRepositoryException; 060import org.ametys.plugins.repository.RepositoryConstants; 061import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData; 062import org.ametys.plugins.repository.data.repositorydata.RepositoryData; 063import org.ametys.runtime.model.compare.DataChangeType; 064import org.ametys.runtime.model.compare.DataChangeTypeDetail; 065import org.ametys.runtime.model.exception.BadItemTypeException; 066import org.ametys.runtime.model.type.DataContext; 067 068/** 069 * Helper for resource type of elements stored in the repository 070 */ 071public final class ResourceElementTypeHelper 072{ 073 /** Prefix to use for resource's metadata */ 074 public static final String METADATA_PREFIX = "jcr"; 075 076 /** Mime type metadata identifier */ 077 public static final String MIME_TYPE_IDENTIFIER = "mimeType"; 078 079 /** Encoding metadata identifier */ 080 public static final String ENCODING_IDENTIFIER = "encoding"; 081 082 /** Last modification date metadata identifier */ 083 public static final String LAST_MODIFICATION_DATE_IDENTIFIER = "lastModified"; 084 085 /** Identifier of the data containing the resource's data */ 086 public static final String DATA_IDENTIFIER = "data"; 087 088 /** hash metadata identifier */ 089 public static final String HASH_IDENTIFIER = "hash"; 090 091 /** file name metadata identifier */ 092 public static final String FILENAME_IDENTIFIER = "filename"; 093 094 /** Identifier of the data to check to know if the resource is empty */ 095 public static final String EMPTY_RESOURCE_IDENTIFIER = "isEmpty"; 096 097 private ResourceElementTypeHelper() 098 { 099 // Empty constructor 100 } 101 102 /** 103 * Converts the given string to a {@link Binary} 104 * @param value the string to convert 105 * @return the converted {@link Binary} 106 */ 107 public static Binary convertStringToBinary(String value) 108 { 109 try 110 { 111 byte [] valueAsByteArray = IOUtils.toByteArray(new StringReader(value), StandardCharsets.UTF_8); 112 Binary binary = new Binary(); 113 binary.setInputStream(new ByteArrayInputStream(valueAsByteArray)); 114 binary.setMimeType("text/plain"); 115 binary.setLastModificationDate(ZonedDateTime.now()); 116 binary.setFilename("no-name.txt"); 117 118 return binary; 119 } 120 catch (IOException e) 121 { 122 throw new IllegalArgumentException("Unable to convert the given value to a Binary", e); 123 } 124 } 125 126 /** 127 * Converts the given byte array to a {@link Binary} 128 * @param value the byte array to convert 129 * @return the converted {@link Binary} 130 */ 131 public static Binary convertByteArrayToBinary(byte[] value) 132 { 133 try 134 { 135 ByteArrayInputStream byteArrayInputStreamForTika = new ByteArrayInputStream(value); 136 Tika tika = new Tika(); 137 String mediaTypeAsString = tika.detect(byteArrayInputStreamForTika); 138 MimeType mimeType = MimeTypes.getDefaultMimeTypes().forName(mediaTypeAsString); 139 140 Binary binary = new Binary(); 141 binary.setInputStream(new ByteArrayInputStream(value)); 142 binary.setMimeType(mediaTypeAsString); 143 binary.setLastModificationDate(ZonedDateTime.now()); 144 binary.setFilename("no-name" + mimeType.getExtension()); 145 146 return binary; 147 } 148 catch (MimeTypeException | IOException e) 149 { 150 throw new IllegalArgumentException("Unable to convert the given value to a Binary", e); 151 } 152 } 153 154 /** 155 * Convert the single binary into a JSON object 156 * @param binary the binary to convert 157 * @param binaryType the type of the binary 158 * @param context The context of the binary to convert 159 * @return The file as JSON 160 */ 161 public static Map<String, Object> singleBinaryToJSON(Binary binary, String binaryType, DataContext context) 162 { 163 return ResourceElementTypeHelper.singleFileToJSON(binary, binaryType, context.getDataPath(), _getBinaryURI(context)); 164 } 165 166 /** 167 * Convert the single file into a JSON object 168 * @param file the file to convert 169 * @param fileType the type of the file 170 * @param path the path of file (ex: data path for a binary or resource id for an explorer file) 171 * @param fileURI uri of the file to give to the {@link ResolveURIComponent} 172 * @return The file as JSON 173 */ 174 public static Map<String, Object> singleFileToJSON(File file, String fileType, String path, String fileURI) 175 { 176 Map<String, Object> fileInfos = new LinkedHashMap<>(); 177 178 Optional.ofNullable(file.getMimeType()).ifPresent(mimeType -> fileInfos.put("mimeType", mimeType)); 179 Optional.ofNullable(file.getLastModificationDate()).ifPresent(lastModificationDate -> fileInfos.put("lastModified", lastModificationDate.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))); 180 181 fileInfos.put("size", String.valueOf(file.getLength())); 182 if (path != null) 183 { 184 fileInfos.put("path", path); 185 } 186 fileInfos.put("type", fileType); 187 Optional.ofNullable(file.getName()).ifPresent(filename -> fileInfos.put("filename", filename)); 188 189 String viewUrl = ResolveURIComponent.resolveBoundedImage(fileType, fileURI, 100, 100); 190 String downloadUrl = ResolveURIComponent.resolve(fileType, fileURI, true); 191 192 fileInfos.put("viewUrl", viewUrl); 193 fileInfos.put("downloadUrl", downloadUrl); 194 195 return fileInfos; 196 } 197 198 /** 199 * Sets the given resource's data using the given DOM element and additional data 200 * @param resource the resource 201 * @param element the DOM element 202 * @param additionalData additional data containing the input stream 203 * @param context the Cocoon's context, used to retrieve mime types from file names if needed 204 */ 205 public static void resourceFromXML(NamedResource resource, Element element, Optional<Object> additionalData, Context context) 206 { 207 String urlValue = element.getTextContent().trim(); 208 String filename = null; 209 if (StringUtils.isNotEmpty(urlValue)) 210 { 211 try 212 { 213 URL url = new URL(urlValue); 214 215 try (InputStream is = url.openStream()) 216 { 217 resource.setInputStream(is); 218 } 219 220 String path = url.getPath(); 221 filename = path.substring(path.lastIndexOf("/") + 1); 222 } 223 catch (IOException e) 224 { 225 throw new IllegalArgumentException("Unable to get the resource value from the given XML", e); 226 } 227 } 228 else 229 { 230 filename = element.getAttribute("filename"); 231 232 @SuppressWarnings("unchecked") 233 Map<String, InputStream> files = additionalData.isPresent() ? (Map<String, InputStream>) additionalData.get() : Map.of(); 234 if (files.containsKey(filename)) 235 { 236 try 237 { 238 InputStream inputStream = files.get(filename); 239 resource.setInputStream(inputStream); 240 } 241 catch (IOException e) 242 { 243 throw new IllegalArgumentException("Unable to get the resource value from the given XML", e); 244 } 245 } 246 } 247 248 resource.setFilename(filename); 249 250 String lastModified = element.getAttribute("lastModified"); 251 ZonedDateTime time = StringUtils.isEmpty(lastModified) ? ZonedDateTime.now() : ZonedDateTime.parse(lastModified, DateTimeFormatter.ISO_OFFSET_DATE_TIME); 252 resource.setLastModificationDate(time); 253 254 String mimeType = element.getAttribute("mime-type"); 255 if (StringUtils.isEmpty(mimeType)) 256 { 257 mimeType = context.getMimeType(filename.toLowerCase()); 258 259 if (mimeType == null) 260 { 261 mimeType = "application/unknown"; 262 } 263 } 264 resource.setMimeType(mimeType); 265 } 266 267 /** 268 * Converts an uploaded file to a {@link Binary}. 269 * @param upload the uploaded file. 270 * @return the newly created {@link Binary}. 271 */ 272 public static Binary binaryFromUpload(Upload upload) 273 { 274 Binary binary = new Binary(); 275 276 binary.setFilename(upload.getFilename()); 277 binary.setMimeType(upload.getMimeType()); 278 binary.setLastModificationDate(upload.getUploadedDate()); 279 280 try 281 { 282 binary.setInputStream(upload.getInputStream()); 283 } 284 catch (IOException e) 285 { 286 throw new RuntimeException("Unable to set binary data", e); 287 } 288 289 return binary; 290 } 291 292 /** 293 * Generates SAX events for the given single binary 294 * @param contentHandler the {@link ContentHandler} that will receive the SAX events 295 * @param tagName the tag name of the SAX event to generate. 296 * @param binary the single file to SAX 297 * @param binaryType the type of the file 298 * @param context The context of the binary to convert 299 * @param attributes the attributes for the SAX event to generate 300 * @throws SAXException if an error occurs during the SAX events generation 301 */ 302 public static void singleBinaryToSAX(ContentHandler contentHandler, String tagName, Binary binary, String binaryType, DataContext context, AttributesImpl attributes) throws SAXException 303 { 304 singleFileToSAX(contentHandler, tagName, binary, binaryType, context.getDataPath(), _getBinaryURI(context), attributes); 305 } 306 307 private static String _getBinaryURI(DataContext context) 308 { 309 String dataPath = context.getDataPath(); 310 String uri = context.getObjectId() 311 .map(id -> dataPath + "?objectId=" + id) 312 .orElse(dataPath); 313 return uri; 314 } 315 316 /** 317 * Generates SAX events for the given single file 318 * @param contentHandler the {@link ContentHandler} that will receive the SAX events 319 * @param tagName the tag name of the SAX event to generate. 320 * @param file the single file to SAX 321 * @param fileType the type of the file 322 * @param path the path of file (ex: data path for a binary or resource id for an explorer file) 323 * @param fileURI uri of the file 324 * @param attributes the attributes for the SAX event to generate 325 * @throws SAXException if an error occurs during the SAX events generation 326 */ 327 public static void singleFileToSAX(ContentHandler contentHandler, String tagName, File file, String fileType, String path, String fileURI, AttributesImpl attributes) throws SAXException 328 { 329 AttributesImpl localAttributes = new AttributesImpl(attributes); 330 331 Optional.ofNullable(file.getMimeType()) 332 .ifPresent(mimeType -> localAttributes.addCDATAAttribute("mime-type", mimeType)); 333 334 Optional.ofNullable(file.getLastModificationDate()) 335 .map(DateUtils::zonedDateTimeToString) 336 .ifPresent(lastModified -> localAttributes.addCDATAAttribute("lastModified", lastModified)); 337 338 Optional.ofNullable(file.getName()) 339 .ifPresent(filename -> localAttributes.addCDATAAttribute("filename", filename)); 340 341 localAttributes.addCDATAAttribute("type", fileType); 342 localAttributes.addCDATAAttribute("size", String.valueOf(file.getLength())); 343 localAttributes.addCDATAAttribute("path", path); 344 localAttributes.addCDATAAttribute("uri", fileURI); 345 346 XMLUtils.createElement(contentHandler, tagName, localAttributes); 347 } 348 349 /** 350 * Checks if the given resource data is empty. A resource data is considered as empty if it has no stream data 351 * @param resourceData the resource data to check 352 * @return <code>true</code> if the resource has is empty, <code>false</code> otherwise 353 */ 354 public static boolean isResourceDataEmpty(RepositoryData resourceData) 355 { 356 if (resourceData.hasValue(EMPTY_RESOURCE_IDENTIFIER, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL)) 357 { 358 return resourceData.getBoolean(EMPTY_RESOURCE_IDENTIFIER, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL); 359 } 360 else 361 { 362 return false; 363 } 364 } 365 366 /** 367 * Read the binary from the given repository data 368 * @param binaryData the repository data containing the binary's data 369 * @return the read binary 370 */ 371 public static Binary readBinaryData(RepositoryData binaryData) 372 { 373 String hash = getStringValue(binaryData, HASH_IDENTIFIER, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL); 374 Binary binary = new Binary(binaryData, hash); 375 376 readResourceData(binaryData, binary); 377 Optional.ofNullable(getStringValue(binaryData, FILENAME_IDENTIFIER, RepositoryConstants.NAMESPACE_PREFIX)).ifPresent(binary::setFilename); 378 379 return binary; 380 } 381 382 /** 383 * Read the resource from the given repository data 384 * @param resourceData the repository data containing the resource's data 385 * @param resource the resource to read 386 */ 387 public static void readResourceData(RepositoryData resourceData, Resource resource) 388 { 389 Optional.ofNullable(getStringValue(resourceData, MIME_TYPE_IDENTIFIER, METADATA_PREFIX)).ifPresent(resource::setMimeType); 390 Optional.ofNullable(getStringValue(resourceData, ENCODING_IDENTIFIER, METADATA_PREFIX)).ifPresent(resource::setEncoding); 391 Optional.ofNullable(getDateValue(resourceData, LAST_MODIFICATION_DATE_IDENTIFIER, METADATA_PREFIX)).ifPresent(resource::setLastModificationDate); 392 } 393 394 /** 395 * Empties the resource data with the given name. A resource data is considered as empty if it has no stream data 396 * @param parentResourceData the parent of the resource data to empty 397 * @param name the name of the resource data 398 * @param nodeType the node type of the resource data 399 */ 400 public static void emptyResourceData(ModifiableRepositoryData parentResourceData, String name, String nodeType) 401 { 402 ModifiableRepositoryData resourceData = parentResourceData.hasValue(name) ? parentResourceData.getRepositoryData(name) : parentResourceData.addRepositoryData(name, nodeType); 403 404 try (ByteArrayInputStream is = new ByteArrayInputStream(StringUtils.EMPTY.getBytes())) 405 { 406 resourceData.setValue(DATA_IDENTIFIER, is, METADATA_PREFIX); 407 } 408 catch (IOException e) 409 { 410 throw new AmetysRepositoryException(e); 411 } 412 413 resourceData.setValue(EMPTY_RESOURCE_IDENTIFIER, true, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL); 414 } 415 416 /** 417 * Write the resource in the given repository data 418 * @param parentData the repository data where to write the binary 419 * @param name the name of the element to write 420 * @param value the binary to write 421 */ 422 public static void writeSingleBinaryValue(ModifiableRepositoryData parentData, String name, Binary value) 423 { 424 ModifiableRepositoryData binaryData; 425 if (parentData.hasValue(name)) 426 { 427 binaryData = getRepositoryData(parentData, RepositoryConstants.BINARY_NODETYPE, name, RepositoryConstants.NAMESPACE_PREFIX); 428 } 429 else 430 { 431 binaryData = parentData.addRepositoryData(name, RepositoryConstants.BINARY_NODETYPE); 432 } 433 434 if (value != null) 435 { 436 binaryData.setValue(EMPTY_RESOURCE_IDENTIFIER, false, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL); 437 438 writeResourceData(binaryData, value); 439 Optional.ofNullable(value.getFilename()).ifPresent(filename -> binaryData.setValue(FILENAME_IDENTIFIER, filename, RepositoryConstants.NAMESPACE_PREFIX)); 440 441 if (binaryData.hasValue(HASH_IDENTIFIER, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL)) 442 { 443 String currentHash = getStringValue(binaryData, HASH_IDENTIFIER, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL); 444 if (!currentHash.equals(value.getHash())) 445 { 446 Optional.ofNullable(value.getHash()).ifPresent(hash -> binaryData.setValue(HASH_IDENTIFIER, hash, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL)); 447 Optional.ofNullable(value.getInputStream()).ifPresent(stream -> binaryData.setValue(DATA_IDENTIFIER, stream, METADATA_PREFIX)); 448 } 449 } 450 else 451 { 452 Optional.ofNullable(value.getHash()).ifPresent(hash -> binaryData.setValue(HASH_IDENTIFIER, hash, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL)); 453 Optional.ofNullable(value.getInputStream()).ifPresent(stream -> binaryData.setValue(DATA_IDENTIFIER, stream, METADATA_PREFIX)); 454 } 455 } 456 } 457 458 /** 459 * Write the resource in the given repository data 460 * @param resourceData the repository data where to write the resource's data 461 * @param value the resource to write 462 */ 463 public static void writeResourceData(ModifiableRepositoryData resourceData, Resource value) 464 { 465 Optional.ofNullable(value.getMimeType()).ifPresent(mimeType -> resourceData.setValue(MIME_TYPE_IDENTIFIER, mimeType, METADATA_PREFIX)); 466 Optional.ofNullable(value.getEncoding()).ifPresent(encoding -> resourceData.setValue(ENCODING_IDENTIFIER, encoding, METADATA_PREFIX)); 467 Optional.ofNullable(value.getLastModificationDate()).ifPresent(lastModificationDate -> resourceData.setValue(LAST_MODIFICATION_DATE_IDENTIFIER, _getCalendarFromZonedDateTime(lastModificationDate), METADATA_PREFIX)); 468 } 469 470 /** 471 * Compare the given single binaries and retrieves the changes as a stream of {@link Triple}s. The {@link Triple} contains: 472 * <ul> 473 * <li>the general type of the change (added, modified or removed) as a {@link DataChangeType},</li> 474 * <li>some details about this change if possible (after or before for a date, more or less for a number, ...) as a {@link DataChangeTypeDetail}</li> 475 * <li>The data concerned by this change if not the element itself (or an empty String)</li> 476 * </ul> 477 * @param binary1 the 1st single binary 478 * @param binary2 the 2nd single binary 479 * @return the changes between the two given single binaries as a stream of {@link Triple}s. Retrieves an empty stream if there is no change 480 */ 481 public static Stream<Triple<DataChangeType, DataChangeTypeDetail, String>> compareSingleBinaries(Binary binary1, Binary binary2) 482 { 483 try 484 { 485 return Stream.of 486 ( 487 // Get changes about the binary's metadata 488 _compareSingleResourcesMetadata(binary1, binary2), 489 490 // Get changes about the binary's file name 491 ModelItemTypeHelper.compareSingleObjects(binary1.getFilename(), binary2.getFilename(), "fileName") 492 .map(Stream::of) 493 .orElseGet(Stream::empty), 494 495 // Get changes about the binary's content 496 _compareSingleResourcesContent(binary1, binary2) 497 ) 498 .flatMap(Function.identity()); 499 } 500 catch (IOException e) 501 { 502 throw new IllegalArgumentException("Unable to compare the two given binaries", e); 503 } 504 } 505 506 /** 507 * Compare the given single rich texts and retrieves the changes as a stream of {@link Triple}s. The {@link Triple} contains: 508 * <ul> 509 * <li>the general type of the change (added, modified or removed) as a {@link DataChangeType},</li> 510 * <li>some details about this change if possible (after or before for a date, more or less for a number, ...) as a {@link DataChangeTypeDetail}</li> 511 * <li>The data concerned by this change if not the element itself (or an empty String)</li> 512 * </ul> 513 * @param richText1 the 1st single rich text 514 * @param richText2 the 2nd single rich text 515 * @return the changes between the two given single rich texts as a stream of {@link Triple}s. Retrieves an empty stream if there is no change 516 */ 517 public static Stream<Triple<DataChangeType, DataChangeTypeDetail, String>> compareSingleRichTexts(RichText richText1, RichText richText2) 518 { 519 try 520 { 521 // Note: folders are not compared, they are used for pictures and id of the pictures are in the InputStream, so if something changed, the InputStream changed. 522 return Stream.of 523 ( 524 _compareSingleResourcesMetadata(richText1, richText2), 525 _compareSingleResourcesContent(richText1, richText2) 526 ) 527 .flatMap(Function.identity()); 528 } 529 catch (IOException e) 530 { 531 throw new IllegalArgumentException("Unable to compare the two given rich texts", e); 532 } 533 } 534 535 /** 536 * Compare the metadata of the given single resources and retrieves the changes as a stream of {@link Triple}s. The {@link Triple}s contain: 537 * <ul> 538 * <li>the general type of the change (added, modified or removed) as a {@link DataChangeType},</li> 539 * <li>some details about this change if possible (after or before for a date, more or less for a number, ...) as a {@link DataChangeTypeDetail}</li> 540 * <li>The data concerned by this change if not the element itself (or an empty String)</li> 541 * </ul> 542 * @param resource1 the 1st single resource 543 * @param resource2 the 2nd single resource 544 * @return the changes between the metadata of the two given single resources as a stream of {@link Triple}s. Retrieves an empty stream if there is no change 545 * @throws IOException if an error occurs while reading the resources' data 546 */ 547 protected static Stream<Triple<DataChangeType, DataChangeTypeDetail, String>> _compareSingleResourcesMetadata(Resource resource1, Resource resource2) throws IOException 548 { 549 return Stream.of 550 ( 551 ModelItemTypeHelper.compareSingleObjects(resource1.getEncoding(), resource2.getEncoding(), ENCODING_IDENTIFIER), 552 ModelItemTypeHelper.compareSingleObjects(resource1.getMimeType(), resource2.getMimeType(), MIME_TYPE_IDENTIFIER) 553 ) 554 .filter(Optional::isPresent) 555 .map(Optional::get); 556 } 557 558 /** 559 * Compare the content of the given single resources and retrieves the changes as a stream of {@link Triple}. The {@link Triple}s contain: 560 * <ul> 561 * <li>the general type of the change (added, modified or removed) as a {@link DataChangeType},</li> 562 * <li>some details about this change if possible (after or before for a date, more or less for a number, ...) as a {@link DataChangeTypeDetail}</li> 563 * <li>The data concerned by this change if not the element itself (or an empty String)</li> 564 * </ul> 565 * @param resource1 the 1st single resource 566 * @param resource2 the 2nd single resource 567 * @return the changes between the content of the two given single resources as a stream of {@link Triple}s. Retrieves an empty stream if there is no change 568 * @throws IOException if an error occurs while reading the resources' data 569 */ 570 protected static Stream<Triple<DataChangeType, DataChangeTypeDetail, String>> _compareSingleResourcesContent(Resource resource1, Resource resource2) throws IOException 571 { 572 try ( 573 InputStream is1 = resource1.getInputStream(); 574 InputStream is2 = resource2.getInputStream(); 575 ) 576 { 577 if (!IOUtils.contentEquals(is1, is2)) 578 { 579 return Stream.of(new ImmutableTriple<>(DataChangeType.MODIFIED, DataChangeTypeDetail.NONE, "inputStream")); 580 } 581 else 582 { 583 return Stream.empty(); 584 } 585 } 586 } 587 588 /** 589 * Retrieves the string value from the given repository data 590 * @param repositoryData the repository data containing the data to retrieve 591 * @param name the name of the data to retrieve 592 * @param prefix the prefix of the data to retrieve 593 * @return the string value 594 */ 595 public static String getStringValue(RepositoryData repositoryData, String name, String prefix) 596 { 597 if (!repositoryData.hasValue(name, prefix)) 598 { 599 return null; 600 } 601 else if (RepositoryData.STRING_REPOSITORY_DATA_TYPE.equals(repositoryData.getType(name, prefix))) 602 { 603 return repositoryData.getString(name, prefix); 604 } 605 else 606 { 607 throw new BadItemTypeException("Try to get string value from the non string data '" + prefix + ":" + name + "' on '" + repositoryData + "'"); 608 } 609 } 610 611 /** 612 * Retrieves the string values from the given repository data 613 * @param repositoryData the repository data containing the data to retrieve 614 * @param name the name of the data to retrieve 615 * @param prefix the prefix of the data to retrieve 616 * @return the string values 617 */ 618 public static String[] getStringValues(RepositoryData repositoryData, String name, String prefix) 619 { 620 if (!repositoryData.hasValue(name, prefix)) 621 { 622 return null; 623 } 624 else if (RepositoryData.STRING_REPOSITORY_DATA_TYPE.equals(repositoryData.getType(name, prefix))) 625 { 626 return repositoryData.getStrings(name, prefix); 627 } 628 else 629 { 630 throw new BadItemTypeException("Try to get string value from the non string data '" + prefix + ":" + name + "' on '" + repositoryData + "'"); 631 } 632 } 633 634 /** 635 * Retrieves the child repository data from the given repository data 636 * @param <T> Type of the repository data (modifiable or not) 637 * @param parentData the repository data containing the data to retrieve 638 * @param dataTypeName the type of the data to retrieve 639 * @param name the name of the data to retrieve 640 * @param prefix the prefix of the data to retrieve 641 * @return the child repository data 642 */ 643 @SuppressWarnings("unchecked") 644 public static <T extends RepositoryData> T getRepositoryData(T parentData, String dataTypeName, String name, String prefix) 645 { 646 if (!parentData.hasValue(name, prefix)) 647 { 648 return null; 649 } 650 else if (dataTypeName.equals(parentData.getType(name, prefix))) 651 { 652 return (T) parentData.getRepositoryData(name, prefix); 653 } 654 else 655 { 656 throw new BadItemTypeException("Try to get data of type '" + dataTypeName + "' from the non data '" + prefix + ":" + name + "' on '" + parentData + "'"); 657 } 658 } 659 660 /** 661 * Retrieves the date value from the given repository data 662 * @param repositoryData the repository data containing data to retrieve 663 * @param prefix the prefix of the data to retrieve 664 * @param name the name of the data to retrieve 665 * @return the date value 666 */ 667 public static ZonedDateTime getDateValue(RepositoryData repositoryData, String name, String prefix) 668 { 669 if (!repositoryData.hasValue(name, prefix)) 670 { 671 return null; 672 } 673 else if (RepositoryData.CALENDAR_REPOSITORY_DATA_TYPE.equals(repositoryData.getType(name, prefix))) 674 { 675 return DateUtils.asZonedDateTime(repositoryData.getDate(name, prefix)); 676 } 677 else 678 { 679 throw new BadItemTypeException("Try to get date value from the non date data '" + prefix + ":" + name + "' on '" + repositoryData + "'"); 680 } 681 } 682 683 private static Calendar _getCalendarFromZonedDateTime(ZonedDateTime zonedDateTime) 684 { 685 ZonedDateTime dateTimeOnUTC = zonedDateTime.withZoneSameInstant(ZoneId.of("UTC")); 686 return GregorianCalendar.from(dateTimeOnUTC); 687 } 688}