001/*
002 *  Copyright 2016 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.content;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.Iterator;
024import java.util.LinkedHashMap;
025import java.util.List;
026import java.util.Locale;
027import java.util.Map;
028
029import javax.jcr.Node;
030import javax.jcr.NodeIterator;
031import javax.jcr.RepositoryException;
032
033import org.apache.avalon.framework.component.Component;
034import org.apache.avalon.framework.context.Context;
035import org.apache.avalon.framework.context.ContextException;
036import org.apache.avalon.framework.context.Contextualizable;
037import org.apache.avalon.framework.logger.AbstractLogEnabled;
038import org.apache.avalon.framework.service.ServiceException;
039import org.apache.avalon.framework.service.ServiceManager;
040import org.apache.avalon.framework.service.Serviceable;
041import org.apache.cocoon.components.ContextHelper;
042import org.apache.commons.collections.CollectionUtils;
043import org.apache.commons.lang3.ArrayUtils;
044import org.apache.commons.lang3.StringUtils;
045import org.apache.commons.lang3.tuple.ImmutablePair;
046import org.apache.commons.lang3.tuple.Pair;
047
048import org.ametys.cms.ObservationConstants;
049import org.ametys.cms.content.references.OutgoingReferencesHelper;
050import org.ametys.cms.contenttype.ContentConstants;
051import org.ametys.cms.contenttype.ContentType;
052import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
053import org.ametys.cms.contenttype.ContentTypesHelper;
054import org.ametys.cms.contenttype.MetadataDefinition;
055import org.ametys.cms.contenttype.MetadataManager;
056import org.ametys.cms.contenttype.MetadataType;
057import org.ametys.cms.contenttype.RepeaterDefinition;
058import org.ametys.cms.repository.Content;
059import org.ametys.cms.repository.DefaultContent;
060import org.ametys.cms.repository.ModifiableContent;
061import org.ametys.cms.repository.WorkflowAwareContent;
062import org.ametys.cms.search.model.SystemProperty;
063import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
064import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
065import org.ametys.core.observation.Event;
066import org.ametys.core.observation.ObservationManager;
067import org.ametys.core.ui.Callable;
068import org.ametys.core.user.CurrentUserProvider;
069import org.ametys.plugins.explorer.resources.Resource;
070import org.ametys.plugins.repository.AmetysObjectResolver;
071import org.ametys.plugins.repository.AmetysRepositoryException;
072import org.ametys.plugins.repository.RepositoryConstants;
073import org.ametys.plugins.repository.UnknownAmetysObjectException;
074import org.ametys.plugins.repository.jcr.JCRAmetysObject;
075import org.ametys.plugins.repository.metadata.CompositeMetadata;
076import org.ametys.plugins.repository.metadata.MultilingualString;
077import org.ametys.plugins.repository.metadata.MultilingualStringHelper;
078import org.ametys.plugins.repository.metadata.UnknownMetadataException;
079import org.ametys.plugins.workflow.AbstractWorkflowComponent;
080import org.ametys.plugins.workflow.support.WorkflowProvider;
081import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
082
083import com.opensymphony.workflow.WorkflowException;
084
085/**
086 * Helper for {@link Content}
087 *
088 */
089public class ContentHelper extends AbstractLogEnabled implements Component, Serviceable, Contextualizable
090{
091    /** The component role. */
092    public static final String ROLE = ContentHelper.class.getName();
093    
094    private AmetysObjectResolver _resolver;
095    private ContentTypesHelper _contentTypesHelper;
096    private ContentTypeExtensionPoint _contentTypeEP;
097    
098    private ObservationManager _observationManager;
099    private WorkflowProvider _workflowProvider;
100    private CurrentUserProvider _currentUserProvider;
101    private SystemPropertyExtensionPoint _systemPropertyExtensionPoint;
102    
103    private Context _context;
104
105    @Override
106    public void service(ServiceManager smanager) throws ServiceException
107    {
108        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
109        _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
110        _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
111        _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE);
112        _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE);
113        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
114        _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) smanager.lookup(SystemPropertyExtensionPoint.ROLE);
115    }
116    
117    @Override
118    public void contextualize(Context context) throws ContextException
119    {
120        _context = context;
121    }
122    
123    /**
124     * Add a content type to an existing content
125     * @param contentId The content id
126     * @param contentTypeId The content type to add
127     * @param actionId The workflow action id
128     * @return The result in a Map
129     * @throws WorkflowException if 
130     * @throws AmetysRepositoryException if an error occurred
131     */
132    @Callable
133    public Map<String, Object> addContentType (String contentId, String contentTypeId, int actionId) throws AmetysRepositoryException, WorkflowException
134    {
135        return _setContentType(contentId, contentTypeId, actionId, false);
136    }
137    
138    /**
139     * Remove a content type to an existing content
140     * @param contentId The content id
141     * @param contentTypeId The content type to add
142     * @param actionId The workflow action id
143     * @return The result in a Map
144     * @throws WorkflowException if 
145     * @throws AmetysRepositoryException if an error occurred
146     */
147    @Callable
148    public Map<String, Object> removeContentType (String contentId, String contentTypeId, int actionId) throws AmetysRepositoryException, WorkflowException
149    {
150        return _setContentType(contentId, contentTypeId, actionId, true);
151    }
152    
153    /**
154     * Add a mixin type to an existing content
155     * @param contentId The content id
156     * @param mixinId The mixin type to add
157     * @param actionId The workflow action id
158     * @return The result in a Map
159     * @throws WorkflowException if 
160     * @throws AmetysRepositoryException if an error occurred
161     */
162    @Callable
163    public Map<String, Object> addMixinType (String contentId, String mixinId, int actionId) throws AmetysRepositoryException, WorkflowException
164    {
165        return _setMixinType(contentId, mixinId, actionId, false);
166    }
167    
168    /**
169     * Remove a mixin type to an existing content
170     * @param contentId The content id
171     * @param mixinId The mixin type to add
172     * @param actionId The workflow action id
173     * @return The result in a Map
174     * @throws WorkflowException if 
175     * @throws AmetysRepositoryException if an error occurred
176     */
177    @Callable
178    public Map<String, Object> removeMixinType (String contentId, String mixinId, int actionId) throws AmetysRepositoryException, WorkflowException
179    {
180        return _setMixinType(contentId, mixinId, actionId, true);
181    }
182    
183    /**
184     * Get content edition information.
185     * @param contentId the content ID.
186     * @return a Map containing content edition information.
187     */
188    @Callable
189    public Map<String, Object> getContentEditionInformation(String contentId)
190    {
191        Map<String, Object> info = new HashMap<>();
192        
193        Content content = _resolver.resolveById(contentId);
194        
195        info.put("hasIndexingReferences", hasIndexingReferences(content));
196        
197        return info;
198    }
199    
200    /**
201     * Test if the given content has indexing references, i.e. if modifying it
202     * potentially implies reindexing other contents.
203     * @param content the content to test.
204     * @return <code>true</code> if one of the content types or mixins has indexing references, <code>false</code> otherwise.
205     */
206    public boolean hasIndexingReferences(Content content)
207    {
208        for (String cTypeId : content.getTypes())
209        {
210            if (_contentTypeEP.hasIndexingReferences(cTypeId))
211            {
212                return true;
213            }
214        }
215        
216        for (String mixinId : content.getMixinTypes())
217        {
218            if (_contentTypeEP.hasIndexingReferences(mixinId))
219            {
220                return true;
221            }
222        }
223        
224        return false;
225    }
226    
227    private Map<String, Object> _setContentType (String contentId, String contentTypeId, int actionId, boolean remove) throws AmetysRepositoryException, WorkflowException
228    {
229        Map<String, Object> result = new HashMap<>();
230        
231        Content content = _resolver.resolveById(contentId);
232
233        if (content instanceof ModifiableContent)
234        {
235            ModifiableContent modifiableContent = (ModifiableContent) content;
236            
237            List<String> currentTypes = new ArrayList<>(Arrays.asList(content.getTypes()));
238            
239            boolean hasChange = false;
240            if (remove)
241            {
242                if (currentTypes.size() > 1)
243                {
244                    hasChange = currentTypes.remove(contentTypeId);
245                }
246                else
247                {
248                    result.put("failure", true);
249                    result.put("msg", "empty-list");
250                }
251            }
252            else if (!currentTypes.contains(contentTypeId))
253            {
254                ContentType cType = _contentTypeEP.getExtension(contentTypeId);
255                if (cType.isMixin())
256                {
257                    result.put("failure", true);
258                    result.put("msg", "no-content-type");
259                    getLogger().error("Content type '" + contentTypeId + "' is a mixin type. It can not be added as content type.");
260                }
261                else if (!_contentTypesHelper.isCompatibleContentType(content, contentTypeId))
262                {
263                    result.put("failure", true);
264                    result.put("msg", "invalid-content-type");
265                    getLogger().error("Content type '" + contentTypeId + "' is incompatible with content '" + contentId + "'.");
266                }
267                else
268                {
269                    currentTypes.add(contentTypeId);
270                    hasChange = true;
271                }
272            }
273            
274            if (hasChange)
275            {
276                // TODO check if the content type is compatible
277                modifiableContent.setTypes(currentTypes.toArray(new String[currentTypes.size()]));
278                modifiableContent.saveChanges();
279                
280                if (content instanceof WorkflowAwareContent)
281                {
282                    
283                    WorkflowAwareContent waContent = (WorkflowAwareContent) content;
284                    AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
285                    
286                    Map<String, Object> inputs = new HashMap<>();
287                    inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content);
288                    inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>());
289                    inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>());
290                    
291                    workflow.doAction(waContent.getWorkflowId(), actionId, inputs);
292                }
293                
294                result.put("success", true);
295                
296                Map<String, Object> eventParams = new HashMap<>();
297                eventParams.put(ObservationConstants.ARGS_CONTENT, modifiableContent);
298                eventParams.put(ObservationConstants.ARGS_CONTENT_ID, contentId);
299                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams));
300            }
301        }
302        else
303        {
304            result.put("failure", true);
305            result.put("msg", "no-modifiable-content");
306            getLogger().error("Can not modified content types to a non-modifiable content '" + content.getId() + "'.");
307        }
308        
309        return result;
310    }
311    
312    private Map<String, Object> _setMixinType (String contentId, String mixinId, int actionId, boolean remove) throws AmetysRepositoryException, WorkflowException
313    {
314        Map<String, Object> result = new HashMap<>();
315        
316        Content content = _resolver.resolveById(contentId);
317
318        if (content instanceof ModifiableContent)
319        {
320            ModifiableContent modifiableContent = (ModifiableContent) content;
321            
322            List<String> currentMixins = new ArrayList<>(Arrays.asList(content.getMixinTypes()));
323            
324            boolean hasChange = false;
325            if (remove)
326            {
327                hasChange = currentMixins.remove(mixinId);
328            }
329            else if (!currentMixins.contains(mixinId))
330            {
331                ContentType cType = _contentTypeEP.getExtension(mixinId);
332                if (!cType.isMixin())
333                {
334                    result.put("failure", true);
335                    result.put("msg", "no-mixin");
336                    getLogger().error("The content type '" + mixinId + "' is not a mixin type, it can be not be added as a mixin.");
337                }
338                else if (!_contentTypesHelper.isCompatibleContentType(content, mixinId))
339                {
340                    result.put("failure", true);
341                    result.put("msg", "invalid-mixin");
342                    getLogger().error("Mixin '" + mixinId + "' is incompatible with content '" + contentId + "'.");
343                }
344                else
345                {
346                    currentMixins.add(mixinId);
347                    hasChange = true;
348                }
349            }
350            
351            if (hasChange)
352            {
353                // TODO check if the content type is compatible
354                modifiableContent.setMixinTypes(currentMixins.toArray(new String[currentMixins.size()]));
355                modifiableContent.saveChanges();
356                
357                if (content instanceof WorkflowAwareContent)
358                {
359                    WorkflowAwareContent waContent = (WorkflowAwareContent) content;
360                    AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
361                    
362                    Map<String, Object> inputs = new HashMap<>();
363                    inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content);
364                    inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>());
365                    inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>());
366                    
367                    workflow.doAction(waContent.getWorkflowId(), actionId, inputs);
368                }
369                
370                result.put("success", true);
371                
372                Map<String, Object> eventParams = new HashMap<>();
373                eventParams.put(ObservationConstants.ARGS_CONTENT, modifiableContent);
374                eventParams.put(ObservationConstants.ARGS_CONTENT_ID, contentId);
375                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams));
376            }
377        }
378        else
379        {
380            result.put("failure", true);
381            result.put("msg", "no-modifiable-content");
382            getLogger().error("Can not modified mixins to a non-modifiable content '" + content.getId() + "'.");
383        }
384        
385        return result;
386    }
387    
388    /**
389     * Determines if the content is a reference table content type
390     * @param content The content
391     * @return true if content is a reference table
392     */
393    public boolean isReferenceTable(Content content)
394    {
395        for (String cTypeId : content.getTypes())
396        {
397            ContentType cType = _contentTypeEP.getExtension(cTypeId);
398            if (cType != null)
399            {
400                if (!cType.isReferenceTable())
401                {
402                    return false;
403                }
404            }
405            else
406            {
407                if (getLogger().isWarnEnabled())
408                {
409                    getLogger().warn(String.format("Unable to determine if a content is a reference table, unknown content type : '%s'.", cTypeId));
410                }
411            }
412        }
413        return true;
414    }
415    
416    /**
417     * Determines if a content is a multilingual content
418     * @param content The content
419     * @return <code>true</code> if the content is an instance of content type
420     */
421    public boolean isMultilingual(Content content)
422    {
423        for (String cTypeId : content.getTypes())
424        {
425            ContentType cType = _contentTypeEP.getExtension(cTypeId);
426            if (cType != null && cType.isMultilingual())
427            {
428                return true;
429            }
430        }
431        return false;
432    }
433    
434    /**
435     * Determines if the content is a simple content type
436     * @param content The content
437     * @return true if content is simple
438     */
439    public boolean isSimple (Content content)
440    {
441        for (String cTypeId : content.getTypes())
442        {
443            ContentType cType = _contentTypeEP.getExtension(cTypeId);
444            if (cType != null)
445            {
446                if (!cType.isSimple())
447                {
448                    return false;
449                }
450            }
451            else
452            {
453                if (getLogger().isWarnEnabled())
454                {
455                    getLogger().warn(String.format("Unable to determine if a content is simple, unknown content type : '%s'.", cTypeId));
456                }
457            }
458        }
459        return true;
460    }
461    
462    /**
463     * Get the typed value(s) of a content at given path.
464     * The path can represent a system property id or a path of a metadata into the content or a metadata on one or more linked contents, such as 'composite/linkedContent/secondContent/composite/metadata'.
465     * The returned value is typed.
466     * @param content The content
467     * @param fieldPath The field id or the path to the metadata, separated by '/'
468     * @param defaultLocale The default locale to resolve localized values if the content's language is null. Can be null.
469     * @param resolveReferences <code>true</code> true to resolve references (such as resource or content)
470     * @return The typed final value. If the final field is multiple, or contains into a repeater or multiple 'CONTENT' metadata, the returned value will be a Collection
471     */
472    public Object getValue(Content content, String fieldPath, Locale defaultLocale, boolean resolveReferences)
473    {
474        return getValue(content, fieldPath, defaultLocale, resolveReferences, false);
475    }
476    
477    /**
478     * Get the typed value(s) of a content at given path.
479     * The path can represent a system property id or a path of a metadata into the content or a metadata on one or more linked contents, such as 'composite/linkedContent/secondContent/composite/metadata'.
480     * The returned value is typed.
481     * @param content The content
482     * @param fieldPath The field id or the path to the metadata, separated by '/'
483     * @param defaultLocale The default locale to resolve localized values if the content's language is null. Can be null.
484     * @param resolveReferences <code>true</code> true to resolve references (such as resource or content)
485     * @param returnNullValues <code>true</code> true to return null values when the metadata does not exists in a repeater or linked content.
486     * @return The typed final value. If the final field is multiple, or contains into a repeater or multiple 'CONTENT' metadata, the returned value will be a Collection
487     */
488    public Object getValue(Content content, String fieldPath, Locale defaultLocale, boolean resolveReferences, boolean returnNullValues)
489    {
490        if (StringUtils.isAnyEmpty(fieldPath))
491        {
492            return null;
493        }
494        
495        // Manage System Properties
496        String[] pathSegments = fieldPath.split(ContentConstants.METADATA_PATH_SEPARATOR);
497        String propertyName = pathSegments[pathSegments.length - 1];
498        
499        if (_systemPropertyExtensionPoint.hasExtension(propertyName))
500        {
501            if (_systemPropertyExtensionPoint.isDisplayable(propertyName))
502            {
503                SystemProperty systemProperty = _systemPropertyExtensionPoint.getExtension(propertyName);
504                return _getSystemPropertyValue(content, pathSegments, systemProperty);
505            }
506            else
507            {
508                throw new IllegalArgumentException("The system property '" + propertyName + "' is not displayable.");
509            }
510        }
511        else
512        {
513            return getMetadataValue(content, fieldPath, defaultLocale, resolveReferences, returnNullValues);
514        }
515    }
516
517    private Object _getSystemPropertyValue(Content content, String[] pathSegments, SystemProperty systemProperty)
518    {
519        String contentFieldPath = StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 0, pathSegments.length - 1);
520        List<Content> contentsContainingSystemProperty = getTargetContents(content, contentFieldPath);
521        if (contentsContainingSystemProperty.size() == 1)
522        {
523            Object value = systemProperty.getValue(contentsContainingSystemProperty.get(0));
524            
525            if (value instanceof Object[])
526            {
527                return Arrays.asList((Object[]) value);
528            }
529            else
530            {
531                return value;
532            }
533        }
534        else
535        {
536            List<Object> values = new ArrayList<>();
537            for (Content contentContainingSystemProperty : contentsContainingSystemProperty)
538            {
539                Object value = systemProperty.getValue(contentContainingSystemProperty);
540                
541                if (value instanceof Object[])
542                {
543                    values.addAll(Arrays.asList((Object[]) value));
544                }
545                else
546                {
547                    values.add(value);
548                }
549            }
550            return values;
551        }
552    }
553    
554    /**
555     * Get the content from which to get the system property.
556     * @param sourceContent The source content.
557     * @param fieldPath The field path
558     * @return The target content.
559     */
560    public Content getTargetContent(Content sourceContent, String fieldPath)
561    {
562        if (StringUtils.isBlank(fieldPath))
563        {
564            return sourceContent;
565        }
566        else
567        {
568            Object value = getMetadataValue(sourceContent, fieldPath, null, true);
569            if (value != null && value instanceof Content)
570            {
571                return (Content) value;
572            }
573            else if (value != null && value instanceof Collection<?> && ((Collection) value).size() > 0)
574            {
575                Object first = ((Collection) value).iterator().next();
576                if (first instanceof Content)
577                {
578                    return (Content) first;
579                }
580            }
581        }
582        
583        return null;
584    }
585    
586    /**
587     * Get the contents from which to get the system property.
588     * @param sourceContent The source content.
589     * @param fieldPath The field path
590     * @return The target contents.
591     */
592    public List<Content> getTargetContents(Content sourceContent, String fieldPath)
593    {
594        List<Content> targetContents = new ArrayList<>();
595        
596        if (StringUtils.isBlank(fieldPath))
597        {
598            targetContents.add(sourceContent);
599        }
600        else
601        {
602            Object value = getMetadataValue(sourceContent, fieldPath, null, true);
603            if (value != null && value instanceof Content)
604            {
605                targetContents.add((Content) value);
606            }
607            else if (value != null && value instanceof Collection<?>)
608            {
609                Iterator it = ((Collection) value).iterator();
610                while (it.hasNext())
611                {
612                    Object object = it.next();
613                    if (object instanceof Content)
614                    {
615                        targetContents.add((Content) object);
616                    }
617                    
618                }
619            }
620        }
621        
622        return targetContents;
623    }
624
625    /**
626     * Get the typed metadata value(s) of a content at given path.
627     * The path can represent a path of a metadata into the content or a metadata on one or more linked contents, such as 'composite/linkedContent/secondContent/composite/metadata'.
628     * The returned value is typed.
629     * @param content The content
630     * @param metadataPath The path to the metadata, separated by '/'
631     * @param defaultLocale The locale to use to sax localized values such as multilingual content or multilingual string. 
632     * Only to be valued if initial content's language is null, otherwise set this parameter to null.
633     * @param resolveReferences <code>true</code> true to resolve references (such as resource or content)
634     * @return The typed final value. If the final metadata is multiple, or contains into a repeater or multiple 'CONTENT' metadata, the returned value will be a Collection
635     */
636    public Object getMetadataValue(Content content, String metadataPath, Locale defaultLocale, boolean resolveReferences)
637    {
638        return getMetadataValue(content, metadataPath, defaultLocale, resolveReferences, false);
639    }
640    
641    /**
642     * Get the typed metadata value(s) of a content at given path.
643     * The path can represent a path of a metadata into the content or a metadata on one or more linked contents, such as 'composite/linkedContent/secondContent/composite/metadata'.
644     * The returned value is typed.
645     * @param content The content
646     * @param metadataPath The path to the metadata, separated by '/'
647     * @param defaultLocale The locale to use to sax localized values such as multilingual content or multilingual string. 
648     * Only to be valued if initial content's language is null, otherwise set this parameter to null.
649     * @param resolveReferences <code>true</code> true to resolve references (such as resource or content)
650     * @param returnNullValues <code>true</code> true to return null values when the metadata does not exists in a repeater or linked content.
651     * @return The typed final value. If the final metadata is multiple, or contains into a repeater or multiple 'CONTENT' metadata, the returned value will be a Collection
652     */
653    public Object getMetadataValue(Content content, String metadataPath, Locale defaultLocale, boolean resolveReferences, boolean returnNullValues)
654    {
655        String[] pathSegments = metadataPath.split(ContentConstants.METADATA_PATH_SEPARATOR);
656        
657        MetadataDefinition definition = _contentTypesHelper.getMetadataDefinition(pathSegments[0], content);
658        if (definition != null)
659        {
660            Locale contentLocale = content.getLanguage() != null ? new Locale(content.getLanguage()) : defaultLocale;
661            return getMetadataValue(content.getMetadataHolder(), definition, metadataPath, contentLocale, resolveReferences, returnNullValues);
662        }
663        
664        getLogger().warn("Unknown metadata definition at path '" + metadataPath + "' for content '" + content.getId());
665        return null;
666    }
667    
668    /**
669     * Get the typed values of a content at given path.
670     * The value is always returned into a collection of object event if the metadata is a single metadata.
671     * The path can represent a path of a metadata into the content or a metadata on one or more linked contents, such as 'composite/linkedContent/secondContent/composite/metadata'.
672     * The returned value is typed.
673     * @param contentId The ID of the content
674     * @param metadataPath The Path to the metadata, separated by '/'
675     * @return The typed final value. If the final metadata is single, the returned value will be a Collection of one element
676     */
677    @Callable
678    public List<Object> getMetadataValues(String contentId, String metadataPath)
679    {
680        return getMetadataValues(_resolver.resolveById(contentId), metadataPath, null, false, true);
681    }
682    
683    /**
684     * Get the typed values of a content at given path.
685     * The value is always returned into a collection of object event if the metadata is a single metadata.
686     * The path can represent a path of a metadata into the content or a metadata on one or more linked contents, such as 'composite/linkedContent/secondContent/composite/metadata'.
687     * The returned value is typed.
688     * @param content The content
689     * @param metadataPath The path to the metadata, separated by '/'
690     * @param defaultLocale The locale to use to sax localized values such as multilingual content or multilingual string. 
691     * Only to be valued if initial content's language is null, otherwise set this parameter to null.
692     * @param resolveReferences <code>true</code> true to resolve references (such as resource or content)
693     * @param returnNullValues <code>true</code> true to return null values when the metadata does not exists in a repeater or linked content.
694     * @return The typed final value. If the final metadata is single, the returned value will be a Collection of one element
695     */
696    public List<Object> getMetadataValues(Content content, String metadataPath, Locale defaultLocale, boolean resolveReferences, boolean returnNullValues)
697    {
698        String[] pathSegments = metadataPath.split(ContentConstants.METADATA_PATH_SEPARATOR);
699        
700        MetadataDefinition definition = _contentTypesHelper.getMetadataDefinition(pathSegments[0], content);
701        if (definition != null)
702        {
703            Locale locale = content.getLanguage() != null ? new Locale(content.getLanguage()) : defaultLocale;
704            Object values = getMetadataValue(content.getMetadataHolder(), definition, metadataPath, locale, resolveReferences, returnNullValues);
705            if (values instanceof Collection<?>)
706            {
707                return new ArrayList<>((Collection<?>) values); 
708            }
709            else if (values != null || returnNullValues)
710            {
711                return Arrays.asList(values);
712            }
713            else
714            {
715                return Collections.EMPTY_LIST;
716            }
717        }
718        
719        getLogger().warn("Unknown metadata definition at path '" + metadataPath + "' for content '" + content.getId());
720        
721        return null;
722    }
723    
724    /**
725     * Get the title of a content.&lt;br&gt;
726     * If the content is a multilingual content, the title will be retrieved for the current locale if exists, or for default locale 'en' if exists, or for the first found locale.
727     * @param content The content
728     * @return The title of the content
729     */
730    public String getTitle(Content content)
731    {
732        Locale defaultLocale = null;
733        
734        try
735        {
736            Map objectModel = (Map) _context.get(ContextHelper.CONTEXT_OBJECT_MODEL);
737            if (objectModel != null)
738            {
739                // The object model can be null if #getTitle(content) is called outside a request
740                defaultLocale = org.apache.cocoon.i18n.I18nUtils.findLocale(objectModel, "locale", null, Locale.getDefault(), true);
741            }
742        }
743        catch (ContextException e)
744        {
745            // There is no context
746        }
747        
748        // TODO Use user preference language ?
749        return content.getTitle(defaultLocale);
750    }
751    
752    /**
753     * Get the title variants of a multilingual content
754     * @param content The multilingual content
755     * @return the content's title for each locale
756     * @throws IllegalArgumentException if the content is not a multilingual content
757     */
758    public Map<String, String> getTitleVariants(Content content)
759    {
760        if (!isMultilingual(content))
761        {
762            throw new IllegalArgumentException("Can not get title variants for a non-multilingual content " + content.getId());
763        }
764        
765        Map<String, String> variants = new HashMap<>();
766        
767        MultilingualString value = content.getMetadataHolder().getMultilingualString(DefaultContent.METADATA_TITLE);
768        for (Locale locale : value.getLocales())
769        {
770            variants.put(locale.getLanguage(), value.getValue(locale));
771        }
772        
773        return variants;
774    }
775    
776    /**
777     * Get the typed value(s) at given path.
778     * The path can represent a path of a metadata in the parent composite metadata or a path of a metadata into a linked content.
779     * The returned value is typed.
780     * @param metadataHolder The parent composite metadata
781     * @param definition The definition of the first metadata in path
782     * @param metadataPath The path to the metadata, separated by '/'
783     * @param locale The locale to used to resolve localized metadata
784     * @param resolveReferences <code>true</code> true to resolve references (such as resource or content)
785     * @param returnNullValues <code>true</code> true to return null values when metadata does not exists.
786     * @return The typed final value. If the final metadata is multiple, or contains into a repeater or multiple 'CONTENT' metadata, the returned value will be a Collection
787     */
788    public Object getMetadataValue(CompositeMetadata metadataHolder, MetadataDefinition definition, String metadataPath, Locale locale, boolean resolveReferences, boolean returnNullValues)
789    {
790        String[] pathSegments = metadataPath.split(ContentConstants.METADATA_PATH_SEPARATOR);
791        
792        String metadataName = pathSegments[0];
793        
794        if (!metadataHolder.hasMetadata(metadataName))
795        {
796            return null;
797        }
798        
799        MetadataType type = definition.getType();
800        switch (type)
801        {
802            case COMPOSITE:
803                return _getCompositeMetadataValue(metadataHolder, definition, locale, resolveReferences, returnNullValues, pathSegments, metadataName);
804            
805            case CONTENT:
806                return _getContentMetadataValue(metadataHolder, definition, locale, resolveReferences, returnNullValues, pathSegments, metadataName);
807            default:
808                if (pathSegments.length == 1)
809                {
810                    return getSimpleMetadataValue(metadataHolder, definition, metadataName, locale, resolveReferences);
811                }
812                
813                throw new IllegalArgumentException("Metadata at path '" + definition.getId() + "' is a simple metadata : can not invoked get sub metadata values at path " + StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length));
814        }
815    }
816
817    private Object _getContentMetadataValue(CompositeMetadata metadataHolder, MetadataDefinition definition, Locale defaultLocale, boolean resolveReferences, boolean returnNullValues,
818            String[] pathSegments, String metadataName)
819    {
820        if (pathSegments.length > 1)
821        {
822            if (definition.isMultiple())
823            {
824                List<Object> values = new ArrayList<>();
825                String[] refContentIds = metadataHolder.getStringArray(metadataName, new String[0]);
826                for (String refContentId : refContentIds)
827                {
828                    Content refContent = _resolver.resolveById(refContentId);
829                    Locale locale = refContent.getLanguage() != null ? new Locale(refContent.getLanguage()) : defaultLocale;
830                    Object remoteValue = getMetadataValue(refContent, StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), locale, resolveReferences, returnNullValues);
831                    if (remoteValue != null && remoteValue instanceof Collection<?>)
832                    {
833                        values.addAll((Collection<?>) remoteValue);
834                    }
835                    else if (remoteValue != null || returnNullValues)
836                    {
837                        values.add(remoteValue);
838                    }
839                }
840                return values;
841            }
842            else
843            {
844                String refContentId = metadataHolder.getString(metadataName);
845                Content refContent = _resolver.resolveById(refContentId);
846                Locale locale = refContent.getLanguage() != null ? new Locale(refContent.getLanguage()) : defaultLocale;
847                return getMetadataValue(refContent, StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), locale, resolveReferences, returnNullValues);
848            }   
849        }
850        else
851        {
852            return getSimpleMetadataValue(metadataHolder, definition, metadataName, defaultLocale, resolveReferences);
853        }
854    }
855
856    private Object _getCompositeMetadataValue(CompositeMetadata metadataHolder, MetadataDefinition definition, Locale locale, boolean resolveReferences, boolean returnNullValues, String[] pathSegments, String metadataName)
857    {
858        if (pathSegments.length > 1)
859        {
860            if (definition instanceof RepeaterDefinition)
861            {
862                // Repeater: get and sort the entry names.
863                CompositeMetadata repeater = metadataHolder.getCompositeMetadata(metadataName);
864                String[] entries = repeater.getMetadataNames();
865                Arrays.sort(entries, MetadataManager.REPEATER_ENTRY_COMPARATOR);
866                
867                List<Object> values = new ArrayList<>();
868                
869                for (String entryName : entries)
870                {
871                    CompositeMetadata entry = repeater.getCompositeMetadata(entryName);
872                    
873                    Object entryValue = getMetadataValue(entry, definition.getMetadataDefinition(pathSegments[1]), StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), locale, resolveReferences, returnNullValues);
874                    if (entryValue != null && entryValue instanceof Collection<?>)
875                    {
876                        values.addAll((Collection<?>) entryValue);
877                    }
878                    else if (entryValue != null || returnNullValues)
879                    {
880                        values.add(entryValue);
881                    }
882                }
883                
884                return values;
885            }
886            else
887            {
888                // Composite.
889                CompositeMetadata subMetadataHolder = metadataHolder.getCompositeMetadata(metadataName);
890                MetadataDefinition subMetadataDef = definition.getMetadataDefinition(pathSegments[1]);
891                return getMetadataValue(subMetadataHolder, subMetadataDef, StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), locale, resolveReferences, returnNullValues);
892            }
893        }
894                        
895        throw new IllegalArgumentException("Metadata at path '" + definition.getId() + "' is a composite metadata : can not invoked #getMetadataValue");
896    }
897    
898    /**
899     * Get the typed value(s) of a simple metadata.
900     * @param metadataHolder The parent composite metadata
901     * @param definition The definition of the first metadata in path
902     * @param metadataName The name of the metadata
903     * @param locale The locale to used to resolve localized metadata
904     * @param resolveReferences <code>true</code> true to resolve references (such as resource or content)
905     * @return The typed final value.
906     */
907    public Object getSimpleMetadataValue(CompositeMetadata metadataHolder, MetadataDefinition definition, String metadataName, Locale locale, boolean resolveReferences)
908    {
909        if (metadataName.contains(ContentConstants.METADATA_PATH_SEPARATOR))
910        {
911            throw new IllegalArgumentException("The metadata name cannot represent a path : " + metadataName);
912        }
913        
914        Object value = null;
915        
916        switch (definition.getType())
917        {
918            case LONG:
919                value = _getLongValue(metadataHolder, metadataName, definition);
920                break;
921            case DOUBLE:
922                value = _getDoubleValue(metadataHolder, metadataName, definition);
923                break;
924            case BOOLEAN:
925                value = _getBooleanValue(metadataHolder, metadataName, definition);
926                break;
927            case DATE:
928            case DATETIME:
929                value = _getDateValue(metadataHolder, metadataName, definition);
930                break;
931            case USER:
932                value = _getUserValue(metadataHolder, metadataName, definition);
933                break;
934            case BINARY:
935                value = _getBinaryValue(metadataHolder, metadataName);
936                break;
937            case FILE:
938                value = _getFileValue(metadataHolder, metadataName, definition, resolveReferences);
939                break;
940            case GEOCODE:
941                value = _getGeocodeValue(metadataHolder, metadataName);
942                break;
943            case RICH_TEXT:
944                value = _getRichTextValue(metadataHolder, metadataName);
945                break;
946            case CONTENT:
947                value = _getContentValue(metadataHolder, metadataName, definition, resolveReferences);
948                break;
949            case SUB_CONTENT:
950                // TODO or ignore ?
951                break;
952            case REFERENCE:
953                value = _getReferenceValue(metadataHolder, metadataName, definition);
954                break;
955            case MULTILINGUAL_STRING:
956                value = _getMultilingualStringValue(metadataHolder, metadataName, locale, resolveReferences);
957                break;
958            case STRING:
959            default:
960                value = _getStringValue(metadataHolder, metadataName, definition);
961        }
962        
963        return value;
964    }
965    
966    private Object _getStringValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition)
967    {
968        try
969        {
970            if (definition.isMultiple())
971            {
972                return Arrays.asList(metadataHolder.getStringArray(metadataName));
973            }
974            else
975            {
976                return metadataHolder.getString(metadataName);
977            }
978        }
979        catch (UnknownMetadataException e)
980        {
981            // Ignore, just return null.
982            return null;
983        }
984    }
985    
986    private Object _getMultilingualStringValue(CompositeMetadata metadataHolder, String metadataName, Locale locale, boolean resolve)
987    {
988        if (resolve)
989        {
990            return metadataHolder.getMultilingualString(metadataName);
991        }
992        else
993        {
994            return MultilingualStringHelper.getValue(metadataHolder, metadataName, locale);
995        }
996    }
997    
998    private Object _getContentValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition, boolean resolve)
999    {
1000        try
1001        {
1002            if (definition.isMultiple())
1003            {
1004                String[] refContentIds = metadataHolder.getStringArray(metadataName);
1005                if (resolve)
1006                {
1007                    List<Content> contents = new ArrayList<>();
1008                    for (String refContentId : refContentIds)
1009                    {
1010                        try
1011                        {
1012                            contents.add(_resolver.resolveById(refContentId));
1013                        }
1014                        catch (UnknownAmetysObjectException e)
1015                        {
1016                            // Ignore
1017                            if (getLogger().isWarnEnabled())
1018                            {
1019                                getLogger().warn("Metadata '" + definition.getId() + "' refers a non-existing content of id '" + refContentId + "'", e);
1020                            }
1021                        }
1022                    }
1023                    return contents;
1024                }
1025                else
1026                {
1027                    return Arrays.asList(refContentIds);
1028                }
1029            }
1030            else
1031            {
1032                if (resolve)
1033                {
1034                    try
1035                    {
1036                        return _resolver.resolveById(metadataHolder.getString(metadataName));
1037                    }
1038                    catch (UnknownAmetysObjectException e)
1039                    {
1040                        return null;
1041                    }
1042                }
1043                else
1044                {
1045                    return metadataHolder.getString(metadataName);
1046                }
1047            }
1048        }
1049        catch (UnknownMetadataException e)
1050        {
1051            // Ignore, just return null.
1052            return null;
1053        }
1054    }
1055    
1056    private Object _getUserValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition)
1057    {
1058        try
1059        {
1060            if (definition.isMultiple())
1061            {
1062                return Arrays.asList(metadataHolder.getUserArray(metadataName));
1063            }
1064            else
1065            {
1066                return metadataHolder.getUser(metadataName);
1067            }
1068        }
1069        catch (UnknownMetadataException e)
1070        {
1071            // Ignore, just return null.
1072            return null;
1073        }
1074    }
1075    
1076    private Object _getDateValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition)
1077    {
1078        try
1079        {
1080            if (definition.isMultiple())
1081            {
1082                return Arrays.asList(metadataHolder.getDateArray(metadataName));
1083            }
1084            else
1085            {
1086                return metadataHolder.getDate(metadataName);
1087            }
1088        }
1089        catch (UnknownMetadataException e)
1090        {
1091            // Ignore, just return null.
1092            return null;
1093        }
1094    }
1095    
1096    private Object _getLongValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition)
1097    {
1098        try
1099        {
1100            if (definition.isMultiple())
1101            {
1102                return Arrays.asList(ArrayUtils.toObject(metadataHolder.getLongArray(metadataName)));
1103            }
1104            else
1105            {
1106                return metadataHolder.getLong(metadataName);
1107            }
1108        }
1109        catch (UnknownMetadataException e)
1110        {
1111            // Ignore, just return null.
1112            return null;
1113        }
1114    }
1115    
1116    private Object _getDoubleValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition)
1117    {
1118        try
1119        {
1120            if (definition.isMultiple())
1121            {
1122                return Arrays.asList(ArrayUtils.toObject(metadataHolder.getDoubleArray(metadataName)));
1123            }
1124            else
1125            {
1126                return metadataHolder.getDouble(metadataName);
1127            }
1128        }
1129        catch (UnknownMetadataException e)
1130        {
1131            // Ignore, just return null.
1132            return null;
1133        }
1134    }
1135    
1136    private Object _getBooleanValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition)
1137    {
1138        try
1139        {
1140            if (definition.isMultiple())
1141            {
1142                return Arrays.asList(ArrayUtils.toObject(metadataHolder.getBooleanArray(metadataName)));
1143            }
1144            else
1145            {
1146                return metadataHolder.getBoolean(metadataName);
1147            }
1148        }
1149        catch (UnknownMetadataException e)
1150        {
1151            // Ignore, just return null.
1152            return null;
1153        }
1154    }
1155    
1156    private Object _getRichTextValue(CompositeMetadata metadataHolder, String metadataName)
1157    {
1158        try
1159        {
1160            return metadataHolder.getRichText(metadataName);
1161        }
1162        catch (UnknownMetadataException e)
1163        {
1164            // Ignore, just return null.
1165            return null;
1166        }
1167    }
1168    
1169    private Object _getBinaryValue(CompositeMetadata metadataHolder, String metadataName)
1170    {
1171        try
1172        {
1173            return metadataHolder.getBinaryMetadata(metadataName);
1174        }
1175        catch (UnknownMetadataException e)
1176        {
1177            // Ignore, just return null.
1178            return null;
1179        }
1180    }
1181    
1182    private Object _getResourceValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition, boolean resolve)
1183    {
1184        try
1185        {
1186            if (definition.isMultiple())
1187            {
1188                String[] resourceIds = metadataHolder.getStringArray(metadataName);
1189                if (resolve)
1190                {
1191                    List<Resource> resources = new ArrayList<>();
1192                    for (String resourceId : resourceIds)
1193                    {
1194                        try
1195                        {
1196                            resources.add(_resolver.resolveById(resourceId));
1197                        }
1198                        catch (UnknownAmetysObjectException e)
1199                        {
1200                            // Ignore
1201                        }
1202                    }
1203                    return resources;
1204                }
1205                else
1206                {
1207                    return Arrays.asList(resourceIds);
1208                }
1209            }
1210            else
1211            {
1212                if (resolve)
1213                {
1214                    try
1215                    {
1216                        return _resolver.resolveById(metadataHolder.getString(metadataName));
1217                    }
1218                    catch (UnknownAmetysObjectException e)
1219                    {
1220                        return null;
1221                    }
1222                }
1223                else
1224                {
1225                    return metadataHolder.getString(metadataName);
1226                }
1227            }
1228        }
1229        catch (UnknownMetadataException e)
1230        {
1231            // Ignore, just return null.
1232            return null;
1233        }
1234    }
1235    
1236    private Object _getGeocodeValue(CompositeMetadata metadataHolder, String metadataName)
1237    {
1238        try
1239        {
1240            // FIXME should return a GeoCode object
1241            CompositeMetadata geoMetadata = metadataHolder.getCompositeMetadata(metadataName);
1242            
1243            if (geoMetadata.hasMetadata("longitude") && geoMetadata.hasMetadata("latitude")) 
1244            {
1245                Double longitude = geoMetadata.getDouble("longitude");
1246                Double latitude = geoMetadata.getDouble("latitude");
1247                
1248                Map<String, Double> geocode = new LinkedHashMap<>();
1249                geocode.put("longitude", longitude);
1250                geocode.put("latitude", latitude);
1251                
1252                return geocode;
1253            }
1254        }
1255        catch (UnknownMetadataException e)
1256        {
1257            // Ignore, just return null.
1258        }
1259        
1260        return null;
1261    }
1262    
1263    private Object _getReferenceValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition)
1264    {
1265        try
1266        {
1267            // FIXME should return a Reference object
1268            
1269            CompositeMetadata referencesComposite = metadataHolder.getCompositeMetadata(metadataName);
1270            
1271            if (definition.isMultiple())
1272            {
1273                List<Map<String, Object>> references = new ArrayList<>();
1274                
1275                String[] types = referencesComposite.getStringArray("types");
1276                String[] values = referencesComposite.getStringArray("values");
1277                
1278                for (int i = 0; i < types.length; i++)
1279                {
1280                    Map<String, Object> reference = new HashMap<>(2);
1281                    reference.put("type", types[i]);
1282                    reference.put("value", values[i]);
1283                    
1284                    references.add(reference);
1285                }
1286                
1287                return references;
1288            }
1289            else
1290            {
1291                String type = referencesComposite.getString("type");
1292                String value = referencesComposite.getString("value");
1293                
1294                Map<String, Object> reference = new HashMap<>(2);
1295                reference.put("type", type);
1296                reference.put("value", value);
1297                
1298                return reference;
1299            }
1300        }
1301        catch (UnknownMetadataException e)
1302        {
1303            // Ignore, just return null.
1304            return null;
1305        }
1306    }
1307    
1308    private Object _getFileValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition, boolean resolveReference)
1309    {
1310        if (org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType.BINARY.equals(metadataHolder.getType(metadataName)))
1311        {
1312            return _getBinaryValue(metadataHolder, metadataName);
1313        }
1314        else
1315        {
1316            return _getResourceValue(metadataHolder, metadataName, definition, resolveReference);
1317        }
1318    }
1319    
1320    /**
1321     * Determines if the content has referencing contents other than whose type is in content types to ignore.
1322     * @param content The content to check
1323     * @param ignoreContentTypes The content types to ignore for referencing contents
1324     * @param includeSubTypes True if sub content types are take into account in ignore content types
1325     * @return <code>true</code> if there is at least one Content referencing the content
1326     */
1327    public boolean hasReferencingContents(Content content, List<String> ignoreContentTypes, boolean includeSubTypes)
1328    {
1329        List<String> newIgnoreContentTypes = new ArrayList<>();
1330        newIgnoreContentTypes.addAll(ignoreContentTypes);
1331        if (includeSubTypes)
1332        {
1333            for (String contentType : ignoreContentTypes)
1334            {
1335                newIgnoreContentTypes.addAll(_contentTypeEP.getSubTypes(contentType));
1336            }
1337        }
1338        
1339        for (Content refContent : content.getReferencingContents())
1340        {
1341            List<String> contentTypes = Arrays.asList(refContent.getTypes());
1342            if (!CollectionUtils.containsAny(contentTypes, newIgnoreContentTypes))
1343            {
1344                return true;
1345            }
1346        }
1347        
1348        return false;
1349    }
1350    
1351    /**
1352     * Returns all Contents referencing the given content with their value path
1353     * @param content The content to get references
1354     * @return the list of pair path / contents
1355     */
1356    public List<Pair<String, Content>> getReferencingContents(Content content)
1357    {
1358        List<Pair<String, Content>> incomingReferences = new ArrayList<>();
1359        try
1360        {
1361            NodeIterator results = OutgoingReferencesHelper.getContentOutgoingReferences((JCRAmetysObject) content);
1362            while (results.hasNext())
1363            {
1364                Node node = results.nextNode();
1365                
1366                Node outgoingRefsNode = node.getParent(); // go up towards node 'ametys-internal:outgoing-references;
1367                String path = outgoingRefsNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_OUTGOING_REFERENCES_PATH_PROPERTY).getString();
1368                
1369                Node contentNode = outgoingRefsNode.getParent()  // go up towards node 'ametys-internal:root-outgoing-references
1370                                                   .getParent(); // go up towards node of the content
1371                Content refContent = _resolver.resolve(contentNode, false);
1372 
1373                incomingReferences.add(new ImmutablePair<>(path, refContent));
1374            }
1375        }
1376        catch (RepositoryException e)
1377        {
1378            throw new AmetysRepositoryException("Unable to resolve references for content " + content.getId(), e);
1379        }
1380        
1381        return incomingReferences;
1382    }
1383}