001/*
002 *  Copyright 2022 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.holder.impl;
017
018import java.lang.reflect.Array;
019import java.time.ZonedDateTime;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026
027import org.apache.commons.lang3.StringUtils;
028import org.apache.commons.lang3.tuple.Pair;
029import org.xml.sax.ContentHandler;
030import org.xml.sax.SAXException;
031
032import org.ametys.cms.data.ContentValue;
033import org.ametys.cms.data.ametysobject.ModelAwareDataAwareAmetysObject;
034import org.ametys.cms.data.holder.IndexableDataHolder;
035import org.ametys.cms.data.holder.group.IndexableComposite;
036import org.ametys.cms.data.holder.group.IndexableRepeater;
037import org.ametys.cms.data.holder.group.impl.DefaultModelAwareComposite;
038import org.ametys.cms.data.holder.group.impl.DefaultModelAwareRepeater;
039import org.ametys.cms.model.ContentElementDefinition;
040import org.ametys.cms.model.properties.Property;
041import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
042import org.ametys.core.util.DateUtils;
043import org.ametys.plugins.repository.AmetysObject;
044import org.ametys.plugins.repository.RepositoryConstants;
045import org.ametys.plugins.repository.data.DataComment;
046import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus;
047import org.ametys.plugins.repository.data.holder.DataHolder;
048import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
049import org.ametys.plugins.repository.data.holder.group.ModelAwareComposite;
050import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater;
051import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry;
052import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
053import org.ametys.plugins.repository.data.repositorydata.RepositoryData;
054import org.ametys.plugins.repository.data.type.RepositoryElementType;
055import org.ametys.plugins.repository.data.type.RepositoryModelItemGroupType;
056import org.ametys.plugins.repository.data.type.RepositoryModelItemType;
057import org.ametys.plugins.repository.model.CompositeDefinition;
058import org.ametys.plugins.repository.model.RepeaterDefinition;
059import org.ametys.runtime.model.ElementDefinition;
060import org.ametys.runtime.model.ModelHelper;
061import org.ametys.runtime.model.ModelItem;
062import org.ametys.runtime.model.ModelItemContainer;
063import org.ametys.runtime.model.ViewItemAccessor;
064import org.ametys.runtime.model.exception.BadDataPathCardinalityException;
065import org.ametys.runtime.model.exception.BadItemTypeException;
066import org.ametys.runtime.model.exception.UndefinedItemPathException;
067import org.ametys.runtime.model.type.DataContext;
068import org.ametys.runtime.model.type.ModelItemType;
069
070/**
071 * Default implementation for data holder with model
072 */
073public class DefaultModelAwareDataHolder implements IndexableDataHolder
074{
075    /** Repository data to use to store data in the repository */
076    protected RepositoryData _repositoryData;
077    
078    /** Parent of the current {@link DataHolder} */
079    protected Optional<? extends IndexableDataHolder> _parent;
080    
081    /** Root {@link DataHolder} */
082    protected IndexableDataHolder _root;
083    
084    /** Model containers to use to get information about definitions */
085    protected Collection<? extends ModelItemContainer> _itemContainers;
086    
087    /**
088     * Creates a default model aware data holder
089     * @param repositoryData the repository data to use
090     * @param itemContainers the model containers to use to get information about definitions. Must match the given repository data. A repository data can have several item containers. For example, a content can have several content types.
091     */
092    public DefaultModelAwareDataHolder(RepositoryData repositoryData, ModelItemContainer... itemContainers)
093    {
094        this(repositoryData, Optional.empty(), Optional.empty(), Arrays.asList(itemContainers));
095    }
096    
097    /**
098     * Creates a default model aware data holder
099     * @param repositoryData the repository data to use
100     * @param itemContainers the model containers to use to get information about definitions. Must match the given repository data. A repository data can have several item containers. For example, a content can have several content types.
101     */
102    public DefaultModelAwareDataHolder(RepositoryData repositoryData, Collection<? extends ModelItemContainer> itemContainers)
103    {
104        this(repositoryData, Optional.empty(), Optional.empty(), itemContainers);
105    }
106    
107    /**
108     * Creates a default model aware data holder
109     * @param repositoryData the repository data to use
110     * @param parent the optional parent of the created {@link DataHolder}, empty if the created {@link DataHolder} is the root {@link DataHolder}
111     * @param root the root {@link DataHolder}
112     * @param itemContainers the model containers to use to get information about definitions. Must match the given repository data. A repository data can have several item containers. For example, a content can have several content types.
113     */
114    public DefaultModelAwareDataHolder(RepositoryData repositoryData, Optional<? extends IndexableDataHolder> parent, Optional<? extends IndexableDataHolder> root, ModelItemContainer... itemContainers)
115    {
116        this(repositoryData, parent, root, Arrays.asList(itemContainers));
117    }
118    
119    /**
120     * Creates a default model aware data holder
121     * @param repositoryData the repository data to use
122     * @param parent the parent of the created {@link DataHolder}, empty if the created {@link DataHolder} is the root {@link DataHolder}
123     * @param root the root {@link DataHolder}
124     * @param itemContainers the model containers to use to get information about definitions. Must match the given repository data. A repository data can have several item containers. For example, a content can have several content types.
125     */
126    public DefaultModelAwareDataHolder(RepositoryData repositoryData, Optional<? extends IndexableDataHolder> parent, Optional<? extends IndexableDataHolder> root, Collection<? extends ModelItemContainer> itemContainers)
127    {
128        _repositoryData = repositoryData;
129        _itemContainers = itemContainers;
130        _ensureNonNullItemContainers();
131        
132        _parent = parent;
133        _root = root.map(IndexableDataHolder.class::cast)
134                    .or(() -> _parent.map(IndexableDataHolder::getRootDataHolder)) // if no root is specified but a parent, the root is the parent's root
135                    .orElse(this); // if no root or parent is specified, the root is the current DataHolder 
136
137    }
138    
139    private void _ensureNonNullItemContainers()
140    {
141        if (_itemContainers.contains(null))
142        {
143            throw new NullPointerException(String.format("Invalid item containers for creating DefaultModelAwareDataHolder, one of them is null: %s", _itemContainers));
144        }
145    }
146    
147    public IndexableComposite getComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
148    {
149        Object value = getValue(compositePath);
150        return _getCompositeFromValue(value, compositePath);
151    }
152    
153    public IndexableComposite getLocalComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
154    {
155        Object value = getLocalValue(compositePath);
156        return _getCompositeFromValue(value, compositePath);
157    }
158    
159    public IndexableComposite getExternalComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
160    {
161        Object value = getExternalValue(compositePath);
162        return _getCompositeFromValue(value, compositePath);
163    }
164    
165    private IndexableComposite _getCompositeFromValue(Object value, String compositePath)
166    {
167        if (value == null)
168        {
169            return null;
170        }
171        else if (value instanceof IndexableComposite composite)
172        {
173            return composite;
174        }
175        else
176        {
177            throw new BadItemTypeException("The item at path '" + compositePath + "' is not a composite.");
178        }
179    }
180
181    public IndexableRepeater getRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
182    {
183        Object value = getValue(repeaterPath);
184        return _getRepeaterFromValue(value, repeaterPath);
185    }
186    
187    public IndexableRepeater getLocalRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
188    {
189        Object value = getLocalValue(repeaterPath);
190        return _getRepeaterFromValue(value, repeaterPath);
191    }
192    
193    public IndexableRepeater getExternalRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
194    {
195        Object value = getExternalValue(repeaterPath);
196        return _getRepeaterFromValue(value, repeaterPath);
197    }
198    
199    private IndexableRepeater _getRepeaterFromValue(Object value, String repeaterPath)
200    {
201        if (value == null)
202        {
203            return null;
204        }
205        else if (value instanceof IndexableRepeater repeater)
206        {
207            return repeater;
208        }
209        else
210        {
211            throw new BadItemTypeException("The data at path '" + repeaterPath + "' is not a repeater.");
212        }
213    }
214    
215    public <T> T getValue(String dataPath, boolean allowMultiValuedPathSegments) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
216    {
217        return _getValue(dataPath, allowMultiValuedPathSegments, Optional.empty());
218    }
219    
220    public <T> T getLocalValue(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
221    {
222        return _getValue(dataPath, false, Optional.of(ExternalizableDataStatus.LOCAL));
223    }
224    
225    public <T> T getExternalValue(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
226    {
227        return _getValue(dataPath, false, Optional.of(ExternalizableDataStatus.EXTERNAL));
228    }
229    
230    private <T> T _getValue(String dataPath, boolean allowMultiValuedPathSegments, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
231    {
232        _checkDefinition(dataPath, status.isPresent(), "Unable to retrieve the value at path '" + dataPath + "'.");
233    
234        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
235        
236        if (pathSegments == null || pathSegments.length < 1)
237        {
238            throw new IllegalArgumentException("Unable to retrieve the data at the given path. This path is empty.");
239        }
240        else if (pathSegments.length == 1)
241        {
242            // Simple path => get the value
243            ModelItem modelItem = getDefinition(dataPath);
244            String dataName = _getFinalDataName(dataPath, status);
245            
246            if (modelItem instanceof Property property)
247            {
248                return _getPropertyValue(property);
249            }
250            else if (modelItem instanceof ElementDefinition elementDefinition)
251            {
252                return _getElementValue(elementDefinition, dataName);
253            }
254            else
255            {
256                return _getGroupValue(modelItem, dataName);
257            }
258        }
259        else
260        {
261            if (isMultiple(pathSegments[0]))
262            {
263                if (allowMultiValuedPathSegments)
264                {
265                    return _getMultipleValues(dataPath);
266                }
267                else
268                {
269                    // Multiple items are allowed only at the last segment of the data path
270                    throw new BadDataPathCardinalityException("Unable to retrieve the value at path '" + dataPath + "'. The segment '" + pathSegments[0] + "' refers to a multiple data and can not be used inside the data path.");
271                }
272            }
273            else
274            {
275                // Path where first part is a data holder
276                ModelAwareDataHolder dataHolder = getValue(pathSegments[0]);
277                if (dataHolder == null)
278                {
279                    return null;
280                }
281                else
282                {
283                    String subDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
284                    return status.isPresent()
285                            ? ExternalizableDataStatus.EXTERNAL.equals(status.get())
286                                    ? dataHolder.getExternalValue(subDataPath)
287                                    : dataHolder.getLocalValue(subDataPath)
288                            : dataHolder.getValue(subDataPath, allowMultiValuedPathSegments);
289                }
290            }
291        }
292    }
293    
294    public ExternalizableDataStatus getStatus(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadDataPathCardinalityException
295    {
296        _checkDefinition(dataPath, true, "Unable to retrieve the value at path '" + dataPath + "'.");
297        
298        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
299        
300        if (pathSegments == null || pathSegments.length < 1)
301        {
302            throw new IllegalArgumentException("Unable to retrieve the data at the given path. This path is empty.");
303        }
304        else if (pathSegments.length == 1)
305        {
306            if (_repositoryData.hasValue(dataPath + STATUS_SUFFIX))
307            {
308                String status = _repositoryData.getString(dataPath + STATUS_SUFFIX);
309                return ExternalizableDataStatus.valueOf(status.toUpperCase());
310            }
311            else
312            {
313                return ExternalizableDataStatus.LOCAL;
314            }
315        }
316        else
317        {
318            if (isMultiple(pathSegments[0]))
319            {
320                // Multiple items are allowed only at the last segment of the data path
321                throw new BadDataPathCardinalityException("Unable to retrieve the value at path '" + dataPath + "'. The segment '" + pathSegments[0] + "' refers to a multiple data and can not be used inside the data path.");
322            }
323            else
324            {
325                // Path where first part is a data holder
326                ModelAwareDataHolder dataHolder = getValue(pathSegments[0]);
327                if (dataHolder == null)
328                {
329                    return null;
330                }
331                else
332                {
333                    String subDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
334                    return dataHolder.getStatus(subDataPath);
335                }
336            }
337        }
338    }
339    
340    @SuppressWarnings("unchecked")
341    private <T> T _getPropertyValue(Property property)
342    {
343        return getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametyObject ? (T) property.getValue(ametyObject) : null;
344    }
345    
346    @SuppressWarnings("unchecked")
347    private <T> T _getElementValue(ElementDefinition definition, String dataName)
348    {
349        RepositoryElementType type = (RepositoryElementType) definition.getType();
350        Object value = type.read(_repositoryData, dataName);
351
352        if (definition.isMultiple() && type.getManagedClass().isInstance(value))
353        {
354            // The value is single but should be an array. Create the array with the single value
355            T arrayValue = (T) Array.newInstance(type.getManagedClass(), 1);
356            Array.set(arrayValue, 0, value);
357            return arrayValue;
358        }
359        else if (!definition.isMultiple() && type.getManagedClassArray().isInstance(value))
360        {
361            // The value is multiple but should be single. Retrieve the first value of the array
362            return Array.getLength(value) > 0 ? (T) Array.get(value, 0) : null;
363        }
364        else
365        {
366            return (T) value;
367        }
368    }
369    
370    @SuppressWarnings("unchecked")
371    private <T> T _getGroupValue(ModelItem modelItem, String dataName)
372    {
373        if (modelItem instanceof RepeaterDefinition)
374        {
375            Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataName);
376            if (repeaterNameAndEntryPosition != null)
377            {
378                return (T) DataHolderHelper.getRepeaterEntry(this, repeaterNameAndEntryPosition.getLeft(), repeaterNameAndEntryPosition.getRight());
379            }
380            else
381            {
382                return (T) _getRepeater(dataName, (RepeaterDefinition) modelItem);
383            }
384        }
385        else
386        {
387            return (T) _getComposite(dataName, (CompositeDefinition) modelItem);
388        }
389    }
390
391    @SuppressWarnings("unchecked")
392    private <T> T _getMultipleValues(String dataPath)
393    {
394        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
395        String subDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
396        Class managedClass = _getManagedClass(this, dataPath);
397        
398        Object segmentValue = getValue(pathSegments[0]);
399        if (segmentValue == null)
400        {
401            return (T) Array.newInstance(managedClass, 0);
402        }
403
404        if (segmentValue instanceof ModelAwareRepeater)
405        {
406            ModelAwareRepeater repeater = (ModelAwareRepeater) segmentValue;
407            return DataHolderHelper.aggregateMultipleValues(repeater.getEntries(), subDataPath, managedClass);
408        }
409        else
410        {
411            ModelAwareDataHolder[] dataHolders = (ModelAwareDataHolder[]) segmentValue;
412            return DataHolderHelper.aggregateMultipleValues(Arrays.asList(dataHolders), subDataPath, managedClass);
413        }
414    }
415
416    private Class _getManagedClass(ModelAwareDataHolder dataHolder, String dataPath)
417    {
418        Class managedClass;
419        ModelItem modelItem = dataHolder.getDefinition(dataPath);
420        if (modelItem instanceof ElementDefinition)
421        {
422            managedClass = ((ElementDefinition) modelItem).getType().getManagedClass();
423        }
424        else
425        {
426            if (modelItem instanceof RepeaterDefinition)
427            {
428                Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataPath);
429                managedClass = repeaterNameAndEntryPosition != null ? _getRepeaterEntryClass() : _getRepeaterClass();
430            }
431            else
432            {
433                managedClass = _getCompositeClass();
434            }
435        }
436        return managedClass;
437    }
438    
439    /**
440     * Retrieves the class of the managed repeater entries
441     * @return the class of the managed repeater entries
442     */
443    protected Class _getRepeaterEntryClass()
444    {
445        return ModelAwareRepeaterEntry.class;
446    }
447    
448    /**
449     * Retrieves the class of the managed repeaters
450     * @return the class of the managed repeaters
451     */
452    protected Class _getRepeaterClass()
453    {
454        return ModelAwareRepeater.class;
455    }
456    
457    /**
458     * Retrieves the class of the managed composites
459     * @return the class of the managed composites
460     */
461    protected Class _getCompositeClass()
462    {
463        return ModelAwareComposite.class;
464    }
465    
466    public <T> T getValue(String dataPath, boolean useDefaultFromModel, T defaultValue) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
467    {
468        _checkDefinition(dataPath, "Unable to retrieve the value at path '" + dataPath + "'.");
469        
470        if (hasValue(dataPath))
471        {
472            return  getValue(dataPath);
473        }
474        
475        if (useDefaultFromModel)
476        {
477            ModelItem modelItem = getDefinition(dataPath);
478     
479            if (modelItem instanceof ElementDefinition)
480            {
481                @SuppressWarnings("unchecked")
482                T defaultFromModel = (T) ((ElementDefinition) modelItem).getDefaultValue();
483                if (defaultFromModel != null)
484                {
485                    return defaultFromModel;
486                }
487            }
488        }
489
490        return defaultValue;
491    }
492    
493    /**
494     * Retrieves the composite with the given name
495     * @param name name of the composite to retrieve
496     * @param compositeDefinition the definition of the composite to retrieve
497     * @return the composite
498     * @throws BadItemTypeException if the value stored in the repository with the given name is not a composite
499     */
500    protected ModelAwareComposite _getComposite(String name, CompositeDefinition compositeDefinition) throws BadItemTypeException
501    {
502        RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) compositeDefinition.getType();
503        RepositoryData compositeRepositoryData = type.read(_repositoryData, name);
504        
505        if (compositeRepositoryData != null)
506        {
507            return new DefaultModelAwareComposite(compositeRepositoryData, this, _root, compositeDefinition);
508        }
509        else
510        {
511            return null;
512        }
513    }
514    
515    /**
516     * Retrieves the repeater with the given name
517     * @param name name of the repeater to retrieve
518     * @param repeaterDefinition the definition of the repeater to retrieve
519     * @return the repeater
520     * @throws BadItemTypeException if the value stored in the repository with the given name is not a repeater
521     */
522    protected ModelAwareRepeater _getRepeater(String name, RepeaterDefinition repeaterDefinition) throws BadItemTypeException
523    {
524        RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) repeaterDefinition.getType();
525        RepositoryData repeaterRepositoryData = type.read(_repositoryData, name);
526        
527        if (repeaterRepositoryData != null)
528        {
529            return new DefaultModelAwareRepeater(repeaterRepositoryData, this, _root, repeaterDefinition);
530        }
531        else
532        {
533            return null;
534        }
535    }
536    
537    public List<DataComment> getComments(String dataName) throws IllegalArgumentException, UndefinedItemPathException
538    {
539        _checkDefinition(dataName, "Unable to retrieve the comments of the data named '" + dataName + "'.");
540        
541        List<DataComment> comments = new ArrayList<>();
542        
543        RepositoryData commentsRepositoryData = _repositoryData.getRepositoryData(dataName + COMMENTS_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL);
544        
545        for (String commentId : commentsRepositoryData.getDataNames(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL))
546        {
547            RepositoryData commentRepositoryData = commentsRepositoryData.getRepositoryData(commentId, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL);
548            
549            String content = commentRepositoryData.getString("comment");
550            String author = commentRepositoryData.getString("author");
551            ZonedDateTime date = DateUtils.asZonedDateTime(commentRepositoryData.getDate("date"));
552            
553            DataComment comment = new DataComment(content, date, author);
554            comments.add(comment);
555        }
556        
557        return comments;
558    }
559
560    public boolean hasValue(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException
561    {
562        return _hasValue(dataPath, Optional.empty());
563    }
564    
565    public boolean hasLocalValue(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException
566    {
567        return _hasValue(dataPath, Optional.of(ExternalizableDataStatus.LOCAL));
568    }
569    
570    public boolean hasExternalValue(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException
571    {
572        return _hasValue(dataPath, Optional.of(ExternalizableDataStatus.EXTERNAL));
573    }
574    
575    @SuppressWarnings("unchecked")
576    private boolean _hasValue(String dataPath, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, BadDataPathCardinalityException
577    {
578        if (!hasDefinition(dataPath))
579        {
580            return false;
581        }
582        
583        if (StringUtils.isEmpty(dataPath))
584        {
585            throw new IllegalArgumentException("Unable to check if there is a non empty value at the given path. This path is empty.");
586        }
587        else if (!dataPath.contains(ModelItem.ITEM_PATH_SEPARATOR))
588        {
589            if (DataHolderHelper.isRepeaterEntryPath(dataPath))
590            {
591                Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataPath);
592                return DataHolderHelper.hasNonEmptyRepeaterEntry(this, repeaterNameAndEntryPosition.getLeft(), repeaterNameAndEntryPosition.getRight());
593            }
594            else
595            {
596                if (getDefinition(dataPath) instanceof Property property)
597                {
598                    if (getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject)
599                    {
600                        return property.getValue(ametysObject) != null;
601                    }
602                    else
603                    {
604                        return false;
605                    }
606                }
607                else
608                {
609                    RepositoryModelItemType type = getType(dataPath);
610                    String dataName = _getFinalDataName(dataPath, status);
611                        
612                    try
613                    {
614                        return type.hasNonEmptyValue(_repositoryData, dataName);
615                    }
616                    catch (BadItemTypeException e)
617                    {
618                        return false;
619                    }
620                }
621            }
622        }
623        else
624        {
625            String parentPath = StringUtils.substringBeforeLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
626            
627            // Multiple items are allowed only at the last segment of the data path
628            if (isMultiple(parentPath))
629            {
630                throw new BadDataPathCardinalityException("Unable to check if there is a value at path '" + dataPath + "'. The segment '" + parentPath + "' refers to a multiple data and can not be used inside the data path.");
631            }
632            
633            try
634            {
635                ModelAwareDataHolder parent = getValue(parentPath);
636                if (parent == null)
637                {
638                    return false;
639                }
640                else
641                {
642                    String childName = StringUtils.substringAfterLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
643                    return status.isPresent()
644                            ? ExternalizableDataStatus.EXTERNAL.equals(status.get())
645                                    ? parent.hasExternalValue(childName)
646                                    : parent.hasLocalValue(childName)
647                            : parent.hasValue(childName);
648                }
649            }
650            catch (BadItemTypeException e)
651            {
652                return false;
653            }
654        }
655    }
656
657    public boolean hasValueOrEmpty(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException
658    {
659        return _hasValueOrEmpty(dataPath, Optional.empty());
660    }
661    
662    public boolean hasLocalValueOrEmpty(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException
663    {
664        return _hasValueOrEmpty(dataPath, Optional.of(ExternalizableDataStatus.LOCAL));
665    }
666    
667    public boolean hasExternalValueOrEmpty(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException
668    {
669        return _hasValueOrEmpty(dataPath, Optional.of(ExternalizableDataStatus.EXTERNAL));
670    }
671    
672    @SuppressWarnings("unchecked")
673    private boolean _hasValueOrEmpty(String dataPath, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, BadDataPathCardinalityException
674    {
675        if (!hasDefinition(dataPath))
676        {
677            return false;
678        }
679        
680        if (StringUtils.isEmpty(dataPath))
681        {
682            throw new IllegalArgumentException("Unable to check if there is a value at the given path. This path is empty.");
683        }
684        else if (!dataPath.contains(ModelItem.ITEM_PATH_SEPARATOR))
685        {
686            if (DataHolderHelper.isRepeaterEntryPath(dataPath))
687            {
688                Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataPath);
689                return DataHolderHelper.hasRepeaterEntry(this, repeaterNameAndEntryPosition.getLeft(), repeaterNameAndEntryPosition.getRight());
690            }
691            else
692            {
693                if (getDefinition(dataPath) instanceof Property property)
694                {
695                    if (getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject)
696                    {
697                        return property.getValue(ametysObject) != null;
698                    }
699                    else
700                    {
701                        return false;
702                    }
703                }
704                else
705                {
706                    RepositoryModelItemType type = getType(dataPath);
707                    String dataName = _getFinalDataName(dataPath, status);
708                    return type.hasValue(_repositoryData, dataName);
709                }
710            }
711        }
712        else
713        {
714            String parentPath = StringUtils.substringBeforeLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
715            
716            // Multiple items are allowed only at the last segment of the data path
717            if (isMultiple(parentPath))
718            {
719                throw new BadDataPathCardinalityException("Unable to check if there is a value at path '" + dataPath + "'. The segment '" + parentPath + "' refers to a multiple data and can not be used inside the data path.");
720            }
721            
722            try
723            {
724                ModelAwareDataHolder parent = getValue(parentPath);
725                if (parent == null)
726                {
727                    return false;
728                }
729                else
730                {
731                    String childName = StringUtils.substringAfterLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
732                    return status.isPresent()
733                            ? ExternalizableDataStatus.EXTERNAL.equals(status.get())
734                                    ? parent.hasExternalValueOrEmpty(childName)
735                                    : parent.hasLocalValueOrEmpty(childName)
736                            : parent.hasValueOrEmpty(childName);
737                }
738            }
739            catch (BadItemTypeException e)
740            {
741                return false;
742            }
743        }
744    }
745    
746    /**
747     * Retrieves the name of the data according to the given status
748     * @param dataName the name of the data
749     * @param status the status
750     * @return the final name of the data
751     */
752    protected String _getFinalDataName(String dataName, Optional<ExternalizableDataStatus> status)
753    {
754        if (status.isPresent() && getStatus(dataName) != status.get())
755        {
756            return dataName + ALTERNATIVE_SUFFIX;
757        }
758        
759        return dataName;
760    }
761    
762    public boolean hasComments(String dataName) throws IllegalArgumentException, UndefinedItemPathException
763    {
764        _checkDefinition(dataName, "Unable to check if there are comments on the data named '" + dataName + "'.");
765        
766        return _repositoryData.hasValue(dataName + COMMENTS_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL);
767    }
768    
769    public Collection< ? extends ModelItemContainer> getModel()
770    {
771        return _itemContainers;
772    }
773    
774    public ModelItem getDefinition(String path) throws IllegalArgumentException, UndefinedItemPathException
775    {
776        try
777        {
778            ModelItem definition = IndexableDataHolder.super.getDefinition(path);
779            
780            // A definition has been found, ok
781            return definition;
782        }
783        catch (UndefinedItemPathException e)
784        {
785            // Look for system properties
786            if (StringUtils.contains(path, ModelItem.ITEM_PATH_SEPARATOR))
787            {
788                String parentDataPath = StringUtils.substringBeforeLast(path, ModelItem.ITEM_PATH_SEPARATOR);
789                ModelItem modelItem = getDefinition(parentDataPath);
790                if (modelItem instanceof ContentElementDefinition)
791                {
792                    SystemPropertyExtensionPoint contentSystemPropertyExtensionPoint = IndexableDataHolderHelper.getContentSystemPropertyExtensionPoint();
793                    String propertyName = StringUtils.substringAfterLast(path, ModelItem.ITEM_PATH_SEPARATOR);
794                    if (contentSystemPropertyExtensionPoint.hasExtension(propertyName))
795                    {
796                        return contentSystemPropertyExtensionPoint.getExtension(propertyName);
797                    }
798                }
799            }
800            else if (getParentDataHolder().isEmpty()
801                    && getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject
802                    && ametysObject.getSystemPropertyExtensionPoint().isPresent())
803            {
804                SystemPropertyExtensionPoint systemPropertyExtensionPoint = ametysObject.getSystemPropertyExtensionPoint().get();
805                if (systemPropertyExtensionPoint.hasExtension(path))
806                {
807                    return systemPropertyExtensionPoint.getExtension(path);
808                }
809            }
810            
811            // No system property has been found, throw the UndefinedItemPathException
812            throw e;
813        }
814    }
815
816    public Collection<String> getDataNames()
817    {
818        return ModelHelper.getModelItems(getModel())
819                          .stream()
820                          .map(ModelItem::getName)
821                          .filter(this::hasValueOrEmpty)
822                          .toList();
823    }
824    
825    @SuppressWarnings("unchecked")
826    public void dataToSAX(ContentHandler contentHandler, String dataPath, DataContext context) throws SAXException
827    {
828        _checkDefinition(dataPath, "Unable to generate SAX events for the data at path '" + dataPath + "'.");
829        
830        ModelAwareDataHolder root = getRootDataHolder();
831        DataContext newContext = context.cloneContext();
832        if (root instanceof AmetysObject ametysObject)
833        {
834            newContext.withObjectId(ametysObject.getId());
835        }
836        
837        if (hasValue(dataPath)
838            || newContext.renderEmptyValues() && hasValueOrEmpty(dataPath))
839        {
840            ModelItem modelItem = getDefinition(dataPath);
841            if (modelItem instanceof Property property)
842            {
843                ModelAwareDataAwareAmetysObject ametysObject = _getPropertysAmetysObject(dataPath);
844                property.valueToSAX(contentHandler, ametysObject, newContext);
845            }
846            else
847            {
848                ModelItemType type = modelItem.getType();
849                Object value = getValue(dataPath);
850                
851                type.valueToSAX(contentHandler, modelItem.getName(), value, newContext.cloneContext().withDataPath(dataPath));
852            }
853        }
854    }
855    
856    public void dataToSAX(ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext context) throws SAXException, BadItemTypeException
857    {
858        ModelAwareDataHolder root = getRootDataHolder();
859        
860        DataContext newContext = context.cloneContext();
861        if (root instanceof AmetysObject ametysObject)
862        {
863            newContext.withObjectId(ametysObject.getId());
864        }
865        
866        IndexableDataHolderHelper.dataToSAX(this, contentHandler, viewItemAccessor, newContext, false);
867    }
868    
869    public void dataToSAXForEdition(ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext context) throws SAXException, BadItemTypeException
870    {
871        ModelAwareDataHolder root = getRootDataHolder();
872        
873        DataContext newContext = context.cloneContext();
874        if (root instanceof AmetysObject ametysObject)
875        {
876            newContext.withObjectId(ametysObject.getId());
877        }
878        
879        IndexableDataHolderHelper.dataToSAX(this, contentHandler, viewItemAccessor, newContext, true);
880    }
881    
882    @SuppressWarnings("unchecked")
883    public Object dataToJSON(String dataPath, DataContext context)
884    {
885        _checkDefinition(dataPath, "Unable to convert the data at path '" + dataPath + "' to JSON.");
886        
887        ModelAwareDataHolder root = getRootDataHolder();
888        DataContext newContext = context.cloneContext();
889        if (root instanceof AmetysObject ametysObject)
890        {
891            newContext.withObjectId(ametysObject.getId());
892        }
893        
894        if (hasValue(dataPath)
895            || newContext.renderEmptyValues() && hasValueOrEmpty(dataPath))
896        {
897            ModelItem modelItem = getDefinition(dataPath);
898            if (modelItem instanceof Property property)
899            {
900                ModelAwareDataAwareAmetysObject ametysObject = _getPropertysAmetysObject(dataPath);
901                return property.valueToJSON(ametysObject, newContext);
902            }
903            else
904            {
905                ModelItemType type = modelItem.getType();
906                Object value = getValue(dataPath);
907                
908                return type.valueToJSONForClient(value, newContext.cloneContext().withDataPath(dataPath));
909            }
910        }
911        else
912        {
913            return null;
914        }
915    }
916    
917    /**
918     * Retrieves the ametys object containing the property at the given path
919     * @param dataPath the path of the property
920     * @return the ametys object containing the property
921     * @throws UndefinedItemPathException if the given path does not represent a property
922     */
923    protected ModelAwareDataAwareAmetysObject _getPropertysAmetysObject(String dataPath) throws UndefinedItemPathException
924    {
925        if (StringUtils.contains(dataPath, ModelItem.ITEM_PATH_SEPARATOR))
926        {
927            String parentDataPath = StringUtils.substringBeforeLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
928            ContentValue value = getValue(parentDataPath);
929            return value.getContent();
930        }
931        else if (getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject)
932        {
933            return ametysObject;
934        }
935        else
936        {
937            throw new UndefinedItemPathException("There is no property at path '" + dataPath + "'");
938        }
939    }
940    
941    public Map<String, Object> dataToJSON(ViewItemAccessor viewItemAccessor, DataContext context) throws BadItemTypeException
942    {
943        ModelAwareDataHolder root = getRootDataHolder();
944        
945        DataContext newContext = context.cloneContext();
946        if (root instanceof AmetysObject ametysObject)
947        {
948            newContext.withObjectId(ametysObject.getId());
949        }
950        
951        return IndexableDataHolderHelper.dataToJSON(this, viewItemAccessor, newContext, false);
952    }
953    
954    public Map<String, Object> dataToJSONForEdition(ViewItemAccessor viewItemAccessor, DataContext context) throws BadItemTypeException
955    {
956        ModelAwareDataHolder root = getRootDataHolder();
957        
958        DataContext newContext = context.cloneContext();
959        if (root instanceof AmetysObject ametysObject)
960        {
961            newContext.withObjectId(ametysObject.getId());
962        }
963        
964        return IndexableDataHolderHelper.dataToJSON(this, viewItemAccessor, newContext, true);
965    }
966    
967    public Map<String, Object> dataToMap(ViewItemAccessor viewItemAccessor, DataContext context)
968    {
969        ModelAwareDataHolder root = getRootDataHolder();
970        
971        DataContext newContext = context.cloneContext();
972        if (root instanceof AmetysObject)
973        {
974            newContext.withObjectId(((AmetysObject) root).getId());
975        }
976        
977        return IndexableDataHolderHelper.dataToMap(this, viewItemAccessor, newContext);
978    }
979    
980    public RepositoryData getRepositoryData()
981    {
982        return _repositoryData;
983    }
984    
985    public Optional<? extends IndexableDataHolder> getParentDataHolder()
986    {
987        return _parent;
988    }
989    
990    public IndexableDataHolder getRootDataHolder()
991    {
992        return _root;
993    }
994    
995    /**
996     * Check definition for data path
997     * @param dataPath the data path
998     * @param errorMsg the error message to throw
999     */
1000    protected void _checkDefinition(String dataPath, String errorMsg)
1001    {
1002        _checkDefinition(dataPath, false, errorMsg);
1003    }
1004    
1005    /**
1006     * Check definition for data path
1007     * @param dataPath the data path
1008     * @param checkStatusAvailable <code>true</code> to check if the definition supports externalizable data status
1009     * @param errorMsg the error message to throw
1010     */
1011    protected void _checkDefinition(String dataPath, boolean checkStatusAvailable, String errorMsg)
1012    {
1013        // Check if the model exists
1014        if (_itemContainers.isEmpty())
1015        {
1016            throw new UndefinedItemPathException(errorMsg + " No model is defined for this object [" + getRootDataHolder() + "]");
1017        }
1018        
1019        // Check that there is an item at the given path
1020        if (!hasDefinition(dataPath))
1021        {
1022            throw new UndefinedItemPathException(errorMsg + " There is no such item defined by the model.");
1023        }
1024        
1025        // Externalizable data status is not available for properties
1026        if (checkStatusAvailable && getDefinition(dataPath) instanceof Property)
1027        {
1028            throw new UndefinedItemPathException(errorMsg + " A property can't have an externalizable status.");
1029        }
1030    }
1031}