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