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