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