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}