001/*
002 *  Copyright 2019 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.cms.data;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Optional;
027import java.util.Set;
028import java.util.function.Function;
029import java.util.stream.Collectors;
030import java.util.stream.IntStream;
031import java.util.stream.Stream;
032
033import org.apache.avalon.framework.component.Component;
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.avalon.framework.service.Serviceable;
037import org.apache.commons.collections4.SetUtils;
038import org.apache.commons.lang3.StringUtils;
039import org.apache.commons.lang3.tuple.ImmutablePair;
040import org.apache.commons.lang3.tuple.Pair;
041
042import org.ametys.cms.contenttype.ContentAttributeDefinition;
043import org.ametys.cms.contenttype.ContentType;
044import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
045import org.ametys.cms.data.type.ModelItemTypeConstants;
046import org.ametys.cms.repository.Content;
047import org.ametys.cms.repository.ModifiableContent;
048import org.ametys.plugins.repository.AmetysObjectResolver;
049import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus;
050import org.ametys.plugins.repository.data.holder.DataHolder;
051import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
052import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
053import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater;
054import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry;
055import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareComposite;
056import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeater;
057import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeaterEntry;
058import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
059import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater;
060import org.ametys.plugins.repository.data.holder.values.SynchronizableValue;
061import org.ametys.plugins.repository.data.holder.values.SynchronizationContext;
062import org.ametys.plugins.repository.data.holder.values.UntouchedValue;
063import org.ametys.plugins.repository.data.holder.values.ValueContext;
064import org.ametys.plugins.repository.model.RepeaterDefinition;
065import org.ametys.plugins.repository.model.ViewHelper;
066import org.ametys.runtime.model.ModelItem;
067import org.ametys.runtime.model.ModelViewItemGroup;
068import org.ametys.runtime.model.View;
069import org.ametys.runtime.model.ViewItemContainer;
070import org.ametys.runtime.model.exception.BadItemTypeException;
071
072/**
073 * Helper for data of type 'content'
074 */
075public class ContentDataHelper implements Serviceable, Component
076{
077    /** Avalon role */
078    public static final String ROLE = ContentDataHelper.class.getName();
079    
080    private AmetysObjectResolver _resolver;
081    private ContentTypeExtensionPoint _cTypeExtensionPoint;
082    
083    public void service(ServiceManager manager) throws ServiceException
084    {
085        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
086        _cTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
087    }
088    
089    /**
090     * Retrieves the content identifier of a content data
091     * @param dataHolder data holder that contains the content data
092     * @param dataPath path to the content data
093     * @param defaultValue The default value to return
094     * @return the content identifier
095     * @throws BadItemTypeException if the data at the given path is not a content data
096     */
097    public static String getContentIdFromContentData(ModelAwareDataHolder dataHolder, String dataPath, String defaultValue) throws BadItemTypeException
098    {
099        ContentValue value = dataHolder.getValue(dataPath);
100        return Optional.ofNullable(value)
101                       .map(ContentValue::getContentId)
102                       .orElse(defaultValue);
103    }
104    
105    /**
106     * Retrieves the content identifier of a content data
107     * @param dataHolder data holder that contains the content data
108     * @param dataPath path to the content data
109     * @return the content identifier, empty string if it is invalid
110     * @throws BadItemTypeException if the data at the given path is not a content data
111     */
112    public static String getContentIdFromContentData(ModelAwareDataHolder dataHolder, String dataPath) throws BadItemTypeException
113    {
114        return getContentIdFromContentData(dataHolder, dataPath, StringUtils.EMPTY);
115    }
116    
117    /**
118     * Retrieves the content identifiers in an array from a multiple content data
119     * @param dataHolder data holder that contains the multiple content data
120     * @param dataPath path to the multiple content data
121     * @return an array containing the content identifiers
122     * @throws BadItemTypeException if the data at the given path is not a content data
123     */
124    public static boolean isMultipleContentDataEmpty(ModelAwareDataHolder dataHolder, String dataPath) throws BadItemTypeException
125    {
126        return isMultipleContentDataEmpty(dataHolder, dataPath, ValueContext.newInstance());
127    }
128    
129    /**
130     * Retrieves the content identifiers in an array from a multiple content data
131     * @param dataHolder data holder that contains the multiple content data
132     * @param dataPath path to the multiple content data
133     * @param context the context of the value to retrieve
134     * @return an array containing the content identifiers
135     * @throws BadItemTypeException if the data at the given path is not a content data
136     */
137    public static boolean isMultipleContentDataEmpty(ModelAwareDataHolder dataHolder, String dataPath, ValueContext context) throws BadItemTypeException
138    {
139        return getContentIdsStreamFromMultipleContentData(dataHolder, dataPath, context).count() <= 0;
140    }
141    
142    /**
143     * Retrieves the content identifiers in a {@link List} from a multiple content data
144     * @param dataHolder data holder that contains the multiple content data
145     * @param dataPath path to the multiple content data
146     * @return a {@link List} containing the content identifiers
147     * @throws BadItemTypeException if the data at the given path is not a content data
148     */
149    public static List<String> getContentIdsListFromMultipleContentData(ModelAwareDataHolder dataHolder, String dataPath) throws BadItemTypeException
150    {
151        return getContentIdsListFromMultipleContentData(dataHolder, dataPath, ValueContext.newInstance());
152    }
153    
154    /**
155     * Retrieves the content identifiers in a {@link List} from a multiple content data
156     * @param dataHolder data holder that contains the multiple content data
157     * @param dataPath path to the multiple content data
158     * @param context the context of the value to retrieve
159     * @return a {@link List} containing the content identifiers
160     * @throws BadItemTypeException if the data at the given path is not a content data
161     */
162    public static List<String> getContentIdsListFromMultipleContentData(ModelAwareDataHolder dataHolder, String dataPath, ValueContext context) throws BadItemTypeException
163    {
164        return getContentIdsStreamFromMultipleContentData(dataHolder, dataPath, context).collect(Collectors.toList());
165    }
166    
167    /**
168     * Retrieves the content identifiers in an array from a multiple content data
169     * @param dataHolder data holder that contains the multiple content data
170     * @param dataPath path to the multiple content data
171     * @return an array containing the content identifiers
172     * @throws BadItemTypeException if the data at the given path is not a content data
173     */
174    public static String[] getContentIdsArrayFromMultipleContentData(ModelAwareDataHolder dataHolder, String dataPath) throws BadItemTypeException
175    {
176        return getContentIdsArrayFromMultipleContentData(dataHolder, dataPath, ValueContext.newInstance());
177    }
178    
179    /**
180     * Retrieves the content identifiers in an array from a multiple content data
181     * @param dataHolder data holder that contains the multiple content data
182     * @param dataPath path to the multiple content data
183     * @param context the context of the value to retrieve
184     * @return an array containing the content identifiers
185     * @throws BadItemTypeException if the data at the given path is not a content data
186     */
187    public static String[] getContentIdsArrayFromMultipleContentData(ModelAwareDataHolder dataHolder, String dataPath, ValueContext context) throws BadItemTypeException
188    {
189        return getContentIdsStreamFromMultipleContentData(dataHolder, dataPath, context).toArray(String[]::new);
190    }
191    
192    /**
193     * Retrieves a {@link Stream} of the content identifiers from a multiple content data
194     * @param dataHolder data holder that contains the multiple content data
195     * @param dataPath path to the multiple content data
196     * @return a {@link Stream} of the content identifiers
197     * @throws BadItemTypeException if the data at the given path is not a content data
198     */
199    public static Stream<String> getContentIdsStreamFromMultipleContentData(ModelAwareDataHolder dataHolder, String dataPath) throws BadItemTypeException
200    {
201        return getContentIdsStreamFromMultipleContentData(dataHolder, dataPath, ValueContext.newInstance());
202    }
203    
204    /**
205     * Retrieves a {@link Stream} of the content identifiers from a multiple content data
206     * @param dataHolder data holder that contains the multiple content data
207     * @param dataPath path to the multiple content data
208     * @param context the context of the value to retrieve
209     * @return a {@link Stream} of the content identifiers
210     * @throws BadItemTypeException if the data at the given path is not a content data
211     */
212    public static Stream<String> getContentIdsStreamFromMultipleContentData(ModelAwareDataHolder dataHolder, String dataPath, ValueContext context) throws BadItemTypeException
213    {
214        ContentValue[] value = DataHolderHelper.getValue(dataHolder, dataPath, context);
215        return Optional.ofNullable(value)
216                       .map(v -> Arrays.stream(v))
217                       .orElse(Stream.empty())
218                       .map(ContentValue::getContentId);
219    }
220    
221    /**
222     * Prepares a write operation in the given {@link ModelAwareDataHolder} by traversing a {@link View} with the values to be written.<br>
223     * The goal is to find content attributes and to extract added and removed values.
224     * @param viewItemContainer the view to walk through
225     * @param content the source {@link ModifiableContent}
226     * @param values the new values
227     * @param context the synchronization context
228     * @return the contents to-be-added and to-be-removed
229     */
230    public Collection<ReferencedContents> collectReferencedContents(ViewItemContainer viewItemContainer, ModifiableContent content, Map<String, Object> values, SynchronizationContext context)
231    {
232        // content ids, grouped by invert attribute paths, instead of definition paths, so that if several attributes share the same invert attribute, they are handled together
233        // data structure is <definition> mapped <previous contents mapped with their data paths, new contents mapped with their data paths>
234        Map<ContentAttributeDefinition, Pair<Map<ContentValue, List<String>>, Map<ContentValue, List<String>>>> references = new HashMap<>();
235
236        _collectReferencedContents(content, viewItemContainer, Optional.of(content), StringUtils.EMPTY, Optional.of(StringUtils.EMPTY), Optional.of(values), false, context, references);
237        
238        Collection<ReferencedContents> contents = new ArrayList<>();
239        for (ContentAttributeDefinition definition : references.keySet())
240        {
241            Pair<Map<ContentValue, List<String>>, Map<ContentValue, List<String>>> allValues = references.get(definition);
242            Map<ContentValue, List<String>> previousValues = allValues.getLeft();
243            Map<ContentValue, List<String>> newValues = allValues.getRight();
244            
245            Map<ContentValue, List<String>> addedValues = new HashMap<>();
246            Map<ContentValue, List<String>> removedValues = new HashMap<>();
247            
248            newValues.forEach((value, list) -> {
249                if (!previousValues.containsKey(value))
250                {
251                    addedValues.put(value, list);
252                }
253            });
254            
255            previousValues.forEach((value, list) -> {
256                if (!newValues.containsKey(value))
257                {
258                    removedValues.put(value, list);
259                }
260            });
261            
262            String invert = definition.getInvertRelationPath();
263            ContentType invertType = _cTypeExtensionPoint.getExtension(definition.getContentTypeId());
264            ContentAttributeDefinition invertDefinition = (ContentAttributeDefinition) invertType.getModelItem(invert);
265
266            Map<ContentValue, ContentValue> thirdPartyContents = new HashMap<>();
267            if (!DataHolderHelper.isMultiple(invertDefinition))
268            {
269                List<ModifiableContent> refContents = addedValues.keySet().stream().map(v -> v.getContentIfExists()).flatMap(Optional::stream).collect(Collectors.toList());
270                
271                for (ModifiableContent refContent : refContents)
272                {
273                    ContentValue previousValue = refContent.getValue(invert);
274                    if (previousValue != null)
275                    {
276                        String previousContentId = previousValue.getContentId();
277                        if (!content.getId().equals(previousContentId))
278                        {
279                            // the single-valued content attribute is about to change, we should store the 3rd party content involved in the invert relation
280                            thirdPartyContents.put(new ContentValue(refContent), previousValue);
281                        }
282                    }
283                }
284            }
285            
286            contents.add(new ReferencedContents(definition, addedValues, removedValues, thirdPartyContents));
287        }
288        
289        return contents;
290    }
291    
292    @SuppressWarnings("unchecked")
293    private void _collectReferencedContents(Content content,
294                                            ViewItemContainer viewItemContainer, 
295                                            Optional<ModelAwareDataHolder> currentDataHolder, 
296                                            String dataPathPrefix,
297                                            Optional<String> oldDataPathPrefix, 
298                                            Optional<Map<String, Object>> values, 
299                                            boolean insideKeptDataHolder,
300                                            SynchronizationContext context,
301                                            Map<ContentAttributeDefinition, Pair<Map<ContentValue, List<String>>, Map<ContentValue, List<String>>>> references)
302    {
303        ViewHelper.visitView(viewItemContainer, 
304            (element, definition) -> {
305                // simple element
306                String name = definition.getName();
307                
308                if (definition instanceof ContentAttributeDefinition)
309                {
310                    ContentAttributeDefinition contentDefinition = (ContentAttributeDefinition) definition;
311                    String invert = contentDefinition.getInvertRelationPath();
312                    if (invert != null)
313                    {
314                        _collectInvertRelation(content, currentDataHolder, dataPathPrefix, oldDataPathPrefix, values, insideKeptDataHolder, name, contentDefinition, context, references);
315                    }
316                }
317            }, 
318            (group, definition) -> {
319                // composite
320                String name = definition.getName();
321                String updatedPrefix = dataPathPrefix + name + ModelItem.ITEM_PATH_SEPARATOR;
322                Optional<String> updatedOldPrefix = oldDataPathPrefix.map(p -> p + name + ModelItem.ITEM_PATH_SEPARATOR);
323                _collectReferencedContents(content, group, currentDataHolder.map(v -> v.getComposite(name)), updatedPrefix, updatedOldPrefix,  values.map(v -> (Map<String, Object>) v.get(name)), insideKeptDataHolder, context, references);
324            }, 
325            (group, definition) -> {
326                // repeater
327                String name = definition.getName();
328                _collectRepeater(content, group, currentDataHolder, dataPathPrefix, oldDataPathPrefix, values, insideKeptDataHolder, context, name, references);
329            }, 
330            group -> _collectReferencedContents(content, group, currentDataHolder, dataPathPrefix, oldDataPathPrefix, values, insideKeptDataHolder, context, references));
331    }
332    
333    private void _collectInvertRelation(Content content,
334                                        Optional<ModelAwareDataHolder> currentDataHolder, 
335                                        String dataPathPrefix,
336                                        Optional<String> oldDataPathPrefix,
337                                        Optional<Map<String, Object>> values, 
338                                        boolean insideKeptDataHolder,
339                                        String name,
340                                        ContentAttributeDefinition definition,
341                                        SynchronizationContext context,
342                                        Map<ContentAttributeDefinition, Pair<Map<ContentValue, List<String>>, Map<ContentValue, List<String>>>> references)
343    {
344        String dataPath = dataPathPrefix + name;
345
346        Set<ContentValue> previousContents = currentDataHolder
347                .map(dh -> dh.getValue(name))
348                .map(this::_getContentRefs)
349                .orElseGet(Collections::emptySet);
350        Set<ContentValue> newContents;
351        
352        if (insideKeptDataHolder)
353        {
354            newContents = previousContents;
355        }
356        else
357        {
358            Optional<Object> rawValue = values.map(v -> v.get(name));
359            
360            Optional<String> oldDataPath = oldDataPathPrefix.map(p -> p + name);
361            rawValue = rawValue.map(v -> DataHolderHelper.getValueFromSynchronizableValue(v, content, definition, oldDataPath, context));
362            
363            // If the current value is an untouched value, don't edit the invert relation
364            if (rawValue.filter(UntouchedValue.class::isInstance).isPresent())
365            {
366                return;
367            }
368            
369            newContents = rawValue.map(this::_getContentRefs)
370                                  .orElseGet(Collections::emptySet);
371            
372            SynchronizableValue.Mode mode = values.map(v -> v.get(name))
373                                                  .filter(SynchronizableValue.class::isInstance)
374                                                  .map(SynchronizableValue.class::cast)
375                                                  .map(v -> v.getMode())
376                                                  .orElse(SynchronizableValue.Mode.REPLACE);
377            
378            if (mode == SynchronizableValue.Mode.REMOVE)
379            {
380                newContents = SetUtils.difference(previousContents, newContents);
381            }
382            else if (mode == SynchronizableValue.Mode.APPEND)
383            {
384                newContents = SetUtils.union(previousContents, newContents);
385            }
386        }
387
388        _addReferences(references, definition, dataPath, previousContents, newContents);
389    }
390    
391    private Set<ContentValue> _getContentRefs(Object value)
392    {
393        if (value instanceof ContentValue)
394        {
395            return Set.of((ContentValue) value);
396        }
397        else if (value instanceof ContentValue[])
398        {
399            return Set.copyOf(Arrays.asList((ContentValue[]) value));
400        }
401        else if (value instanceof String)
402        {
403            return Set.of(new ContentValue(_resolver, (String) value));
404        }
405        else if (value instanceof String[])
406        {
407            return Arrays.stream((String[]) value).map(id -> new ContentValue(_resolver, id)).collect(Collectors.toSet());
408        }
409        else if (value instanceof ModifiableContent)
410        {
411            return Set.of(new ContentValue((ModifiableContent) value));
412        }
413        else if (value instanceof ModifiableContent[])
414        {
415            return Arrays.stream((ModifiableContent[]) value).map(ContentValue::new).collect(Collectors.toSet());
416        }
417        
418        return Collections.emptySet();
419    }
420    
421    private void _addReferences(Map<ContentAttributeDefinition, Pair<Map<ContentValue, List<String>>, Map<ContentValue, List<String>>>> references, ContentAttributeDefinition definition, String dataPath, Set<ContentValue> previousContents, Set<ContentValue> newContents)
422    {
423        Pair<Map<ContentValue, List<String>>, Map<ContentValue, List<String>>> values = references.get(definition);
424        Map<ContentValue, List<String>> previousValues = values == null ? new HashMap<>() : values.getLeft();
425        Map<ContentValue, List<String>> newValues = values == null ? new HashMap<>() : values.getRight();
426        
427        previousContents.forEach(c -> previousValues.computeIfAbsent(c, __ -> new ArrayList<>()).add(dataPath));
428        newContents.forEach(c -> newValues.computeIfAbsent(c, __ -> new ArrayList<>()).add(dataPath));
429        
430        references.put(definition, Pair.of(previousValues, newValues));
431    }
432    
433    @SuppressWarnings("unchecked")
434    private void _collectRepeater(Content content,
435                                  ModelViewItemGroup group, 
436                                  Optional<ModelAwareDataHolder> currentDataHolder, 
437                                  String dataPathPrefix,
438                                  Optional<String> oldDataPathPrefix,
439                                  Optional<Map<String, Object>> values, 
440                                  boolean insideKeptDataHolder,
441                                  SynchronizationContext context,
442                                  String name,
443                                  Map<ContentAttributeDefinition, Pair<Map<ContentValue, List<String>>, Map<ContentValue, List<String>>>> references)
444    {
445        Object value = values.map(v -> v.get(name)).orElse(null);
446        List<Map<String, Object>> newEntries = null;
447        Map<Integer, Integer> mapping = null;
448        SynchronizableRepeater.Mode mode = SynchronizableRepeater.Mode.REPLACE_ALL;
449        if (value instanceof List)
450        {
451            newEntries = (List<Map<String, Object>>) value;
452            mapping = IntStream.rangeClosed(1, newEntries.size()).boxed().collect(Collectors.toMap(Function.identity(), Function.identity()));
453        }
454        else if (value instanceof SynchronizableRepeater)
455        {
456            newEntries = ((SynchronizableRepeater) value).getEntries();
457            mapping = ((SynchronizableRepeater) value).getPositionsMapping();
458            mode = ((SynchronizableRepeater) value).getMode();
459        }
460        
461        // first collect data for actually existing entries
462        Set<Integer> alreadyHandledValues = new HashSet<>();
463        ModelAwareRepeater repeater = currentDataHolder.map(v -> v.getRepeater(name)).orElse(null);
464        if (repeater != null)
465        {
466            for (ModelAwareRepeaterEntry entry : repeater.getEntries())
467            {
468                int position = entry.getPosition();
469                
470                Optional<Map<String, Object>> newValues = Optional.empty();
471                String newPositionSuffix = "";
472                boolean newKeptValue = insideKeptDataHolder;
473                
474                if (!insideKeptDataHolder)
475                {
476                    if (mode == SynchronizableRepeater.Mode.REPLACE_ALL)
477                    {
478                        Integer newPosition = mapping != null ? mapping.get(position) : null;
479                        
480                        if (newPosition != null)
481                        {
482                            // not removed entry
483                            newValues = newEntries != null ? Optional.ofNullable(newEntries.get(newPosition - 1)) : Optional.empty();
484                            newPositionSuffix = "[" + newPosition + "]";
485                            alreadyHandledValues.add(newPosition);
486                        }
487                    }
488                    else if (mode == SynchronizableRepeater.Mode.REPLACE)
489                    {
490                        List<Integer> replacePositions = ((SynchronizableRepeater) value).getReplacePositions();
491                        int index = replacePositions.indexOf(position);
492                        
493                        if (index >= 0)
494                        {
495                            assert newEntries != null;
496                            newValues = Optional.ofNullable(newEntries.get(index));
497                            newPositionSuffix = "[" + position + "]";
498                            alreadyHandledValues.add(position);
499                        }
500                        else
501                        {
502                            // entry kept as is
503                            newKeptValue = true;
504                        }
505                    }
506                    else
507                    {
508                        Set<Integer> removedEntries = ((SynchronizableRepeater) value).getRemovedEntries();
509                        newKeptValue = !removedEntries.contains(position);
510                    }
511                }
512                
513                String updatePrefix = dataPathPrefix + name + newPositionSuffix + ModelItem.ITEM_PATH_SEPARATOR;
514                Optional<String> updatedOldPrefix = oldDataPathPrefix.map(p -> dataPathPrefix + name + "[" + position + "]" + ModelItem.ITEM_PATH_SEPARATOR);
515                _collectReferencedContents(content, group, Optional.of(entry), updatePrefix, updatedOldPrefix, newValues, newKeptValue, context, references);
516            }
517        }
518        
519        // then collect data for newly created entries
520        if (newEntries != null)
521        {
522            for (int i = 1; i <= newEntries.size(); i++)
523            {
524                if (!alreadyHandledValues.contains(i))
525                {
526                    String updatedPrefix = dataPathPrefix + name + "[" + i + "]" + ModelItem.ITEM_PATH_SEPARATOR;
527                    _collectReferencedContents(content, group, Optional.empty(), updatedPrefix, Optional.empty(), Optional.of(newEntries.get(i - 1)), insideKeptDataHolder, context, references);
528                }
529            }
530        }
531    }
532    
533    /**
534     * Find any broken reference in data holder's content attributes.
535     * A reference is considered as broken if the referenced content does not exist
536     * @param dataHolder the {@link ModelAwareDataHolder} to inspect.
537     * @param fix if <code>true</code>, and the dataHolder is modifiable, broken references will be removed
538     * Warning: if modifications are made on the given data holder, the modifications won't be saved and the workflow won't change 
539     * @return all broken references, by attribute path
540     */
541    public static Map<String, List<String>> checkBrokenReferences(ModelAwareDataHolder dataHolder, boolean fix)
542    {
543        Map<String, List<String>> result = new HashMap<>();
544        Map<String, Object> contentAttributes = DataHolderHelper.findEditableItemsByType(dataHolder, ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID);
545        for (Map.Entry<String, Object> entry : contentAttributes.entrySet())
546        {
547            String dataPath = entry.getKey();
548            Object value = entry.getValue();
549            if (value instanceof ContentValue)
550            {
551                ContentValue contentValue = (ContentValue) value;
552                if (contentValue.getContentIfExists().isEmpty())
553                {
554                    result.computeIfAbsent(dataPath, __ -> new ArrayList<>()).add(contentValue.getContentId());
555                    if (fix && dataHolder instanceof ModifiableModelAwareDataHolder)
556                    {
557                        ((ModifiableModelAwareDataHolder) dataHolder).setValue(dataPath, null);
558                    }
559                }
560            }
561            else if (value instanceof ContentValue[])
562            {
563                List<ContentValue> newValues = new ArrayList<>();
564                for (ContentValue contentValue : (ContentValue[]) value)
565                {
566                    if (contentValue.getContentIfExists().isEmpty())
567                    {
568                        result.computeIfAbsent(dataPath, __ -> new ArrayList<>()).add(contentValue.getContentId());
569                    }
570                    else if (fix)
571                    {
572                        newValues.add(contentValue);
573                    }
574                }
575                if (fix && dataHolder instanceof ModifiableModelAwareDataHolder)
576                {
577                    ((ModifiableModelAwareDataHolder) dataHolder).setValue(dataPath, newValues.toArray(ContentValue[]::new));
578                }
579            }
580        }
581        return result;
582    }
583    
584    /**
585     * Find any broken invert relation in a content attributes.
586     * The invert relation is considered as broken if the referenced content does not reference the first one by the attribute with the invert relation path
587     * This check does not take nonexistent referenced contents into account. First call the {@link ContentDataHelper#checkBrokenReferences(ModelAwareDataHolder, boolean)} method
588     * @param content the content to inspect.
589     * @param fix if <code>true</code>, and the referenced contents are modifiable, broken invert relations will be fixed by adding the relation on the referenced content
590     * if the referenced content is referencing another content and the attribute is single (and not in a repeater), the third content will be modified too, to remove the relation to the second content.
591     * Warning: if modifications are made on some contents, the modifications won't be saved and the workflows won't change 
592     * @return All broken invert relations in a collection of {@link ReferencedContents}.
593     * Call {@link ReferencedContents#getAddedContents()} to get all contents referenced by the given content that does not reference it in return
594     * Call {@link ReferencedContents#getThirdPartyContents()} to get contents referenced by content in added contents that reference it in return
595     */
596    public static Collection<ReferencedContents> checkBrokenInvertRelations(Content content, boolean fix)
597    {
598        // Search for all content attribute's values
599        Map<String, Object> contentAttributes = DataHolderHelper.findEditableItemsByType(content, ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID);
600
601        // Collect broken invert relations
602        Collection<ReferencedContents> brokenInvertRelations = new ArrayList<>();
603        for (Map.Entry<String, Object> entry : contentAttributes.entrySet())
604        {
605            String dataPath = entry.getKey();
606            Object value = entry.getValue();
607            ContentAttributeDefinition definition = (ContentAttributeDefinition) content.getDefinition(dataPath);
608            
609            // Continue check only on content attributes with an invert relation path
610            if (StringUtils.isNotBlank(definition.getInvertRelationPath()))
611            {
612                Map<ContentValue, List<String>> contentsToAdd = new HashMap<>();
613                Map<ContentValue, ContentValue> thirdPartyContents = new HashMap<>();
614                
615                if (value instanceof ContentValue)
616                {
617                    Pair<Boolean, Optional<ContentValue>> isBroken = _isInvertRelationBroken(content, (ContentValue) value, definition);
618                    if (isBroken.getLeft())
619                    {
620                        contentsToAdd.put((ContentValue) value, List.of(dataPath));
621                        isBroken.getRight()
622                                .ifPresent(v -> thirdPartyContents.put((ContentValue) value, v));
623                    }
624                }
625                else if (value instanceof ContentValue[])
626                {
627                    for (ContentValue contentValue : (ContentValue[]) value)
628                    {
629                        Pair<Boolean, Optional<ContentValue>> isBroken = _isInvertRelationBroken(content, contentValue, definition);
630                        if (isBroken.getLeft())
631                        {
632                            contentsToAdd.computeIfAbsent(contentValue, __ -> new ArrayList<>()).add(dataPath);
633                            isBroken.getRight()
634                                    .ifPresent(v -> thirdPartyContents.put(contentValue, v));
635                        }
636                    }
637                }
638                
639                // Add a ReferencedContents object if there is at least one broken relation
640                // No need to check the third part content, this map can't have entries if the first one is empty
641                if (!contentsToAdd.isEmpty())
642                {
643                    ReferencedContents brokenInvertRelation = new ReferencedContents(definition, contentsToAdd, Map.of(), thirdPartyContents);
644                    brokenInvertRelations.add(brokenInvertRelation);
645                }
646            }
647        }
648        
649        if (fix)
650        {
651            Map<String, ModifiableContent> modifiedContents = new HashMap<>();
652            
653            for (ReferencedContents brokenInvertRelation : brokenInvertRelations)
654            {
655                // Add relation on contents referenced by the given one
656                String invertRelationPath = brokenInvertRelation.getDefinition().getInvertRelationPath();
657                modifiedContents.putAll(manageInvertRelations(invertRelationPath, brokenInvertRelation.getAddedContents(), content.getId(), ContentDataHelper::addInvertRelation, modifiedContents, Map.of()));
658                
659                // Remove relations on third party contents
660                Map<ContentValue, Collection<String>> thirdPartyContents = new HashMap<>();
661                for (Map.Entry<ContentValue, ContentValue> thirdPartyContent : brokenInvertRelation.getThirdPartyContents().entrySet())
662                {
663                    thirdPartyContents.computeIfAbsent(thirdPartyContent.getValue(), __ -> new ArrayList<>()).add(thirdPartyContent.getKey().getContentId());
664                }
665                modifiedContents.putAll(manageInvertRelations(brokenInvertRelation.getDefinition().getPath(), thirdPartyContents, ContentDataHelper::removeInvertRelation, modifiedContents, Map.of()));
666            }
667        }
668
669        return brokenInvertRelations;
670    }
671    
672    /**
673     * Check if the invert relation between the given contents is broken
674     * @param source the source content of the invert relation
675     * @param destinationValue the destination content of the invert relation
676     * @param definition the definition concerned by the checked invert relation
677     * @return a {@link Pair} containing the result of the check ({@link Boolean} part left) and an optional third part content, if the relation is broken and the destination content references another content in a single attribute that should be modified
678     */
679    private static Pair<Boolean, Optional<ContentValue>> _isInvertRelationBroken(Content source, ContentValue destinationValue, ContentAttributeDefinition definition)
680    {
681        Boolean isBroken = Boolean.FALSE;
682        Optional<ContentValue> thirdPartContent = Optional.empty();
683
684        // Get the target content - ignore nonexistent contents, the checkBrokenReferences dealt with it
685        Optional<? extends Content> optDestination = destinationValue.getContentIfExists();
686        if (optDestination.isPresent())
687        {
688            // Get the value of the target content's attribute
689            Content destination = optDestination.get();
690            Object value = destination.getValue(definition.getInvertRelationPath(), true);
691            if (value == null)
692            {
693                isBroken = true;
694            }
695            else if (value instanceof ContentValue)
696            {
697                // If the value does not correspond to the current data holder
698                if (!((ContentValue) value).getContentId().equals(source.getId()))
699                {
700                    isBroken = true;
701                    
702                    // If the value of the destination is in relation to the destination too, this third part content will have to be modified
703                    Object thirdPartValue = ((ContentValue) value).getValue(definition.getPath(), true);
704                    if (thirdPartValue instanceof ContentValue)
705                    {
706                        if (((ContentValue) thirdPartValue).getContentId().equals(destination.getId()))
707                        {
708                            thirdPartContent = Optional.of((ContentValue) value);
709                        }
710                    }
711                    else if (thirdPartValue instanceof ContentValue[])
712                    {
713                        for (ContentValue thridPartSingleValue : (ContentValue[]) thirdPartValue)
714                        {
715                            if (thridPartSingleValue.getContentId().equals(destination.getId()))
716                            {
717                                thirdPartContent = Optional.of((ContentValue) value);
718                                break;
719                            }
720                        }
721                    }
722                }
723            }
724            else if (value instanceof ContentValue[])
725            {
726                // Search for the source content in the values of the destination
727                boolean foundSource = false;
728                for (ContentValue contentValue : (ContentValue[]) value)
729                {
730                    if (contentValue.getContentId().equals(source.getId()))
731                    {
732                        foundSource = true;
733                        break;
734                    }
735                }
736                
737                isBroken = Boolean.valueOf(!foundSource);
738            }
739        }
740        
741        return new ImmutablePair<>(isBroken, thirdPartContent);
742    }
743    
744    /**
745     * Manages the invert relations concerned by the given path, through the given {@link InvertRelationManager} 
746     * @param invertRelationPath the concerned invert relation path
747     * @param referencedContents a {@link Set} containing the referenced contents to manage (add or remove the relation)
748     * @param referencingContentId the id of the content referencing the given contents
749     * @param invertRelationManager the {@link InvertRelationManager} to use (to add or remove the invert relations)
750     * @param alreadyModifiedContents a {@link Map} of contents (indexed by their identifiers) that have already been modified. 
751     *      This map will be used to initialize the returned map and to search for contents that have already been resolved (to resolve each content only once, even if there are several modifications on a same content)
752     * @param externalizableDataContext the context {@link Map} that is used to determine if a data is externalizable
753     * @return a {@link Map} containing the contents (indexed by their identifiers) that have been modified by this call and the ones that have already been modified
754     */
755    public static Map<String, ModifiableContent> manageInvertRelations(String invertRelationPath, Set<ContentValue> referencedContents, String referencingContentId, InvertRelationManager invertRelationManager, Map<String, ModifiableContent> alreadyModifiedContents, Map<String, Object> externalizableDataContext)
756    {
757        Map<ContentValue, Collection<String>> referencedContentsWithReferencing = referencedContents.stream()
758                .collect(Collectors.toMap(value -> value, value -> Set.of(referencingContentId)));
759        
760        return manageInvertRelations(invertRelationPath, referencedContentsWithReferencing, invertRelationManager, alreadyModifiedContents, externalizableDataContext);
761    }
762    
763    /**
764     * Manages the invert relations concerned by the given path, through the given {@link InvertRelationManager} 
765     * @param invertRelationPath the concerned invert relation path
766     * @param referencedContents a {@link Map} containing the referenced contents to manage (add or remove the relation) and the id of the content referencing this content
767     * @param invertRelationManager the {@link InvertRelationManager} to use (to add or remove the invert relations)
768     * @param alreadyModifiedContents a {@link Map} of contents (indexed by their identifiers) that have already been modified. 
769     *      This map will be used to initialize the returned map and to search for contents that have already been resolved (to resolve each content only once, even if there are several modifications on a same content)
770     * @param externalizableDataContext the context {@link Map} that is used to determine if a data is externalizable
771     * @return a {@link Map} containing the contents (indexed by their identifiers) that have been modified by this call and the ones that have already been modified
772     */
773    public static Map<String, ModifiableContent> manageInvertRelations(String invertRelationPath, Map<ContentValue, Collection<String>> referencedContents, InvertRelationManager invertRelationManager, Map<String, ModifiableContent> alreadyModifiedContents, Map<String, Object> externalizableDataContext)
774    {
775        Map<String, ModifiableContent> allModifiedContents = new HashMap<>(alreadyModifiedContents);
776        
777        for (ContentValue referencedContent : referencedContents.keySet())
778        {
779            String contentId = referencedContent.getContentId();
780            Optional<ModifiableContent> optContent = allModifiedContents.containsKey(contentId)
781                    ? Optional.of(allModifiedContents.get(contentId))
782                            : referencedContent.getContentIfExists();
783            
784            if (optContent.isPresent())
785            {
786                ModifiableContent content = optContent.get();
787                
788                ModelItem modelItem = content.getDefinition(invertRelationPath);
789                ValueContext valueContext = ValueContext.newInstance();
790                if (DataHolderHelper.getExternalizableDataProviderExtensionPoint().isDataExternalizable(content, modelItem))
791                {
792                    if (ExternalizableDataStatus.EXTERNAL.equals(content.getStatus(invertRelationPath)))
793                    {
794                        throw new IllegalStateException("Unable to manage the invert relation on distant content '" + content + "', the data at path '" + invertRelationPath + "' is external.");
795                    }
796                    else
797                    {
798                        valueContext.withStatus(ExternalizableDataStatus.LOCAL);
799                    }
800                }
801                
802                if (invertRelationManager.manageInvertRelation(content, invertRelationPath, referencedContents.get(referencedContent), valueContext))
803                {
804                    allModifiedContents.put(contentId, content);
805                }
806            }
807        }
808        
809        return allModifiedContents;
810    }
811    
812    /**
813     * Adds the invert relation to the given {@link DataHolder}
814     * @param dataHolder the data holder where to add the invert relation
815     * @param invertRelationPath the path of the invert relation
816     * @param referencingContentIds the id of the contents that reference the data holder
817     * @param context the value's context
818     * @return <code>true</code> if the given data holder has been modified, <code>false</code> otherwise
819     */
820    public static boolean addInvertRelation(ModifiableModelAwareDataHolder dataHolder, String invertRelationPath, Collection<String> referencingContentIds, ValueContext context)
821    {
822        String[] pathSegments = StringUtils.split(invertRelationPath, ModelItem.ITEM_PATH_SEPARATOR);
823        String dataName = pathSegments[0];
824        
825        if (pathSegments.length == 1)
826        {
827            ContentAttributeDefinition invertRelationDefinition = (ContentAttributeDefinition) dataHolder.getDefinition(invertRelationPath);
828            if (invertRelationDefinition.isMultiple())
829            {
830                List<String> newValues = new ArrayList<>();
831                
832                if (DataHolderHelper.hasValue(dataHolder, invertRelationPath, context))
833                {
834                    ContentValue[] oldValues = DataHolderHelper.getValue(dataHolder, invertRelationPath, context);
835                    
836                    newValues = Arrays.stream(oldValues)
837                            .map(ContentValue::getContentId)
838                            .collect(Collectors.toList());
839                }
840
841                newValues.addAll(referencingContentIds);
842                DataHolderHelper.setValue(dataHolder, invertRelationPath, newValues.toArray(new String[newValues.size()]), context);
843                return true;
844            }
845            else
846            {
847                assert referencingContentIds.size() == 1;
848                DataHolderHelper.setValue(dataHolder, invertRelationPath, referencingContentIds.iterator().next(), context);
849                return true;
850            }
851        }
852        else
853        {
854            ModelItem modelItem = dataHolder.getDefinition(dataName);
855            String subInvertRelationPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
856            if (modelItem instanceof RepeaterDefinition)
857            {
858                ModifiableModelAwareRepeaterEntry repeaterEntry = dataHolder.getRepeater(dataName, true).addEntry();
859                return addInvertRelation(repeaterEntry, subInvertRelationPath, referencingContentIds, context); 
860            }
861            else
862            {
863                ModifiableModelAwareComposite composite = dataHolder.getComposite(dataName, true);
864                return addInvertRelation(composite, subInvertRelationPath, referencingContentIds, context);
865            }
866        }
867    }
868    
869    /**
870     * Removes the invert relation to the given {@link DataHolder}
871     * @param referencedContent the content where to remove the invert relation
872     * @param invertRelationPath the path of the invert relation
873     * @param referencingContentIds the id of the contents that do not reference the given content anymore
874     * @param context the value's context
875     * @return <code>true</code> if the referenced content has been modified, <code>false</code> otherwise
876     */
877    public static boolean removeInvertRelation(ModifiableContent referencedContent, String invertRelationPath, Collection<String> referencingContentIds, ValueContext context)
878    {
879        ContentAttributeDefinition invertRelationDefinition = (ContentAttributeDefinition) referencedContent.getDefinition(invertRelationPath);
880        if (invertRelationDefinition.isMultiple())
881        {
882            if (DataHolderHelper.hasValue(referencedContent, invertRelationPath, context))
883            {
884                ContentValue[] oldValues = DataHolderHelper.getValue(referencedContent, invertRelationPath, context);
885                
886                String[] newValues = Arrays.stream(oldValues)
887                        .map(ContentValue::getContentId)
888                        .filter(id -> !referencingContentIds.contains(id))
889                        .toArray(size -> new String[size]);
890
891                DataHolderHelper.setValue(referencedContent, invertRelationPath, newValues, context);
892                return oldValues.length > newValues.length;
893            }
894            else
895            {
896                return false;
897            }
898        }
899        else
900        {
901            return _removeSingleInvertRelation(referencedContent, invertRelationPath, referencingContentIds, context);
902        }
903    }
904    
905    private static boolean _removeSingleInvertRelation(ModifiableModelAwareDataHolder dataHolder, String invertRelationPath, Collection<String> referencingContentIds, ValueContext context)
906    {
907        String[] pathSegments = StringUtils.split(invertRelationPath, ModelItem.ITEM_PATH_SEPARATOR);
908        String dataName = pathSegments[0];
909        
910        if (pathSegments.length == 1)
911        {
912            ContentValue value = DataHolderHelper.getValue(dataHolder, dataName, context);
913            if (value != null && referencingContentIds.contains(value.getContentId()))
914            {
915                DataHolderHelper.removeValue(dataHolder, dataName, context);
916                return true;
917            }
918            else
919            {
920                return false;
921            }
922        }
923        else
924        {
925            ModelItem modelItem = dataHolder.getDefinition(dataName);
926            String subInvertRelationPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
927            if (modelItem instanceof RepeaterDefinition)
928            {
929                boolean modified  = false;
930
931                ModifiableModelAwareRepeater repeater = dataHolder.getRepeater(dataName);
932                if (repeater != null)
933                {
934                    for (ModifiableModelAwareRepeaterEntry repeaterEntry : repeater.getEntries())
935                    {
936                        modified = _removeSingleInvertRelation(repeaterEntry, subInvertRelationPath, referencingContentIds, context) || modified; 
937                    }
938                }
939                
940                return modified;
941            }
942            else
943            {
944                ModifiableModelAwareComposite composite = dataHolder.getComposite(dataName);
945                return _removeSingleInvertRelation(composite, subInvertRelationPath, referencingContentIds, context);
946            }
947        }
948    }
949    
950    /**
951     * Invert relation manager (to add or remove a relation on a content
952     */
953    @FunctionalInterface
954    public interface InvertRelationManager
955    {
956        /**
957         * Manages the invert relation to the given {@link DataHolder}
958         * @param referencedContent the content where to manage the invert relation
959         * @param invertRelationPath the path of the invert relation
960         * @param referencingContentIds the id of the contents that reference.d the given content
961         * @param context the value's context
962         * @return <code>true</code> if the referenced content has been modified, <code>false</code> otherwise
963         */
964        public boolean manageInvertRelation(ModifiableContent referencedContent, String invertRelationPath, Collection<String> referencingContentIds, ValueContext context);
965    }
966}