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