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