001/*
002 *  Copyright 2018 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.plugins.repository.data.holder.impl;
017
018import java.lang.reflect.Array;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028import java.util.stream.Stream;
029
030import org.apache.avalon.framework.activity.Disposable;
031import org.apache.avalon.framework.component.Component;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.avalon.framework.service.Serviceable;
035import org.apache.cocoon.xml.AttributesImpl;
036import org.apache.cocoon.xml.XMLUtils;
037import org.apache.commons.lang3.ArrayUtils;
038import org.apache.commons.lang3.StringUtils;
039import org.apache.commons.lang3.tuple.ImmutablePair;
040import org.apache.commons.lang3.tuple.Pair;
041import org.xml.sax.ContentHandler;
042import org.xml.sax.SAXException;
043
044import org.ametys.core.ui.Callable;
045import org.ametys.core.util.DateUtils;
046import org.ametys.plugins.repository.AmetysObject;
047import org.ametys.plugins.repository.AmetysObjectResolver;
048import org.ametys.plugins.repository.data.DataComment;
049import org.ametys.plugins.repository.data.UnknownDataException;
050import org.ametys.plugins.repository.data.ametysobject.ModelAwareDataAwareAmetysObject;
051import org.ametys.plugins.repository.data.ametysobject.ModelLessDataAwareAmetysObject;
052import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus;
053import org.ametys.plugins.repository.data.external.ExternalizableDataProviderExtensionPoint;
054import org.ametys.plugins.repository.data.holder.DataHolder;
055import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
056import org.ametys.plugins.repository.data.holder.ModelLessDataHolder;
057import org.ametys.plugins.repository.data.holder.ModifiableDataHolder;
058import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
059import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
060import org.ametys.plugins.repository.data.holder.group.Composite;
061import org.ametys.plugins.repository.data.holder.group.ModelAwareComposite;
062import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater;
063import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry;
064import org.ametys.plugins.repository.data.holder.group.ModifiableComposite;
065import org.ametys.plugins.repository.data.holder.group.ModifiableRepeater;
066import org.ametys.plugins.repository.data.holder.group.Repeater;
067import org.ametys.plugins.repository.data.holder.group.RepeaterEntry;
068import org.ametys.plugins.repository.data.holder.values.SynchronizableValue;
069import org.ametys.plugins.repository.data.holder.values.SynchronizationContext;
070import org.ametys.plugins.repository.data.holder.values.UntouchedValue;
071import org.ametys.plugins.repository.data.holder.values.ValueContext;
072import org.ametys.plugins.repository.model.RepeaterDefinition;
073import org.ametys.plugins.repository.model.ViewHelper;
074import org.ametys.runtime.model.ElementDefinition;
075import org.ametys.runtime.model.ModelItem;
076import org.ametys.runtime.model.ModelViewItem;
077import org.ametys.runtime.model.ViewItem;
078import org.ametys.runtime.model.ViewItemAccessor;
079import org.ametys.runtime.model.ViewItemContainer;
080import org.ametys.runtime.model.exception.BadDataPathCardinalityException;
081import org.ametys.runtime.model.exception.BadItemTypeException;
082import org.ametys.runtime.model.exception.NotUniqueTypeException;
083import org.ametys.runtime.model.exception.UndefinedItemPathException;
084import org.ametys.runtime.model.exception.UnknownTypeException;
085import org.ametys.runtime.model.type.DataContext;
086import org.ametys.runtime.model.type.ModelItemType;
087
088/**
089 * Helper for implementations of data holder
090 */
091public final class DataHolderHelper implements Component, Serviceable, Disposable
092{
093    /** Pattern for repeater entry : entryName[i] */
094    public static final Pattern REPEATER_ENTRY_PATTERN = Pattern.compile("(.*)\\[(\\d+)\\]$");
095    
096    private static ExternalizableDataProviderExtensionPoint _externalizableDataProviderEP;
097    private static AmetysObjectResolver _resolver;
098    
099    @Override
100    public void service(ServiceManager manager) throws ServiceException
101    {
102        _externalizableDataProviderEP = (ExternalizableDataProviderExtensionPoint) manager.lookup(ExternalizableDataProviderExtensionPoint.ROLE);
103        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
104    }
105    
106    public void dispose()
107    {
108        _externalizableDataProviderEP = null;
109        _resolver = null;
110    }
111    
112    /**
113     * Checks if there is a repeater entry at the given position
114     * @param dataHolder data holder that contains the repeater entry. The data holder must be the direct parent of the repeater
115     * @param repeaterName the name of the repeater
116     * @param entryPosition the position of the entry
117     * @return <code>true</code> if there is an entry at the given position, <code>false</code> otherwise
118     * @throws BadItemTypeException if the value stored in the repository for the given repeater name is not a repeater
119     */
120    public static boolean hasRepeaterEntry(ModelAwareDataHolder dataHolder, String repeaterName, int entryPosition) throws BadItemTypeException
121    {
122        if (dataHolder.getRepositoryData().hasValue(repeaterName))
123        {
124            Repeater repeater = dataHolder.getRepeater(repeaterName);
125            return repeater.hasEntry(entryPosition);
126        }
127
128        return false;
129    }
130    
131    /**
132     * Checks if there is a non empty repeater entry at the given position
133     * @param dataHolder data holder that contains the repeater entry. The data holder must be the direct parent of the repeater
134     * @param repeaterName the name of the repeater
135     * @param entryPosition the position of the entry
136     * @return <code>true</code> if there is a non empty entry at the given position, <code>false</code> otherwise
137     * @throws BadItemTypeException if the value stored in the repository for the given repeater name is not a repeater
138     */
139    public static boolean hasNonEmptyRepeaterEntry(ModelAwareDataHolder dataHolder, String repeaterName, int entryPosition) throws BadItemTypeException
140    {
141        if (dataHolder.getRepositoryData().hasValue(repeaterName))
142        {
143            Repeater repeater = dataHolder.getRepeater(repeaterName);
144            if (repeater.hasEntry(entryPosition))
145            {
146                RepeaterEntry entry = repeater.getEntry(entryPosition);
147                return !entry.getDataNames().isEmpty();
148            }
149        }
150            
151        return false;
152    }
153    
154    /**
155     * Retrieves the repeater entry at the given position
156     * @param dataHolder data holder that contains the repeater entry. The data holder must be the direct parent of the repeater
157     * @param repeaterName the name of the repeater
158     * @param entryPosition the position of the entry
159     * @return the repeater entry
160     * @throws BadItemTypeException if the value stored in the repository for the given repeater name is not a repeater
161     */
162    public static RepeaterEntry getRepeaterEntry(ModelAwareDataHolder dataHolder, String repeaterName, int entryPosition) throws BadItemTypeException
163    {
164        Repeater repeater = dataHolder.getRepeater(repeaterName);
165        
166        if (repeater == null)
167        {
168            return null;
169        }
170        
171        if (repeater.hasEntry(entryPosition))
172        {
173            return repeater.getEntry(entryPosition);
174        }
175        else
176        {
177            return null;
178        }
179    }
180    
181    /**
182     * Test if the path is a repeater entry path (for example entries[1])
183     * @param path the path representing the repeater entry
184     * @return true if the pathSegment is a repeater entry path
185     */
186    public static boolean isRepeaterEntryPath(String path)
187    {
188        return REPEATER_ENTRY_PATTERN.matcher(path).matches();
189    }
190    
191    /**
192     * Retrieves the pair of repeater name and entry position of the given path segment
193     * Return <code>null</code> if the given path does not represent a repeater entry
194     * @param pathSegment the path segment representing the repeater entry
195     * @return the pair of repeater name and entry position
196     */
197    public static Pair<String, Integer> getRepeaterNameAndEntryPosition(String pathSegment)
198    {
199        Matcher matcher = REPEATER_ENTRY_PATTERN.matcher(pathSegment);
200        if (matcher.matches())
201        {
202            String repeaterName = matcher.group(1);
203            String entryPositionAsString = matcher.group(2);
204            return new ImmutablePair<>(repeaterName, Integer.parseInt(entryPositionAsString));
205        }
206        else
207        {
208            return null;
209        }
210    }
211    
212    /**
213     * Checks if there is a non empty value, for the data at the given path
214     * @param dataHolder the data holder
215     * @param dataPath path of the data
216     * @param context the context of the value to check
217     * @return <code>true</code> if the data at the given path is defined by the model, if there is a non empty value for the data, and if the type of this value matches the type of the definition. <code>false</code> otherwise
218     * @throws IllegalArgumentException if the given data path is null or empty
219     * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple
220     */
221    public static boolean hasValue(ModelAwareDataHolder dataHolder, String dataPath, ValueContext context) throws IllegalArgumentException, BadDataPathCardinalityException
222    {
223        if (context.getStatus().isPresent())
224        {
225            ExternalizableDataStatus status = context.getStatus().get();
226            if (ExternalizableDataStatus.LOCAL.equals(status))
227            {
228                return dataHolder.hasLocalValue(dataPath);
229            }
230            else
231            {
232                return dataHolder.hasExternalValue(dataPath);
233            }
234        }
235        else
236        {
237            return dataHolder.hasValue(dataPath);
238        }
239    }
240    
241    /**
242     * Checks if there is value, even empty, for the data at the given path
243     * @param dataHolder the data holder
244     * @param dataPath path of the data
245     * @param context the context of the value to check
246     * @return <code>true</code> if the data at the given path is defined by the model, if there is a value, even empty, for the data, and if the type of this value matches the type of the definition. <code>false</code> otherwise
247     * @throws IllegalArgumentException if the given data path is null or empty
248     * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple
249     */
250    public static boolean hasValueOrEmpty(ModelAwareDataHolder dataHolder, String dataPath, ValueContext context) throws IllegalArgumentException, BadDataPathCardinalityException
251    {
252        if (context.getStatus().isPresent())
253        {
254            ExternalizableDataStatus status = context.getStatus().get();
255            if (ExternalizableDataStatus.LOCAL.equals(status))
256            {
257                return dataHolder.hasLocalValueOrEmpty(dataPath);
258            }
259            else
260            {
261                return dataHolder.hasExternalValueOrEmpty(dataPath);
262            }
263        }
264        else
265        {
266            return dataHolder.hasValueOrEmpty(dataPath);
267        }
268    }
269    
270    /**
271     * Retrieves the value at the given path for the data aware ametys object with given id
272     * @param <T> type of the value to retrieve
273     * @param ametysObjectId identifier of the data aware ametys object
274     * @param dataPath path of the data
275     * @return the value of the data or <code>null</code> if not exists or is empty.
276     * @throws IllegalArgumentException if the given data path is null or empty
277     * @throws UndefinedItemPathException if the given data path is not defined by the model
278     * @throws BadItemTypeException if the type defined by the model doesn't match the type of the stored value
279     * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple
280     */
281    @Callable
282    public static <T> T getValue(String ametysObjectId, String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
283    {
284        AmetysObject ametysObject = _resolver.resolveById(ametysObjectId);
285        if (ametysObject instanceof ModelAwareDataAwareAmetysObject)
286        {
287            return ((ModelAwareDataAwareAmetysObject) ametysObject).getValue(dataPath);
288        }
289        else if (ametysObject instanceof ModelLessDataAwareAmetysObject)
290        {
291            return ((ModelLessDataAwareAmetysObject) ametysObject).getValue(dataPath);
292        }
293        else
294        {
295            String message = String.format("Unable to retrieve the value at path '%s' from the ametys object '%s': this ametys object is not data aware.", dataPath, ametysObjectId);
296            throw new IllegalArgumentException(message);
297        }
298    }
299    
300    /**
301     * Retrieves the value of the data at the given path
302     * @param <T> type of the value to retrieve
303     * @param dataHolder the data holder
304     * @param dataPath path of the data
305     * @param context the context of the value to retrieve
306     * @return the value of the data or <code>null</code> if not exists or is empty.
307     * @throws IllegalArgumentException if the given data path is null or empty
308     * @throws UndefinedItemPathException if the given data path is not defined by the model
309     * @throws BadItemTypeException if the type defined by the model doesn't match the type of the stored value
310     * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple
311     */
312    public static <T> T getValue(ModelAwareDataHolder dataHolder, String dataPath, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
313    {
314        if (context.getStatus().isPresent())
315        {
316            ExternalizableDataStatus status = context.getStatus().get();
317            if (ExternalizableDataStatus.LOCAL.equals(status))
318            {
319                return dataHolder.getLocalValue(dataPath);
320            }
321            else
322            {
323                return dataHolder.getExternalValue(dataPath);
324            }
325        }
326        else
327        {
328            return dataHolder.getValue(dataPath);
329        }
330    }
331    
332    /**
333     * Sets the value of the data at the given path
334     * @param dataHolder the data holder
335     * @param dataPath path of the data
336     * @param value the value to set. Give <code>null</code> to empty the value.
337     * @param context context of the data to set
338     * @throws IllegalArgumentException if the given data path is null or empty
339     * @throws UndefinedItemPathException if the given data path is not defined by the model
340     * @throws BadItemTypeException if the type defined by the model doesn't match the given value to set 
341     * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple
342     */
343    public static void setValue(ModifiableModelAwareDataHolder dataHolder, String dataPath, Object value, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
344    {
345        setValue(dataHolder, dataPath, value, context, false);
346    }
347    
348    /**
349     * Sets the value of the data at the given path
350     * @param dataHolder the data holder
351     * @param dataPath path of the data
352     * @param value the value to set. Give <code>null</code> to empty the value.
353     * @param context context of the data to set
354     * @param forceStatus <code>true</code> to force the status to the given status context if defined
355     * @throws IllegalArgumentException if the given data path is null or empty
356     * @throws UndefinedItemPathException if the given data path is not defined by the model
357     * @throws BadItemTypeException if the type defined by the model doesn't match the given value to set 
358     * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple
359     */
360    public static void setValue(ModifiableModelAwareDataHolder dataHolder, String dataPath, Object value, ValueContext context, boolean forceStatus) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
361    {
362        if (context.getStatus().isPresent())
363        {
364            ExternalizableDataStatus status = context.getStatus().get();
365            if (ExternalizableDataStatus.LOCAL.equals(status))
366            {
367                dataHolder.setLocalValue(dataPath, value);
368            }
369            else
370            {
371                dataHolder.setExternalValue(dataPath, value);
372            }
373            
374            if (forceStatus)
375            {
376                dataHolder.setStatus(dataPath, status);
377            }
378        }
379        else
380        {
381            dataHolder.setValue(dataPath, value);
382        }
383    }
384    
385    /**
386     * Removes the stored value of the data at the given path
387     * @param dataHolder the data holder
388     * @param dataPath path of the data
389     * @param context context of the data to remove
390     * @throws IllegalArgumentException if the given data path is null or empty
391     * @throws UnknownDataException if the value at the given data path does not exist
392     * @throws BadItemTypeException if the value of the parent of the given path is not an item container
393     * @throws UndefinedItemPathException if the given data path is not defined by the model
394     * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple
395     */
396    public static void removeValue(ModifiableModelAwareDataHolder dataHolder, String dataPath, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, UnknownDataException, BadDataPathCardinalityException
397    {
398        if (context.getStatus().isPresent())
399        {
400            ExternalizableDataStatus status = context.getStatus().get();
401            if (ExternalizableDataStatus.LOCAL.equals(status))
402            {
403                dataHolder.removeLocalValue(dataPath);
404            }
405            else
406            {
407                dataHolder.removeExternalValue(dataPath);
408            }
409        }
410        else
411        {
412            dataHolder.removeValue(dataPath);
413        }
414    }
415    
416    /**
417     * Creates a value context from the given {@link SynchronizationContext}
418     * @param dataHolder the data holder
419     * @param dataPath the path of the value needing context
420     * @param synchronizationContext the {@link SynchronizationContext}
421     * @return the created {@link ValueContext}
422     */
423    public static ValueContext createValueContextFromSynchronizationContext(ModelAwareDataHolder dataHolder, String dataPath, SynchronizationContext synchronizationContext)
424    {
425        ModelItem modelItem = dataHolder.getDefinition(dataPath);
426        ValueContext valueContext = ValueContext.newInstance();
427        
428        // If the data is not externalizable at all, there is no status to set.
429        // This can have an impact during values synchronization (ex: metatada for externalizable data status would be removed)
430        boolean isDataExternalizableInAnyContext = getExternalizableDataProviderExtensionPoint().isDataExternalizable(dataHolder.getRootDataHolder(), modelItem);
431        if (isDataExternalizableInAnyContext)
432        {
433            // If data is externalizable (not necessary in the current context), the value context has to be set, taking current context into account
434            boolean isDataExternalizableInCurrentContext = getExternalizableDataProviderExtensionPoint().isDataExternalizable(dataHolder.getRootDataHolder(), modelItem, synchronizationContext.getExternalizableDataContext());
435            if (ExternalizableDataStatus.EXTERNAL.equals(synchronizationContext.getStatusToSynchronize()) && isDataExternalizableInCurrentContext)
436            {
437                valueContext.withStatus(ExternalizableDataStatus.EXTERNAL);
438            }
439            else
440            {
441                valueContext.withStatus(ExternalizableDataStatus.LOCAL);
442            }
443        }
444        
445        
446        return valueContext;
447    }
448    
449    /**
450     * Copies the source {@link DataHolder} to the given {@link ModifiableDataHolder} destination.
451     * @param source the source {@link DataHolder}
452     * @param destination the {@link ModifiableDataHolder} destination 
453     */
454    public static void copyTo(DataHolder source, ModifiableDataHolder destination)
455    {
456        for (String name : source.getDataNames())
457        {
458            copyTo(source, destination, name);
459        }
460    }
461    
462    /**
463     * Copy the data name of the source {@link DataHolder} to the given {@link ModifiableDataHolder} destination.
464     * @param source the source {@link DataHolder}
465     * @param destination the {@link ModifiableDataHolder} destination 
466     * @param name the name of the data
467     */
468    public static void copyTo(DataHolder source, ModifiableDataHolder destination, String name)
469    {
470        Object value = source instanceof ModelAwareDataHolder ? ((ModelAwareDataHolder) source).getValue(name) : ((ModelLessDataHolder) source).getValue(name);
471        
472        if (value instanceof Composite composite)
473        {
474            ModifiableComposite compositeDestination = destination.getComposite(name, true);
475            composite.copyTo(compositeDestination);
476        }
477        else if (destination instanceof ModifiableModelAwareDataHolder modelAwareDestination)
478        {
479            if (value instanceof Repeater repeater)
480            {
481                ModifiableRepeater repeaterDestination = modelAwareDestination.getRepeater(name, true);
482                repeater.copyTo(repeaterDestination);
483            }
484            else
485            {
486                ModelItem modelItem = modelAwareDestination.getDefinition(name);
487                if (modelItem instanceof ElementDefinition definition && definition.isEditable())
488                {
489                    modelAwareDestination.setValue(name, value);
490                }
491            }
492        }
493        else if (destination instanceof ModifiableModelLessDataHolder modelLessDestination)
494        {
495            modelLessDestination.setValue(name, value);
496        }
497    }
498    
499    /**
500     * Generates SAX events for the data comments in the given view
501     * @param dataHolder the {@link ModelAwareDataHolder}
502     * @param contentHandler the {@link ContentHandler} that will receive the SAX events
503     * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items for which generate SAX events
504     * @param dataPath the data path corresponding of the current data holder. Can be <code>null</code> or empty.
505     * @throws SAXException if an error occurs during the SAX events generation
506     */
507    public static void commentsToSAX(ModelAwareDataHolder dataHolder, ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, String dataPath) throws SAXException
508    {
509        for (ViewItem viewItem : viewItemAccessor.getViewItems())
510        {
511            if (viewItem instanceof ModelViewItem)
512            {
513                String dataName = ((ModelViewItem) viewItem).getDefinition().getName();
514                String newDataPath = StringUtils.isNotEmpty(dataPath) ? dataPath + ModelItem.ITEM_PATH_SEPARATOR + dataName : dataName;
515                
516                if (dataHolder.hasComments(dataName))
517                {
518                    List<DataComment> comments = dataHolder.getComments(dataName);
519                    
520                    AttributesImpl commentsAttrs = new AttributesImpl();
521                    commentsAttrs.addCDATAAttribute("path", newDataPath);
522                    XMLUtils.startElement(contentHandler, "metadata", commentsAttrs);
523                    
524                    int id = 1;
525                    for (DataComment comment : comments)
526                    {
527                        AttributesImpl commentAttrs = new AttributesImpl();
528                        commentAttrs.addCDATAAttribute("id", String.valueOf(id++));
529                        commentAttrs.addCDATAAttribute("date", DateUtils.zonedDateTimeToString(comment.getDate()));
530                        commentAttrs.addCDATAAttribute("author", comment.getAuthor());
531                        XMLUtils.createElement(contentHandler, "comment", commentAttrs, comment.getComment());
532                    } 
533                    
534                    XMLUtils.endElement(contentHandler, "metadata");
535                }
536                
537                if (viewItem instanceof ViewItemAccessor)
538                {
539                    Object value = dataHolder.getValue(dataName);
540                    if (value instanceof ModelAwareDataHolder)
541                    {
542                        commentsToSAX((ModelAwareDataHolder) value, contentHandler, (ViewItemAccessor) viewItem, newDataPath);
543                    }
544                    else if (value instanceof ModelAwareRepeater)
545                    {
546                        for (ModelAwareRepeaterEntry entry : ((ModelAwareRepeater) value).getEntries())
547                        {
548                            String entryDataPath = newDataPath + "[" + entry.getPosition() + "]";
549                            commentsToSAX(entry, contentHandler, (ViewItemAccessor) viewItem, entryDataPath);
550                        }
551                        
552                    }
553                }
554            }
555            else if (viewItem instanceof ViewItemAccessor)
556            {
557                commentsToSAX(dataHolder, contentHandler, (ViewItemAccessor) viewItem, dataPath);
558            }
559        }
560    }
561    
562    /**
563     * Generates SAX events for data contained in this {@link DataHolder}
564     * @param dataHolder the {@link ModelLessDataHolder} to SAX
565     * @param contentHandler the {@link ContentHandler} that will receive the SAX events
566     * @param context The context of the data to SAX
567     * @throws SAXException if an error occurs during the SAX events generation
568     * @throws UnknownTypeException if there is no compatible type with the saxed value
569     * @throws NotUniqueTypeException if there are many compatible types (there is no way to determine which type is the good one) with the saxed value
570     */
571    public static void dataToSAX(ModelLessDataHolder dataHolder, ContentHandler contentHandler, DataContext context) throws SAXException, UnknownTypeException, NotUniqueTypeException
572    {
573        for (String dataName : dataHolder.getDataNames())
574        {
575            DataContext newContext = context.cloneContext().addSegmentToDataPath(dataName);
576            dataToSAX(dataHolder, contentHandler, dataName, newContext);
577        }
578    }
579    
580    /**
581     * Generates SAX events for data at the given relative path in this {@link DataHolder}
582     * @param dataHolder the {@link ModelLessDataHolder} to SAX
583     * @param contentHandler the {@link ContentHandler} that will receive the SAX events
584     * @param relativeDataPath the path of the data to SAX, relative to the given data holder
585     * @param context The context of the data to SAX
586     * @throws SAXException if an error occurs during the SAX events generation
587     * @throws UnknownTypeException if there is no compatible type with the saxed value
588     * @throws NotUniqueTypeException if there are many compatible types (there is no way to determine which type is the good one) with the saxed value
589     */
590    public static void dataToSAX(ModelLessDataHolder dataHolder, ContentHandler contentHandler, String relativeDataPath, DataContext context) throws SAXException, UnknownTypeException, NotUniqueTypeException
591    {
592        String dataName = relativeDataPath;
593        if (relativeDataPath.contains(ModelItem.ITEM_PATH_SEPARATOR))
594        {
595            dataName = StringUtils.substringAfterLast(relativeDataPath, ModelItem.ITEM_PATH_SEPARATOR);
596        }
597
598        if (dataHolder.hasValue(relativeDataPath))
599        {
600            ModelItemType type = dataHolder.getType(relativeDataPath);
601            Object value = dataHolder.getValueOfType(relativeDataPath, type.getId());
602            type.valueToSAX(contentHandler, dataName, value, context);
603        }
604        else if (context.renderEmptyValues() && dataHolder.hasValueOrEmpty(relativeDataPath))
605        {
606            XMLUtils.createElement(contentHandler, dataName);
607        }
608    }
609    
610    /**
611     * Convert the data contained in the given {@link DataHolder}
612     * @param dataHolder the {@link ModelLessDataHolder}
613     * @param context The context of the data to convert
614     * @return The data of the given {@link DataHolder} as JSON
615     * @throws UnknownTypeException if there is no compatible type with the value to convert
616     * @throws NotUniqueTypeException if there are many compatible types (there is no way to determine which type is the good one) with the value to convert
617     */
618    public static Map<String, Object> dataToJSON(ModelLessDataHolder dataHolder, DataContext context) throws UnknownTypeException, NotUniqueTypeException
619    {
620        Map<String, Object> result = new HashMap<>();
621        for (String dataName : dataHolder.getDataNames())
622        {
623            DataContext newContext = context.cloneContext().addSegmentToDataPath(dataName);
624            result.put(dataName, dataToJSON(dataHolder, dataName, newContext));
625        }
626        
627        return result;
628    }
629    
630    /**
631     * Convert the data at the given relative path into a JSON object
632     * @param dataHolder the {@link ModelLessDataHolder}
633     * @param relativeDataPath the path of the data to convert, relative to the given data holder
634     * @param context The context of the data to convert
635     * @return The value as JSON
636     * @throws UnknownTypeException if there is no compatible type with the value to convert
637     * @throws NotUniqueTypeException if there are many compatible types (there is no way to determine which type is the good one) with the value to convert
638     */
639    public static Object dataToJSON(ModelLessDataHolder dataHolder, String relativeDataPath, DataContext context) throws UnknownTypeException, NotUniqueTypeException
640    {
641        ModelItemType type = dataHolder.getType(relativeDataPath);
642        Object value = dataHolder.getValueOfType(relativeDataPath, type.getId());
643        return type.valueToJSONForClient(value, context);
644    }
645    
646    /**
647     * Find all modifiable data of the given type on a {@link ModelAwareDataHolder}, including in composite or repeater entries.
648     * Properties are not modifiables, so they are excluded from the result
649     * @param <T> the actual type of the requested data.
650     * @param dataHolder the data holder.
651     * @param type the type identifier.
652     * @return a Map with all data paths as keys and corresponding data as values.
653     */
654    public static <T extends Object> Map<String, T> findEditableItemsByType(ModelAwareDataHolder dataHolder, String type)
655    {
656        ViewItemAccessor viewItemAccessor = org.ametys.runtime.model.ViewHelper.createViewItemAccessor(dataHolder.getModel());
657        return _findItemsByType(dataHolder, viewItemAccessor, type, false, StringUtils.EMPTY);
658    }
659    
660    /**
661     * Find all data of the given type on a {@link ModelAwareDataHolder}, including in composite or repeater entries.
662     * @param <T> the actual type of the requested data.
663     * @param dataHolder the data holder.
664     * @param type the type identifier.
665     * @return a Map with all data paths as keys and corresponding data as values.
666     */
667    public static <T extends Object> Map<String, T> findItemsByType(ModelAwareDataHolder dataHolder, String type)
668    {
669        ViewItemAccessor viewItemAccessor = org.ametys.runtime.model.ViewHelper.createViewItemAccessor(dataHolder.getModel());
670        return _findItemsByType(dataHolder, viewItemAccessor, type, true, StringUtils.EMPTY);
671    }
672    
673    /**
674     * Find all data of the given type on a {@link ModelAwareDataHolder}, including in composite or repeater entries.
675     * @param <T> the actual type of the requested data.
676     * @param dataHolder the data holder.
677     * @param viewItemAccessor the view items to restrict to
678     * @param type the type identifier.
679     * @return a Map with all data paths as keys and corresponding data as values.
680     */
681    public static <T extends Object> Map<String, T> findItemsByType(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, String type)
682    {
683        return _findItemsByType(dataHolder, viewItemAccessor, type, true, StringUtils.EMPTY);
684    }
685    
686    private static <T extends Object> Map<String, T> _findItemsByType(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, String type, boolean includeNotEditableItems, String prefix)
687    {
688        Map<String, T> attributes = new HashMap<>();
689        
690        ViewHelper.visitView(viewItemAccessor,
691            (element, definition) -> {
692                // simple element
693                if ((includeNotEditableItems || definition.isEditable()) && definition.getType().getId().equals(type))
694                {
695                    String name = definition.getName();
696                    attributes.put(prefix + name, dataHolder.getValue(name));
697                }
698            }, 
699            (group, definition) -> {
700                // composite
701                String name = definition.getName();
702                ModelAwareComposite composite = dataHolder.getComposite(name);
703                if (composite != null)
704                {
705                    attributes.putAll(_findItemsByType(composite, group, type, includeNotEditableItems, prefix + name + "/"));
706                }
707            }, 
708            (group, definition) -> {
709                // repeater
710                String name = definition.getName();
711                ModelAwareRepeater repeater = dataHolder.getRepeater(name);
712                if (repeater != null)
713                {
714                    for (ModelAwareRepeaterEntry entry : repeater.getEntries())
715                    {
716                        attributes.putAll(_findItemsByType(entry, group, type, includeNotEditableItems, prefix + name + "[" + entry.getPosition() + "]/"));
717                    }
718                }
719            }, 
720            group -> attributes.putAll(_findItemsByType(dataHolder, group, type, includeNotEditableItems, prefix)));
721        
722        return attributes;
723    }
724    
725    /**
726     * Retrieve the {@link ExternalizableDataProviderExtensionPoint}
727     * @return the {@link ExternalizableDataProviderExtensionPoint}
728     */
729    public static ExternalizableDataProviderExtensionPoint getExternalizableDataProviderExtensionPoint()
730    {
731        return _externalizableDataProviderEP;
732    }
733    
734    /**
735     * Returns the value of the synchronized value corresponding of the future status
736     * If the value is not a {@link SynchronizableValue}, the value itself is returned
737     * The future status is determined from the synchronizable value itself or from the context if needed.
738     * As the returned value is extracted from the synchronizable value, it can be an {@link UntouchedValue} 
739     * @param value the synchronizable value
740     * @param dataHolder the data holder concerned by the value
741     * @param modelItem the model item corresponding to the value
742     * @param dataPath the current data path of the value. Can be empty if the data is in a repeater, in a not yet existing entry
743     * @param context the synchronization context, used to compute the future status of the value
744     * @return the value of the synchronized value corresponding of the future status
745     */
746    public static Object getValueFromSynchronizableValue(Object value, ModelAwareDataHolder dataHolder, ModelItem modelItem, Optional<String> dataPath, SynchronizationContext context)
747    {
748        if (value instanceof SynchronizableValue)
749        {
750            SynchronizableValue syncValue = (SynchronizableValue) value;
751            Optional<ExternalizableDataStatus> valueStatus = Optional.empty();
752            if (!getExternalizableDataProviderExtensionPoint().isDataExternalizable(dataHolder, modelItem, context.getExternalizableDataContext()))
753            {
754                // If the data is not externalizable, use the local value
755                valueStatus = Optional.of(ExternalizableDataStatus.LOCAL);
756            }
757            else if (syncValue.getExternalizableStatus() != null)
758            {
759                // If the given synchronizable value specifies the future status of the data,
760                // the value to validate is the corresponding one
761                valueStatus = Optional.of(syncValue.getExternalizableStatus());
762            }
763            else if (dataPath.isPresent())
764            {
765                // Check if the status of the data is already present in the repository
766                String[] pathSegments = StringUtils.split(dataPath.get(), ModelItem.ITEM_PATH_SEPARATOR);
767                ModelAwareDataHolder parentDataHolder;
768                String dataName;
769                if (pathSegments.length == 1)
770                {
771                    parentDataHolder = dataHolder;
772                    dataName = pathSegments[0];
773                }
774                else
775                {
776                    String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1);
777                    parentDataHolder = dataHolder.getValue(parentPath);
778                    dataName = pathSegments[pathSegments.length - 1];
779                }
780                
781                if (parentDataHolder != null && parentDataHolder.getRepositoryData().hasValue(dataName + ModelAwareDataHolder.STATUS_SUFFIX))
782                {
783                    // Use the current status if it is present
784                    valueStatus = Optional.of(parentDataHolder.getStatus(dataName));
785                }
786                else if (context.forceStatusIfNotPresent())
787                {
788                    valueStatus = Optional.of(context.getStatusToSynchronize());
789                }
790            }
791            else if (context.forceStatusIfNotPresent())
792            {
793                // The data does not exist yet, get the status from the context
794                valueStatus = Optional.of(context.getStatusToSynchronize()); 
795            }
796                
797            return syncValue.getValue(valueStatus);
798        }
799        else
800        {
801            return value;
802        }
803    }
804    
805    /**
806     * Check if the given {@link ModelItem} is multivalued, either being itself mutlivalued or part of a repeater
807     * @param item the model item to check
808     * @return true if the model item is multivalued
809     */
810    public static boolean isMultiple(ModelItem item)
811    {
812        ModelItem currentItem = item;
813        while (currentItem != null)
814        {
815            if (currentItem instanceof RepeaterDefinition || currentItem instanceof ElementDefinition && ((ElementDefinition) currentItem).isMultiple())
816            {
817                return true;
818            }
819            
820            currentItem = currentItem.getParent();
821        }
822        
823        return false;
824    }
825    
826    /**
827     * Converts the given values according to the definitions in the given {@link ViewItemContainer}
828     * @param viewItemContainer the view item container 
829     * @param values the values to convert
830     * @return the converted values
831     */
832    @SuppressWarnings("unchecked")
833    public static Map<String, Object> convertValues(ViewItemContainer viewItemContainer, Map<String, Object> values)
834    {
835        if (values == null)
836        {
837            return null;
838        }
839        
840        Map<String, Object> result = new HashMap<>();
841        
842        ViewHelper.visitView(viewItemContainer, 
843            (element, definition) -> {
844                // simple element
845                String name = definition.getName();
846                
847                if (values.containsKey(name))
848                {
849                    Object value = convertValue(definition, values.get(name));
850                    result.put(name, value);
851                }
852            }, 
853            (group, definition) -> {
854                // composite
855                String name = definition.getName();
856                if (values.containsKey(name))
857                {
858                    result.put(name, convertValues(group, (Map<String, Object>) values.get(name)));
859                }
860            }, 
861            (group, definition) -> {
862                // repeater
863                String name = definition.getName();
864                if (values.containsKey(name))
865                {
866                    List<Map<String, Object>> entries = (List<Map<String, Object>>) values.get(name);
867                    
868                    Object newValue = null;
869                    if (entries != null)
870                    {
871                        List<Map<String, Object>> newEntries = new ArrayList<>();
872                        
873                        for (int i = 0; i < entries.size(); i++)
874                        {
875                            newEntries.add(convertValues(group, entries.get(i)));
876                        }
877                        
878                        newValue = newEntries;
879                    }
880                
881                    result.put(name, newValue);
882                }
883            }, 
884            group -> result.putAll(convertValues(group, values)));
885        
886        return result;
887    }
888    
889    /**
890     * Converts the given value according to the given definition
891     * If the value is multiple, an array is retrieved with each value converted to the right type
892     * @param definition the definition
893     * @param value the value to convert
894     * @return the converted value
895     * @throws BadItemTypeException if the value to convert is not compatible with the data type
896     */
897    public static Object convertValue(ElementDefinition definition, Object value) throws BadItemTypeException
898    {
899        if (value == null)
900        {
901            return null;
902        }
903        
904        if (value instanceof UntouchedValue)
905        {
906            return value;
907        }
908        
909        if (definition.isMultiple())
910        {
911            if (value instanceof Collection)
912            {
913                return ((Collection) value).stream()
914                                           .map(v -> definition.getType().castValue(v))
915                                           .toArray(i -> Array.newInstance(definition.getType().getManagedClass(), i));
916            }
917            else if (value.getClass().isArray())
918            {
919                Class<?> valueType = value.getClass().getComponentType();
920                Stream<Object> valueStream;
921                if (!valueType.isPrimitive())
922                {
923                    valueStream = Arrays.stream((Object[]) value);
924                }
925                else if (valueType.equals(Boolean.TYPE))
926                {
927                    valueStream = Arrays.stream(ArrayUtils.toObject((boolean[]) value));
928                }
929                else if (valueType.equals(Byte.TYPE))
930                {
931                    valueStream = Arrays.stream(ArrayUtils.toObject((byte[]) value));
932                }
933                else if (valueType.equals(Character.TYPE))
934                {
935                    valueStream = Arrays.stream(ArrayUtils.toObject((char[]) value));
936                }
937                else if (valueType.equals(Short.TYPE))
938                {
939                    valueStream = Arrays.stream(ArrayUtils.toObject((short[]) value));
940                }
941                else if (valueType.equals(Integer.TYPE))
942                {
943                    valueStream = Arrays.stream(ArrayUtils.toObject((int[]) value));
944                }
945                else if (valueType.equals(Long.TYPE))
946                {
947                    valueStream = Arrays.stream(ArrayUtils.toObject((long[]) value));
948                }
949                else if (valueType.equals(Double.TYPE))
950                {
951                    valueStream = Arrays.stream(ArrayUtils.toObject((double[]) value));
952                }
953                else if (valueType.equals(Float.TYPE))
954                {
955                    valueStream = Arrays.stream(ArrayUtils.toObject((float[]) value));
956                }
957                else
958                {
959                    throw new IllegalArgumentException(value + " cannot be converted to array");
960                }
961                
962                return valueStream.map(v -> definition.getType().castValue(v)).toArray(i -> (Object[]) Array.newInstance(definition.getType().getManagedClass(), i));
963            }
964            
965            throw new IllegalArgumentException(value + " cannot be converted to array");
966        }
967        else
968        {
969            return definition.getType().castValue(value);
970        }
971    }
972    
973    /**
974     * Aggregates multiple values of the data holders at the given path
975     * @param <T> type of the value to retrieve
976     * @param dataHolders the data holding the values
977     * @param dataPath the path of the data to retrieve
978     * @param managedClass class of the retrieved values
979     * @return aggregated multiple values
980     */
981    @SuppressWarnings("unchecked")
982    public static <T> T aggregateMultipleValues(List<? extends ModelAwareDataHolder> dataHolders, String dataPath, Class managedClass)
983    {
984        // Put all the values in a list
985        List<Object> allValuesAsList = new ArrayList<>();
986        for (ModelAwareDataHolder dataHolder : dataHolders)
987        {
988            Object value = dataHolder.getValue(dataPath, true);
989            if (value != null)
990            {
991                if (value.getClass().isArray())
992                {
993                    for (int i = 0; i < Array.getLength(value); i++)
994                    {
995                        allValuesAsList.add(Array.get(value, i));
996                    }
997                }
998                else
999                {
1000                    allValuesAsList.add(value);
1001                }
1002            }
1003        }
1004        
1005        Object[] array = (Object[]) Array.newInstance(managedClass, allValuesAsList.size());
1006        return (T) allValuesAsList.toArray(array);
1007    }
1008}