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.HashMap;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.logger.AbstractLogEnabled;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.commons.lang3.ArrayUtils;
032import org.apache.commons.lang3.StringUtils;
033
034import org.ametys.cms.ObservationConstants;
035import org.ametys.cms.contenttype.ContentConstants;
036import org.ametys.cms.contenttype.ContentType;
037import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
038import org.ametys.cms.contenttype.ContentTypesHelper;
039import org.ametys.cms.contenttype.MetadataDefinition;
040import org.ametys.cms.contenttype.MetadataManager;
041import org.ametys.cms.contenttype.MetadataType;
042import org.ametys.cms.contenttype.RepeaterDefinition;
043import org.ametys.cms.repository.Content;
044import org.ametys.cms.repository.ModifiableContent;
045import org.ametys.cms.repository.WorkflowAwareContent;
046import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
047import org.ametys.core.observation.Event;
048import org.ametys.core.observation.ObservationManager;
049import org.ametys.core.ui.Callable;
050import org.ametys.core.user.CurrentUserProvider;
051import org.ametys.core.user.UserIdentity;
052import org.ametys.plugins.core.user.UserHelper;
053import org.ametys.plugins.explorer.resources.Resource;
054import org.ametys.plugins.repository.AmetysObjectIterable;
055import org.ametys.plugins.repository.AmetysObjectResolver;
056import org.ametys.plugins.repository.AmetysRepositoryException;
057import org.ametys.plugins.repository.TraversableAmetysObject;
058import org.ametys.plugins.repository.UnknownAmetysObjectException;
059import org.ametys.plugins.repository.metadata.BinaryMetadata;
060import org.ametys.plugins.repository.metadata.CompositeMetadata;
061import org.ametys.plugins.repository.metadata.RichText;
062import org.ametys.plugins.repository.metadata.UnknownMetadataException;
063import org.ametys.plugins.workflow.AbstractWorkflowComponent;
064import org.ametys.plugins.workflow.support.WorkflowProvider;
065import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
066import org.ametys.runtime.parameter.Enumerator;
067
068import com.opensymphony.workflow.WorkflowException;
069
070/**
071 * Helper for {@link Content}
072 *
073 */
074public class ContentHelper extends AbstractLogEnabled implements Component, Serviceable
075{
076    /** The component role. */
077    public static final String ROLE = ContentHelper.class.getName();
078    
079    private AmetysObjectResolver _resolver;
080    private ContentTypesHelper _contentTypesHelper;
081    private ContentTypeExtensionPoint _contentTypeEP;
082
083    private ObservationManager _observationManager;
084    private WorkflowProvider _workflowProvider;
085    private CurrentUserProvider _currentUserProvider;
086
087    private UserHelper _userHelper;
088    
089    @Override
090    public void service(ServiceManager smanager) throws ServiceException
091    {
092        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
093        _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
094        _userHelper = (UserHelper) smanager.lookup(UserHelper.ROLE);
095        _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
096        _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE);
097        _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE);
098        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
099    }
100    
101    /**
102     * Add a content type to an existing content
103     * @param contentId The content id
104     * @param contentTypeId The content type to add
105     * @param actionId The workflow action id
106     * @return The result in a Map
107     * @throws WorkflowException if 
108     * @throws AmetysRepositoryException if an error occurred
109     */
110    @Callable
111    public Map<String, Object> addContentType (String contentId, String contentTypeId, int actionId) throws AmetysRepositoryException, WorkflowException
112    {
113        return _setContentType(contentId, contentTypeId, actionId, false);
114    }
115    
116    /**
117     * Remove a content type to an existing content
118     * @param contentId The content id
119     * @param contentTypeId The content type to add
120     * @param actionId The workflow action id
121     * @return The result in a Map
122     * @throws WorkflowException if 
123     * @throws AmetysRepositoryException if an error occurred
124     */
125    @Callable
126    public Map<String, Object> removeContentType (String contentId, String contentTypeId, int actionId) throws AmetysRepositoryException, WorkflowException
127    {
128        return _setContentType(contentId, contentTypeId, actionId, true);
129    }
130    
131    /**
132     * Add a mixin type to an existing content
133     * @param contentId The content id
134     * @param mixinId The mixin type to add
135     * @param actionId The workflow action id
136     * @return The result in a Map
137     * @throws WorkflowException if 
138     * @throws AmetysRepositoryException if an error occurred
139     */
140    @Callable
141    public Map<String, Object> addMixinType (String contentId, String mixinId, int actionId) throws AmetysRepositoryException, WorkflowException
142    {
143        return _setMixinType(contentId, mixinId, actionId, false);
144    }
145    
146    /**
147     * Remove a mixin type to an existing content
148     * @param contentId The content id
149     * @param mixinId The mixin type to add
150     * @param actionId The workflow action id
151     * @return The result in a Map
152     * @throws WorkflowException if 
153     * @throws AmetysRepositoryException if an error occurred
154     */
155    @Callable
156    public Map<String, Object> removeMixinType (String contentId, String mixinId, int actionId) throws AmetysRepositoryException, WorkflowException
157    {
158        return _setMixinType(contentId, mixinId, actionId, true);
159    }
160    
161    /**
162     * Get metadata values by their path
163     * @param contentId The id of content
164     * @param metadataPath The path of metadata to retrieve
165     * @return The metadata values
166     */
167    @Callable
168    public Map<String, Object> getMetadataValues(String contentId, Collection<String> metadataPath)
169    {
170        Content content = _resolver.resolveById(contentId);
171        return getMetadataValues(content, metadataPath);
172    }
173    
174    /**
175     * Get metadata values by their path
176     * @param content The content.
177     * @param metadataPaths The path of metadata to retrieve, slash-separated.
178     * @return The metadata values
179     */
180    @Callable
181    public Map<String, Object> getMetadataValues(Content content, Collection<String> metadataPaths)
182    {
183        Map<String, Object> values = new HashMap<>();
184        
185        for (String path : metadataPaths)
186        {
187            String[] names = StringUtils.split(path, ContentConstants.METADATA_PATH_SEPARATOR);
188            
189            try
190            {
191                MetadataDefinition metadataDefinition = _contentTypesHelper.getMetadataDefinition(names[0], content.getTypes(), content.getMixinTypes());
192                if (metadataDefinition != null)
193                {
194                    Object value = getMetadataValues(content, path);
195                    values.put(path, value);
196                }
197                else
198                {
199                    values.put(path, null);
200                }
201            }
202            catch (UnknownMetadataException e)
203            {
204                if (getLogger().isInfoEnabled())
205                {
206                    getLogger().info("Metadata of path '" + path + "' does not exist for content '" + content.getId() + "'.", e);
207                }
208                values.put(path, null);
209            }
210        }
211        
212        return values;
213    }
214    
215    /**
216     * Get the value of a metadata on a given content. The metadata can be held by a linked content.
217     * @param content The content.
218     * @param metadataPath The metadata path, slash-separated.
219     * It can represent a property on one or more linked contents, i.e. "composite/linkedContent/secondContent/title".
220     * @return The metadata value.
221     */
222    public Object getMetadataValue(Content content, String metadataPath)
223    {
224        List<Object> values = new ArrayList<>();
225        
226        getMetadataValues(content, metadataPath, false, values);
227        
228        return values.isEmpty() ? null : values.get(0);
229    }
230    
231    /**
232     * Get the values of a metadata on a given content. The metadata can be held by a linked content.
233     * @param content The content.
234     * @param metadataPath The metadata path, slash-separated.
235     * It can represent a property on one or more linked contents, i.e. "composite/linkedContent/secondContent/title".
236     * @return The metadata values as a List.
237     */
238    public List<Object> getMetadataValues(Content content, String metadataPath)
239    {
240        return getMetadataValues(content, metadataPath, false);
241    }
242    
243    /**
244     * Get the values of a metadata on a given content. The metadata can be held by a linked content.
245     * @param content The content.
246     * @param metadataPath The metadata path, slash-separated.
247     * It can represent a property on one or more linked contents, i.e. "composite/linkedContent/secondContent/title".
248     * @param allowsNullValues If true, empty values will be added to the list as "null"
249     * @return The metadata values as a List.
250     */
251    public List<Object> getMetadataValues(Content content, String metadataPath, boolean allowsNullValues)
252    {
253        List<Object> values = new ArrayList<>();
254        
255        getMetadataValues(content, metadataPath, allowsNullValues, values);
256        
257        return values;
258    }
259    
260    /**
261     * Get the values of a metadata reference in a specified content.<br>
262     * The metadata can be in the content or in another linked content (directly or transitively).
263     * @param content The content.
264     * @param fullMetaPath The full metadata path (separated by '/').
265     * @param allowsNullValues If true, empty values will be added to the list as "null"
266     * @param values The list of values to be filled by this method.
267     */
268    protected void getMetadataValues(Content content, String fullMetaPath, boolean allowsNullValues, List<Object> values)
269    {
270        try
271        {
272            CompositeMetadata metadataHolder = content.getMetadataHolder();
273            int slashPos = fullMetaPath.indexOf(ContentConstants.METADATA_PATH_SEPARATOR);
274            String metaName = slashPos >= 0 ? fullMetaPath.substring(0, slashPos) : fullMetaPath;
275            String remainingPath = slashPos >= 0 ? fullMetaPath.substring(slashPos + 1) : "";
276            
277            MetadataDefinition definition = _contentTypesHelper.getMetadataDefinition(metaName, content);
278            
279            if (definition != null)
280            {
281                getMetadataValues(content, metadataHolder, definition, metaName, remainingPath, allowsNullValues, values);
282            }
283        }
284        catch (UnknownMetadataException e)
285        {
286            // Ignore, just do not fill the value list.
287        }
288    }
289    
290    /**
291     * Get the values of a metadata reference in a specified content.<br>
292     * The metadata can be in the content or in another linked content (directly or transitively).
293     * @param currentContent The content currently being browsed.
294     * @param metaHolder The current metadata holder, must in some way possess the "metaName" metadata.
295     * @param definition The current metadata definition.
296     * @param metaName The metadata name.
297     * @param remainingPath The remaining path, can be empty.
298     * @param allowsNullValues If true, empty values will be added to the list as "null"
299     * @param values The list of values to be filled by this method.
300     */
301    @SuppressWarnings("unchecked")
302    protected void getMetadataValues(Content currentContent, CompositeMetadata metaHolder, MetadataDefinition definition, String metaName, String remainingPath, boolean allowsNullValues, List<Object> values)
303    {
304        MetadataType type = definition.getType();
305        
306        if (StringUtils.isNotBlank(remainingPath) && (type == MetadataType.CONTENT || type == MetadataType.SUB_CONTENT))
307        {
308            // metaName represents a metadata in the target content content type.
309            getContentMetadataValues(metaHolder, metaName, remainingPath, allowsNullValues, values, type);
310        }
311        else if (StringUtils.isNotBlank(remainingPath) && type == MetadataType.COMPOSITE)
312        {
313            // The metadata is a composite, either a real one or a repeater.
314            getCompositeMetadataValues(currentContent, metaHolder, definition, metaName, remainingPath, allowsNullValues, values);
315        }
316        else
317        {
318            // metaName represents a sub-metadata.
319            Object value = getMetadataValue(metaHolder, metaName, definition);
320            
321            if (value != null || allowsNullValues)
322            {
323                if (value instanceof Collection)
324                {
325                    values.addAll((Collection<Object>) value);
326                }
327                else
328                {
329                    values.add(value);
330                }
331            }
332        }
333    }
334
335    /**
336     * Get the values of a metadata reference in a specified content.<br>
337     * The metadata can be in the content or in another linked content (directly or transitively).
338     * @param currentContent The content currently being browsed.
339     * @param metaHolder The current metadata holder, must in some way possess the "metaName" metadata.
340     * @param definition The current metadata definition.
341     * @param metaName The metadata name.
342     * @param remainingPath The remaining path, can be empty.
343     * @param allowsNullValues If true, empty values will be added to the list as "null"
344     * @param values The list of values to be filled by this method.
345     */
346    protected void getCompositeMetadataValues(Content currentContent, CompositeMetadata metaHolder, MetadataDefinition definition, String metaName, String remainingPath, boolean allowsNullValues, List<Object> values)
347    {
348        // Compute sub-metadata information.
349        int slashPos = remainingPath.indexOf(ContentConstants.METADATA_PATH_SEPARATOR);
350        String subMetaName = slashPos >= 0 ? remainingPath.substring(0, slashPos) : remainingPath;
351        String nextPath = slashPos >= 0 ? remainingPath.substring(slashPos + 1) : "";
352        
353        CompositeMetadata subHolder = metaHolder.getCompositeMetadata(metaName);
354        MetadataDefinition subDefinition = definition.getMetadataDefinition(subMetaName);
355        
356        if (definition instanceof RepeaterDefinition)
357        {
358            // Repeater: get and sort the entry names.
359            String[] entries = subHolder.getMetadataNames();
360            Arrays.sort(entries, MetadataManager.REPEATER_ENTRY_COMPARATOR);
361            
362            for (String entryName : entries)
363            {
364                CompositeMetadata entry = subHolder.getCompositeMetadata(entryName);
365                getMetadataValues(currentContent, entry, subDefinition, subMetaName, nextPath, allowsNullValues, values);
366            }
367        }
368        else
369        {
370            // Composite.
371            getMetadataValues(currentContent, subHolder, subDefinition, subMetaName, nextPath, allowsNullValues, values);
372        }
373    }
374
375    /**
376     * Get the values of a metadata reference in a specified content.<br>
377     * The metadata can be in the content or in another linked content (directly or transitively).
378     * @param metaHolder The current metadata holder, must in some way possess the "metaName" metadata.
379     * @param metaName The metadata name.
380     * @param remainingPath The remaining path, can be empty.
381     * @param allowsNullValues If true, empty values will be added to the list as "null"
382     * @param values The list of values to be filled by this method.
383     * @param type The metadata type
384     */
385    protected void getContentMetadataValues(CompositeMetadata metaHolder, String metaName, String remainingPath, boolean allowsNullValues, List<Object> values, MetadataType type)
386    {
387        if (type == MetadataType.CONTENT)
388        {
389            String[] refContentIds = metaHolder.getStringArray(metaName, new String[0]);
390            for (String refContentId : refContentIds)
391            {
392                try
393                {
394                    Content refContent = _resolver.resolveById(refContentId);
395                    getMetadataValues(refContent, remainingPath, allowsNullValues, values);
396                }
397                catch (AmetysRepositoryException e)
398                {
399                    // Ignore, just do not fill the value list.
400                    if (getLogger().isWarnEnabled())
401                    {
402                        getLogger().warn("A Repository Exception occured while trying to get the values of the referenced content '" + refContentId + "'", e);
403                    }
404                }
405            }
406        }
407        else
408        {
409            try (AmetysObjectIterable<Content> subContents = metaHolder.getObjectCollection(metaName).getChildren())
410            {
411                for (Content subContent : subContents)
412                {
413                    getMetadataValues(subContent, remainingPath, allowsNullValues, values);
414                }
415            }
416        }
417    }
418    
419    /**
420     * Get content edition information.
421     * @param contentId the content ID.
422     * @return a Map containing content edition information.
423     */
424    @Callable
425    public Map<String, Object> getContentEditionInformation(String contentId)
426    {
427        Map<String, Object> info = new HashMap<>();
428        
429        Content content = _resolver.resolveById(contentId);
430        
431        info.put("hasIndexingReferences", hasIndexingReferences(content));
432        
433        return info;
434    }
435    
436    /**
437     * Test if the given content has indexing references, i.e. if modifying it
438     * potentially implies reindexing other contents.
439     * @param content the content to test.
440     * @return <code>true</code> if one of the content types or mixins has indexing references, <code>false</code> otherwise.
441     */
442    public boolean hasIndexingReferences(Content content)
443    {
444        for (String cTypeId : content.getTypes())
445        {
446            if (_contentTypeEP.hasIndexingReferences(cTypeId))
447            {
448                return true;
449            }
450        }
451        
452        for (String mixinId : content.getMixinTypes())
453        {
454            if (_contentTypeEP.hasIndexingReferences(mixinId))
455            {
456                return true;
457            }
458        }
459        
460        return false;
461    }
462    
463    private Map<String, Object> _setContentType (String contentId, String contentTypeId, int actionId, boolean remove) throws AmetysRepositoryException, WorkflowException
464    {
465        Map<String, Object> result = new HashMap<>();
466        
467        Content content = _resolver.resolveById(contentId);
468
469        if (content instanceof ModifiableContent)
470        {
471            ModifiableContent modifiableContent = (ModifiableContent) content;
472            
473            List<String> currentTypes = new ArrayList<>(Arrays.asList(content.getTypes()));
474            
475            boolean hasChange = false;
476            if (remove)
477            {
478                if (currentTypes.size() > 1)
479                {
480                    hasChange = currentTypes.remove(contentTypeId);
481                }
482                else
483                {
484                    result.put("failure", true);
485                    result.put("msg", "empty-list");
486                }
487            }
488            else if (!currentTypes.contains(contentTypeId))
489            {
490                ContentType cType = _contentTypeEP.getExtension(contentTypeId);
491                if (cType.isMixin())
492                {
493                    result.put("failure", true);
494                    result.put("msg", "no-content-type");
495                    getLogger().error("Content type '" + contentTypeId + "' is a mixin type. It can not be added as content type.");
496                }
497                else if (!_contentTypesHelper.isCompatibleContentType(content, contentTypeId))
498                {
499                    result.put("failure", true);
500                    result.put("msg", "invalid-content-type");
501                    getLogger().error("Content type '" + contentTypeId + "' is incompatible with content '" + contentId + "'.");
502                }
503                else
504                {
505                    currentTypes.add(contentTypeId);
506                    hasChange = true;
507                }
508            }
509            
510            if (hasChange)
511            {
512                // TODO check if the content type is compatible
513                modifiableContent.setTypes(currentTypes.toArray(new String[currentTypes.size()]));
514                modifiableContent.saveChanges();
515                
516                if (content instanceof WorkflowAwareContent)
517                {
518                    
519                    WorkflowAwareContent waContent = (WorkflowAwareContent) content;
520                    AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
521                    
522                    Map<String, Object> inputs = new HashMap<>();
523                    inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content);
524                    inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>());
525                    inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>());
526                    
527                    workflow.doAction(waContent.getWorkflowId(), actionId, inputs);
528                }
529                
530                result.put("success", true);
531                
532                Map<String, Object> eventParams = new HashMap<>();
533                eventParams.put(ObservationConstants.ARGS_CONTENT, modifiableContent);
534                eventParams.put(ObservationConstants.ARGS_CONTENT_ID, contentId);
535                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams));
536            }
537        }
538        else
539        {
540            result.put("failure", true);
541            result.put("msg", "no-modifiable-content");
542            getLogger().error("Can not modified content types to a non-modifiable content '" + content.getId() + "'.");
543        }
544        
545        return result;
546    }
547    
548    private Map<String, Object> _setMixinType (String contentId, String mixinId, int actionId, boolean remove) throws AmetysRepositoryException, WorkflowException
549    {
550        Map<String, Object> result = new HashMap<>();
551        
552        Content content = _resolver.resolveById(contentId);
553
554        if (content instanceof ModifiableContent)
555        {
556            ModifiableContent modifiableContent = (ModifiableContent) content;
557            
558            List<String> currentMixins = new ArrayList<>(Arrays.asList(content.getMixinTypes()));
559            
560            boolean hasChange = false;
561            if (remove)
562            {
563                hasChange = currentMixins.remove(mixinId);
564            }
565            else if (!currentMixins.contains(mixinId))
566            {
567                ContentType cType = _contentTypeEP.getExtension(mixinId);
568                if (!cType.isMixin())
569                {
570                    result.put("failure", true);
571                    result.put("msg", "no-mixin");
572                    getLogger().error("The content type '" + mixinId + "' is not a mixin type, it can be not be added as a mixin.");
573                }
574                else if (!_contentTypesHelper.isCompatibleContentType(content, mixinId))
575                {
576                    result.put("failure", true);
577                    result.put("msg", "invalid-mixin");
578                    getLogger().error("Mixin '" + mixinId + "' is incompatible with content '" + contentId + "'.");
579                }
580                else
581                {
582                    currentMixins.add(mixinId);
583                    hasChange = true;
584                }
585            }
586            
587            if (hasChange)
588            {
589                // TODO check if the content type is compatible
590                modifiableContent.setMixinTypes(currentMixins.toArray(new String[currentMixins.size()]));
591                modifiableContent.saveChanges();
592                
593                if (content instanceof WorkflowAwareContent)
594                {
595                    WorkflowAwareContent waContent = (WorkflowAwareContent) content;
596                    AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
597                    
598                    Map<String, Object> inputs = new HashMap<>();
599                    inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content);
600                    inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>());
601                    inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>());
602                    
603                    workflow.doAction(waContent.getWorkflowId(), actionId, inputs);
604                }
605                
606                result.put("success", true);
607                
608                Map<String, Object> eventParams = new HashMap<>();
609                eventParams.put(ObservationConstants.ARGS_CONTENT, modifiableContent);
610                eventParams.put(ObservationConstants.ARGS_CONTENT_ID, contentId);
611                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams));
612            }
613        }
614        else
615        {
616            result.put("failure", true);
617            result.put("msg", "no-modifiable-content");
618            getLogger().error("Can not modified mixins to a non-modifiable content '" + content.getId() + "'.");
619        }
620        
621        return result;
622    }
623    
624    /**
625     * Get a metadata value from a composite metadata.
626     * @param metaHolder the composite metadata.
627     * @param name the metadata name (path forbidden).
628     * @param definition the metadata definition.
629     * @return the metadata value.
630     */
631    public Object getMetadataValue(CompositeMetadata metaHolder, String name, MetadataDefinition definition)
632    {
633        if (name.contains(ContentConstants.METADATA_PATH_SEPARATOR))
634        {
635            throw new IllegalArgumentException("The metadata name cannot represent a path.");
636        }
637        
638        Object value = null;
639        
640        switch (definition.getType())
641        {
642            case STRING:
643                value = getStringValue(metaHolder, name, definition);
644                break;
645            case LONG:
646                value = getLongValue(metaHolder, name, definition);
647                break;
648            case DOUBLE:
649                value = getDoubleValue(metaHolder, name, definition);
650                break;
651            case BOOLEAN:
652                value = getBooleanValue(metaHolder, name, definition);
653                break;
654            case DATE:
655            case DATETIME:
656                value = getDateValue(metaHolder, name, definition);
657                break;
658            case USER:
659                value = getUserValue(metaHolder, name, definition);
660                break;
661            case BINARY:
662                value = getBinaryValue(metaHolder, name, definition);
663                break;
664            case FILE:
665                value = getFileValue(metaHolder, name, definition);
666                break;
667            case GEOCODE:
668                value = getGeocodeValue(metaHolder, name, definition);
669                break;
670            case RICH_TEXT:
671                value = getRichTextValue(metaHolder, name, definition);
672                break;
673            case REFERENCE:
674                value = getReferenceValue(metaHolder, name, definition);
675                break;
676            case CONTENT:
677                value = getContentReferenceValue(metaHolder, name, definition);
678                break;
679            case SUB_CONTENT:
680                value = getSubcontentValue(metaHolder, name, definition);
681                break;
682            default:
683                value = getStringValue(metaHolder, name, definition);
684                break;
685        }
686        
687        return value;
688    }
689    
690    /**
691     * Get a string metadata value from a composite metadata.
692     * @param metaHolder the composite metadata.
693     * @param name the metadata name.
694     * @param definition the metadata definition.
695     * @return the metadata value as a String or String List.
696     */
697    protected Object getStringValue(CompositeMetadata metaHolder, String name, MetadataDefinition definition)
698    {
699        try
700        {
701            if (definition.isMultiple())
702            {
703                return Arrays.asList(metaHolder.getStringArray(name));
704            }
705            else
706            {
707                return metaHolder.getString(name);
708            }
709        }
710        catch (UnknownMetadataException e)
711        {
712            // Ignore, just return null.
713            return null;
714        }
715    }
716    
717    /**
718     * Build a map representing an enumerator entry (value and label).
719     * @param enumerator the metadata enumerator.
720     * @param value the value to generate.
721     * @return a Map representing the enumerator entry (value and label).
722     */
723    protected Map<String, Object> getEnumeratedValueMap(Enumerator enumerator, String value)
724    {
725        Map<String, Object> entry = new HashMap<>();
726        
727        entry.put("value", value);
728        try
729        {
730            entry.put("label", enumerator.getEntry(value));
731        }
732        catch (Exception e)
733        {
734            entry.put("label", value);
735        }
736        return entry;
737    }
738    
739    /**
740     * Get a long metadata value from a composite metadata.
741     * @param metaHolder the composite metadata.
742     * @param name the metadata name.
743     * @param definition the metadata definition.
744     * @return the metadata value as a long or Long List.
745     */
746    protected Object getLongValue(CompositeMetadata metaHolder, String name, MetadataDefinition definition)
747    {
748        try
749        {
750            if (definition.isMultiple())
751            {
752                return Arrays.asList(ArrayUtils.toObject(metaHolder.getLongArray(name)));
753            }
754            else
755            {
756                return metaHolder.getLong(name);
757            }
758        }
759        catch (UnknownMetadataException e)
760        {
761            // Ignore, just return null.
762            return null;
763        }
764
765    }
766    
767    /**
768     * Get a double metadata value from a composite metadata.
769     * @param metaHolder the composite metadata.
770     * @param name the metadata name.
771     * @param definition the metadata definition.
772     * @return the metadata value as a double or Double List.
773     */
774    protected Object getDoubleValue(CompositeMetadata metaHolder, String name, MetadataDefinition definition)
775    {
776        try
777        {
778            if (definition.isMultiple())
779            {
780                return Arrays.asList(ArrayUtils.toObject(metaHolder.getDoubleArray(name)));
781            }
782            else
783            {
784                return metaHolder.getDouble(name);
785            }
786        }
787        catch (UnknownMetadataException e)
788        {
789            // Ignore, just return null.
790            return null;
791        }
792    }
793    
794    /**
795     * Get a boolean metadata value from a composite metadata.
796     * @param metaHolder the composite metadata.
797     * @param name the metadata name.
798     * @param definition the metadata definition.
799     * @return the metadata value as a boolean or Boolean List.
800     */
801    protected Object getBooleanValue(CompositeMetadata metaHolder, String name, MetadataDefinition definition)
802    {
803        try
804        {
805            if (definition.isMultiple())
806            {
807                return Arrays.asList(ArrayUtils.toObject(metaHolder.getBooleanArray(name)));
808            }
809            else
810            {
811                return metaHolder.getBoolean(name);
812            }
813        }
814        catch (UnknownMetadataException e)
815        {
816            // Ignore, just return null.
817            return null;
818        }
819    }
820    
821    /**
822     * Get a Date metadata value from a composite metadata.
823     * @param metaHolder the composite metadata.
824     * @param name the metadata name.
825     * @param definition the metadata definition.
826     * @return the metadata value as a Date or Date List.
827     */
828    protected Object getDateValue(CompositeMetadata metaHolder, String name, MetadataDefinition definition)
829    {
830        try
831        {
832            if (definition.isMultiple())
833            {
834                return Arrays.asList(metaHolder.getDateArray(name));
835            }
836            else
837            {
838                return metaHolder.getDate(name);
839            }
840        }
841        catch (UnknownMetadataException e)
842        {
843            // Ignore, just return null.
844            return null;
845        }
846    }
847    
848    /**
849     * Get a user metadata value from a composite metadata.
850     * @param metaHolder the composite metadata.
851     * @param name the metadata name.
852     * @param definition the metadata definition.
853     * @return the metadata value as a Map or List of Map.
854     */
855    protected Object getUserValue(CompositeMetadata metaHolder, String name, MetadataDefinition definition)
856    {
857        try
858        {
859            if (definition.isMultiple())
860            {
861                List<Map<String, Object>> values = new ArrayList<>();
862                
863                for (UserIdentity identity : metaHolder.getUserArray(name))
864                {
865                    values.add(_userHelper.user2json(identity, true));
866                }
867                
868                return values;
869            }
870            else
871            {
872                UserIdentity identity = metaHolder.getUser(name);
873                return _userHelper.user2json(identity, true);
874            }
875        }
876        catch (UnknownMetadataException e)
877        {
878            // Ignore, just return null.
879        }
880        
881        return null;
882    }
883    
884    /**
885     * Get a binary metadata value from a composite metadata.
886     * @param metaHolder the composite metadata.
887     * @param name the metadata name.
888     * @param definition the metadata definition.
889     * @return the metadata value as a Map.
890     */
891    protected Object getBinaryValue(CompositeMetadata metaHolder, String name, MetadataDefinition definition)
892    {
893        Map<String, Object> info = new HashMap<>();
894        
895        BinaryMetadata value = metaHolder.getBinaryMetadata(name);
896        String filename = value.getFilename();
897        
898        info.put("type", "metadata");
899        info.put("mime-type", value.getMimeType());
900        info.put("name", name);
901        info.put("size", value.getLength());
902        info.put("lastModified", value.getLastModified());
903        
904        if (filename != null)
905        {
906            info.put("filename", filename);
907        }
908        
909        return info;
910    }
911    
912    /**
913     * Get a file metadata value from a composite metadata.
914     * @param metaHolder the composite metadata.
915     * @param name the metadata name.
916     * @param definition the metadata definition.
917     * @return the metadata value as a Map.
918     */
919    protected Object getFileValue(CompositeMetadata metaHolder, String name, MetadataDefinition definition)
920    {
921        if (org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType.BINARY.equals(metaHolder.getType(name)))
922        {
923            return getBinaryValue(metaHolder, name, definition);
924        }
925        else
926        {
927            return getResourceFileValue(metaHolder, name, definition);
928        }
929    }
930    
931    /**
932     * Get a resource-file metadata value from a composite metadata.
933     * @param metaHolder the composite metadata.
934     * @param name the metadata name.
935     * @param definition the metadata definition.
936     * @return the metadata value as a Map.
937     */
938    protected Object getResourceFileValue(CompositeMetadata metaHolder, String name, MetadataDefinition definition)
939    {
940        try
941        {
942            Map<String, Object> info = new HashMap<>();
943            
944            String value = metaHolder.getString(name);
945            
946            Resource resource = (Resource) _resolver.resolveById(value);
947            
948            String filename = resource.getName();
949            
950            info.put("type", "explorer");
951            info.put("id", resource.getId());
952            info.put("mime-type", resource.getMimeType());
953            info.put("size", resource.getLength());
954            info.put("lastModified", resource.getLastModified());
955            
956            if (filename != null)
957            {
958                info.put("filename", filename);
959            }
960            
961            return info;
962        }
963        catch (UnknownAmetysObjectException e)
964        {
965            // Ignore, just return null.
966            return null;
967        }
968    }
969    
970    /**
971     * Get a geocode metadata value from a composite metadata.
972     * @param metaHolder the composite metadata.
973     * @param name the metadata name.
974     * @param definition the metadata definition.
975     * @return the metadata value as a Map.
976     */
977    protected Object getGeocodeValue(CompositeMetadata metaHolder, String name, MetadataDefinition definition)
978    {
979        try
980        {
981            CompositeMetadata values = metaHolder.getCompositeMetadata(name);
982            
983            if (values.hasMetadata("longitude") && values.hasMetadata("latitude")) 
984            {
985                Double longitude = values.getDouble("longitude");
986                Double latitude = values.getDouble("latitude");
987                
988                Map<String, Object> geocode = new LinkedHashMap<>();
989                geocode.put("longitude", longitude);
990                geocode.put("latitude", latitude);
991                
992                return geocode;
993            }
994        }
995        catch (UnknownMetadataException e)
996        {
997            // Ignore, just return null.
998        }
999        
1000        return null;
1001    }
1002    
1003    /**
1004     * Get a string (HTML format) reprensenting the rich text metadata value from a composite metadata.
1005     * @param metaHolder the composite metadata.
1006     * @param name the metadata name.
1007     * @param definition the metadata definition.
1008     * @return The richtext object
1009     */
1010    protected RichText getRichTextValue(CompositeMetadata metaHolder, String name, MetadataDefinition definition)
1011    {
1012        try
1013        {
1014            return metaHolder.getRichText(name);
1015        }
1016        catch (UnknownMetadataException e)
1017        {
1018            // Ignore, just return null.
1019            return null;
1020        }
1021    }
1022    
1023    /**
1024     * Get a reference metadata value from a composite metadata.
1025     * @param metaHolder the composite metadata.
1026     * @param name the metadata name.
1027     * @param definition the metadata definition.
1028     * @return the metadata value as a List (if multiple) or Map (if single metadata)
1029     */
1030    protected Object getReferenceValue(CompositeMetadata metaHolder, String name, MetadataDefinition definition)
1031    {
1032        try
1033        {
1034            CompositeMetadata referencesComposite = metaHolder.getCompositeMetadata(name);
1035            
1036            if (definition.isMultiple())
1037            {
1038                List<Map<String, Object>> references = new ArrayList<>();
1039                
1040                String[] types = referencesComposite.getStringArray("types");
1041                String[] values = referencesComposite.getStringArray("values");
1042                
1043                for (int i = 0; i < types.length; i++)
1044                {
1045                    Map<String, Object> reference = new HashMap<>(2);
1046                    reference.put("type", types[i]);
1047                    reference.put("value", values[i]);
1048                    
1049                    references.add(reference);
1050                }
1051                
1052                return references;
1053            }
1054            else
1055            {
1056                String type = referencesComposite.getString("type");
1057                String value = referencesComposite.getString("value");
1058                
1059                Map<String, Object> reference = new HashMap<>(2);
1060                reference.put("type", type);
1061                reference.put("value", value);
1062                
1063                return reference;
1064            }
1065        }
1066        catch (UnknownMetadataException e)
1067        {
1068            // Ignore, just return null.
1069            return null;
1070        }
1071    }
1072    
1073    /**
1074     * Get a content reference metadata value from a composite metadata.
1075     * @param metaHolder the composite metadata.
1076     * @param name the metadata name.
1077     * @param definition the metadata definition.
1078     * @return the metadata value as a List of content ID.
1079     */
1080    protected Object getContentReferenceValue(CompositeMetadata metaHolder, String name, MetadataDefinition definition)
1081    {
1082        try
1083        {
1084            String[] values = metaHolder.getStringArray(name);
1085            
1086            if (values != null)
1087            {
1088                return Arrays.asList(values);
1089            }
1090        }
1091        catch (UnknownMetadataException e)
1092        {
1093            // Ignore, just return null.
1094        }
1095        
1096        return null;
1097    }
1098    
1099    /**
1100     * Get a sub-content metadata value from a composite metadata.
1101     * @param metaHolder the composite metadata.
1102     * @param name the metadata name.
1103     * @param definition the metadata definition.
1104     * @return the metadata value as a List of content ID.
1105     */
1106    protected Object getSubcontentValue(CompositeMetadata metaHolder, String name, MetadataDefinition definition)
1107    {
1108        try
1109        {
1110            TraversableAmetysObject contentMeta = metaHolder.getObjectCollection(name);
1111            
1112            List<String> ids = new ArrayList<>();
1113            
1114            try (AmetysObjectIterable<Content> contents = contentMeta.getChildren())
1115            {
1116                for (Content refContent : contents)
1117                {
1118                    ids.add(refContent.getId());
1119                }
1120            }
1121            
1122            return ids;
1123        }
1124        catch (UnknownMetadataException e)
1125        {
1126            // Ignore, just return null.
1127        }
1128        
1129        return null;
1130    }
1131    
1132    /**
1133     * Determines if the content is a simple content type
1134     * @param content The content
1135     * @return true if content is simple
1136     */
1137    public boolean isSimple (Content content)
1138    {
1139        for (String cTypeId : content.getTypes())
1140        {
1141            ContentType cType = _contentTypeEP.getExtension(cTypeId);
1142            if (cType != null)
1143            {
1144                if (!cType.isSimple())
1145                {
1146                    return false;
1147                }
1148            }
1149            else
1150            {
1151                if (getLogger().isWarnEnabled())
1152                {
1153                    getLogger().warn(String.format("Unable to determine if a content is simple, unknown content type : '%s'.", cTypeId));
1154                }
1155            }
1156        }
1157        return true;
1158    }
1159}