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