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