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.search.content;
017
018import java.io.InputStream;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Date;
023import java.util.HashMap;
024import java.util.LinkedList;
025import java.util.List;
026import java.util.Locale;
027import java.util.Map;
028import java.util.Optional;
029import java.util.stream.Collectors;
030
031import org.apache.avalon.framework.component.Component;
032import org.apache.avalon.framework.context.Context;
033import org.apache.avalon.framework.context.ContextException;
034import org.apache.avalon.framework.context.Contextualizable;
035import org.apache.avalon.framework.service.ServiceException;
036import org.apache.avalon.framework.service.ServiceManager;
037import org.apache.avalon.framework.service.Serviceable;
038import org.apache.commons.lang3.ArrayUtils;
039import org.apache.commons.lang3.StringUtils;
040import org.apache.excalibur.xml.sax.SAXParser;
041import org.xml.sax.InputSource;
042
043import org.ametys.cms.content.ContentHelper;
044import org.ametys.cms.content.RichTextHandler;
045import org.ametys.cms.contenttype.ContentConstants;
046import org.ametys.cms.contenttype.ContentType;
047import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
048import org.ametys.cms.contenttype.ContentTypesHelper;
049import org.ametys.cms.contenttype.MetadataDefinition;
050import org.ametys.cms.contenttype.MetadataManager;
051import org.ametys.cms.contenttype.MetadataType;
052import org.ametys.cms.contenttype.RepeaterDefinition;
053import org.ametys.cms.contenttype.indexing.CustomIndexingField;
054import org.ametys.cms.contenttype.indexing.DefaultMetadataIndexingField;
055import org.ametys.cms.contenttype.indexing.IndexingField;
056import org.ametys.cms.contenttype.indexing.IndexingModel;
057import org.ametys.cms.contenttype.indexing.MetadataIndexingField;
058import org.ametys.cms.repository.Content;
059import org.ametys.cms.repository.DefaultContent;
060import org.ametys.cms.search.SearchField;
061import org.ametys.cms.search.model.SystemProperty;
062import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
063import org.ametys.cms.search.solr.field.BooleanSearchField;
064import org.ametys.cms.search.solr.field.ContentSearchField;
065import org.ametys.cms.search.solr.field.DateSearchField;
066import org.ametys.cms.search.solr.field.DoubleSearchField;
067import org.ametys.cms.search.solr.field.JoinedSystemSearchField;
068import org.ametys.cms.search.solr.field.LongSearchField;
069import org.ametys.cms.search.solr.field.MultilingualStringSearchField;
070import org.ametys.cms.search.solr.field.StringSearchField;
071import org.ametys.cms.transformation.xslt.ResolveURIComponent;
072import org.ametys.cms.workflow.EditContentFunction;
073import org.ametys.core.ui.Callable;
074import org.ametys.core.user.UserIdentity;
075import org.ametys.core.util.DateUtils;
076import org.ametys.core.util.I18nUtils;
077import org.ametys.plugins.core.user.UserHelper;
078import org.ametys.plugins.explorer.resources.Resource;
079import org.ametys.plugins.repository.AmetysObjectResolver;
080import org.ametys.plugins.repository.metadata.BinaryMetadata;
081import org.ametys.plugins.repository.metadata.CompositeMetadata;
082import org.ametys.plugins.repository.metadata.MultilingualString;
083import org.ametys.plugins.repository.metadata.MultilingualStringHelper;
084import org.ametys.plugins.repository.metadata.RichText;
085import org.ametys.runtime.parameter.Enumerator;
086import org.ametys.runtime.plugin.component.AbstractLogEnabled;
087
088/**
089 * Component which helps content searching by providing a simple way to access
090 * content properties (either metadata or system properties).
091 */
092public class ContentSearchHelper extends AbstractLogEnabled implements Component, Serviceable, Contextualizable
093{
094
095    /** The component role. */
096    public static final String ROLE = ContentSearchHelper.class.getName();
097    
098    /** The content type extension point. */
099    protected ContentTypeExtensionPoint _cTypeEP;
100    
101    /** The content type helper. */
102    protected ContentTypesHelper _cTypeHelper;
103    
104    /** The content helper. */
105    protected ContentHelper _contentHelper;
106    
107    /** The system property extension point. */
108    protected SystemPropertyExtensionPoint _sysPropEP;
109    
110    /** The ametys object resolver. */
111    protected AmetysObjectResolver _resolver;
112
113    /** The user helper */
114    protected UserHelper _userHelper;
115
116    /** Content Types helper */
117    protected ContentTypesHelper _contentTypesHelper;
118
119    /** i18n Utils */
120    protected I18nUtils _i18nUtils;
121
122    /** Avalon service manager */
123    protected ServiceManager _manager;
124    
125    private Context _context;
126    
127    @Override
128    public void contextualize(Context context) throws ContextException
129    {
130        _context = context;
131    }
132    
133    @Override
134    public void service(ServiceManager manager) throws ServiceException
135    {
136        _manager = manager;
137        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
138        _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
139        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
140        _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
141        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
142        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
143        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
144        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
145    }
146    
147    /**
148     * Get the metadata indexing field for the "title" standard metadata.
149     * @return The standard title metadata indexing field.
150     */
151    public MetadataIndexingField getTitleMetadataIndexingField()
152    {
153        return new DefaultMetadataIndexingField("title", ContentTypesHelper.getTitleMetadataDefinition(), "title");
154    }
155    
156    /**
157     * Get a {@link SearchField} corresponding to a metadata.
158     * @param metadataPath The metadata path.
159     * @param metadataType The metadata type.
160     * @param isTypeContentWithMultilingualTitle <code>true</code> if the type is Content and the linked contents have multilingual titles
161     * @return the search field.
162     */
163    public SearchField getMetadataSearchField(String metadataPath, MetadataType metadataType, boolean isTypeContentWithMultilingualTitle)
164    {
165        return getMetadataSearchField(null, metadataPath, metadataType, isTypeContentWithMultilingualTitle);
166    }
167    
168    /**
169     * Get a {@link SearchField} corresponding to a metadata.
170     * @param joinPaths The join paths
171     * @param metadataPath The metadata path.
172     * @param metadataType The metadata type.
173     * @param isTypeContentWithMultilingualTitle <code>true</code> if the type is Content and the linked contents have multilingual titles
174     * @return the search field.
175     */
176    public SearchField getMetadataSearchField(List<String> joinPaths, String metadataPath, MetadataType metadataType, boolean isTypeContentWithMultilingualTitle)
177    {
178        switch (metadataType)
179        {
180            case STRING:
181            case USER:
182                return new StringSearchField(joinPaths, metadataPath);
183            case CONTENT:
184            case SUB_CONTENT:
185                return new ContentSearchField(joinPaths, metadataPath, isTypeContentWithMultilingualTitle, Optional.ofNullable(_context));
186            case LONG:
187                return new LongSearchField(joinPaths, metadataPath);
188            case DOUBLE:
189                return new DoubleSearchField(joinPaths, metadataPath);
190            case BOOLEAN:
191                return new BooleanSearchField(joinPaths, metadataPath);
192            case DATE:
193            case DATETIME:
194                return new DateSearchField(joinPaths, metadataPath);
195            case MULTILINGUAL_STRING:
196                return new MultilingualStringSearchField(joinPaths, metadataPath, Optional.ofNullable(_context));
197            case COMPOSITE:
198            case BINARY:
199            case FILE:
200            case RICH_TEXT:
201            case REFERENCE:
202            default:
203                return null;
204        }
205    }
206    
207    /**
208     * Get a {@link SearchField} from a field name in a batch of content types.
209     * @param contentTypes The content types, can be empty to search on any content type.
210     * In that case, only the title metadata and system properties will be usable in sort and facets specs.
211     * @param fieldPath The field path, can be either a system property ID or a indexing field name or path.
212     * @return The {@link SearchField} corresponding to the field path, or an {@link Optional#empty() empty optional} if not found
213     */
214    public Optional<SearchField> getSearchField(Collection<String> contentTypes, String fieldPath)
215    {
216        SearchField searchField = null;
217        
218        String[] pathSegments = StringUtils.split(fieldPath, ContentConstants.METADATA_PATH_SEPARATOR);
219        String fieldName = pathSegments[0];
220        
221        // System property ?
222        if (_sysPropEP.hasExtension(fieldName) && pathSegments.length == 1)
223        {
224            return Optional.ofNullable(_sysPropEP.getExtension(fieldName).getSearchField());
225        }
226        
227        String commonContentTypeId = _cTypeHelper.getCommonAncestor(contentTypes);
228        
229        MetadataType type = null;
230        MetadataDefinition def = null;
231        
232        if (commonContentTypeId != null)
233        {
234            
235            // Indexing field.
236            ContentType cType = _cTypeEP.getExtension(commonContentTypeId);
237            IndexingField indexingField = cType.getIndexingModel().getField(fieldName);
238            
239            if (indexingField == null)
240            {
241                throw new IllegalArgumentException("Search field with path '" + fieldPath + "' refers to an unknown indexing field: " + fieldName);
242            }
243            
244            if (indexingField instanceof MetadataIndexingField)
245            {
246                List<String> joinPaths = new ArrayList<>();
247                String[] remainingPathSegments = pathSegments.length > 1 ? (String[]) ArrayUtils.subarray(pathSegments, 1, pathSegments.length) : new String[0];
248                
249                searchField = _getSearchField((MetadataIndexingField) indexingField, remainingPathSegments, joinPaths, true);
250                def = ((MetadataIndexingField) indexingField).getMetadataDefinition();
251            }
252            else if (indexingField instanceof CustomIndexingField)
253            {
254                type = indexingField.getType();
255            }
256        }
257        else if (fieldPath.equals("title"))
258        {
259            // No specific content type: allow only title.
260            type = ContentTypesHelper.getTitleMetadataDefinition().getType();
261        }
262        
263        if (searchField == null)
264        {
265            searchField = type != null ? getMetadataSearchField(fieldPath, type, isTitleMultilingual(def)) : null;
266        }
267        
268        return Optional.ofNullable(searchField);
269    }
270    
271    /**
272     * Get the search field from the indexing field and compute the join paths. Can be null if the last indexing field is a custom indexing field.
273     * @param indexingField The initial indexing field
274     * @param remainingPathSegments The path to access the metadata or an another indexing field from the initial indexing field
275     * @param joinPaths The consecutive's path in case of joint to access the field/metadata
276     * @param addLast <code>true</code> to add the last join path element to the list, <code>false</code> otherwise.
277     * @return The search field or null if not found
278     */
279    protected SearchField _getSearchField(MetadataIndexingField indexingField, String[] remainingPathSegments, List<String> joinPaths, boolean addLast)
280    {
281        StringBuilder currentMetaPath = new StringBuilder();
282        currentMetaPath.append(indexingField.getName());
283        
284        MetadataDefinition definition = indexingField.getMetadataDefinition();
285        
286        for (int i = 0; i < remainingPathSegments.length && definition != null; i++)
287        {
288            String currentPathSegment =  remainingPathSegments[i];
289            if (definition.getType() == MetadataType.CONTENT || definition.getType() == MetadataType.SUB_CONTENT)
290            {
291                // Add path to content from current content type to join paths.
292                // Join paths are the consecutive metadata paths (separated with '/') to access
293                // the searched content, for instance [address/city, links/department].
294                joinPaths.add(currentMetaPath.toString());
295                
296                String refCTypeId = definition.getContentType();
297                if (refCTypeId != null)
298                {
299                    if (!_cTypeEP.hasExtension(refCTypeId))
300                    {
301                        throw new IllegalArgumentException("Search criterion with path '" + StringUtils.join(remainingPathSegments, ContentConstants.METADATA_PATH_SEPARATOR) + "' references an unknown content type:" + refCTypeId); 
302                    }
303                    
304                    ContentType refCType = _cTypeEP.getExtension(refCTypeId);
305                    IndexingModel refIndexingModel = refCType.getIndexingModel();
306                    
307                    IndexingField refIndexingField = refIndexingModel.getField(currentPathSegment);
308                    if (refIndexingField == null && _sysPropEP.hasExtension(currentPathSegment))
309                    {
310                        SystemProperty sysProperty = _sysPropEP.getExtension(currentPathSegment);
311                        return new JoinedSystemSearchField(joinPaths, sysProperty.getSearchField());
312                    }
313                    else if (refIndexingField == null)
314                    {
315                        throw new IllegalArgumentException("Search criterion with path '" + StringUtils.join(remainingPathSegments, ContentConstants.METADATA_PATH_SEPARATOR) + "' refers to an unknown indexing field: " + currentPathSegment);
316                    }
317                    
318                    return _getSearchField((MetadataIndexingField) refIndexingField, ArrayUtils.subarray(remainingPathSegments, i + 1, remainingPathSegments.length), joinPaths, addLast);
319                }
320                else if ("title".equals(currentPathSegment))
321                {
322                    // No specific content type: allow only title.
323                    return _getSearchField(ContentTypesHelper.getTitleMetadataDefinition(), joinPaths);
324                }
325            }
326            else
327            {
328                if (definition instanceof RepeaterDefinition)
329                {
330                    // Add path to repeater from current content type or last repeater to join paths
331                    joinPaths.add(currentMetaPath.toString());
332                    currentMetaPath = new StringBuilder();
333                    currentMetaPath.append(currentPathSegment);
334                }
335                else
336                {
337                    currentMetaPath.append(ContentConstants.METADATA_PATH_SEPARATOR).append(currentPathSegment);
338                }
339                definition = definition.getMetadataDefinition(currentPathSegment);
340            }
341        }
342        
343        if (addLast)
344        {
345            joinPaths.add(currentMetaPath.toString());
346        }
347        
348        return _getSearchField(definition, joinPaths);
349    }
350    
351    private SearchField _getSearchField(MetadataDefinition def, List<String> joinPaths)
352    {
353        MetadataType type = def.getType();
354        
355        String metadataPath = joinPaths.remove(joinPaths.size() - 1);
356        return type != null ? getMetadataSearchField(joinPaths, metadataPath, type, isTitleMultilingual(def)) : null;
357    }
358    
359    /**
360     * Determines if the given metadata definition represents a CONTENT metadata with contents with multilingual titles
361     * @param def The metadata definition
362     * @return <code>true</code> if the given metadata definition represents a CONTENT metadata with contents with multilingual titles
363     */
364    public boolean isTitleMultilingual(MetadataDefinition def)
365    {
366        boolean isTitleMultilingual = Optional.ofNullable(def)
367                .map(MetadataDefinition::getContentType)
368                .map(_cTypeEP::getExtension)
369                .map(cType -> cType.getMetadataDefinition(DefaultContent.METADATA_TITLE))
370                .map(MetadataDefinition::getType)
371                .map(MetadataType.MULTILINGUAL_STRING::equals)
372                .orElse(false);
373        return isTitleMultilingual;
374    }
375    
376    /**
377     * Get metadata values by their path
378     * @param contentId The id of content
379     * @param metadataPath The path of metadata to retrieve
380     * @param defaultLocale The default locale to resolve multilingual values if the content's language is null. Can be null.
381     * @return The metadata values
382     */
383    @Callable
384    public Map<String, Object> getMetadataValues(String contentId, Collection<String> metadataPath, Locale defaultLocale)
385    {
386        Content content = _resolver.resolveById(contentId);
387        return getMetadataValues(content, metadataPath, defaultLocale);
388    }
389    
390    /**
391     * Get metadata values by their path
392     * @param content The initial content
393     * @param metadataPaths The path of metadata to retrieve, slash-separated.
394     * @param defaultLocale The default locale to resolve multilingual values if the content's language is null. Can be null.
395     * @return The metadata values
396     */
397    public Map<String, Object> getMetadataValues(Content content, Collection<String> metadataPaths, Locale defaultLocale)
398    {
399        Map<String, Object> values = new HashMap<>();
400        
401        for (String path : metadataPaths)
402        {
403            Object value = getMetadataValue(content, path, defaultLocale);
404            values.put(path, value);
405        }
406        
407        return values;
408    }
409    
410    /**
411     * Get the value(s) of a metadata - transformed into JSONified value - of a content at given path.
412     * 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'.
413     * The returned value is typed.
414     * @param content The content
415     * @param metadataPath The path to the metadata, separated by '/'
416     * @param defaultLocale The default locale to resolve multilingual values if the content's language is null. Can be null.
417     * @return The final value.
418     */
419    public Object getMetadataValue(Content content, String metadataPath, Locale defaultLocale)
420    {
421        return getMetadataValue(content, metadataPath, defaultLocale, false);
422    }
423    
424    /**
425     * Get the value(s) of a metadata - transformed into JSONified value - of a content at given path.
426     * 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'.
427     * @param content The content
428     * @param metadataPath The path to the metadata, separated by '/'
429     * @param defaultLocale The default locale to resolve multilingual values if the content's language is null. Can be null.
430     * @param resolveReferences <code>true</code> true to resolve references
431     * @return The final value, tranformed for search.
432     */
433    public Object getMetadataValue(Content content, String metadataPath, Locale defaultLocale, boolean resolveReferences)
434    {
435        MetadataDefinition metadataDefinition = _contentTypesHelper.getMetadataDefinition(metadataPath, content);
436        if (metadataDefinition != null && metadataDefinition instanceof RepeaterDefinition)
437        {
438            String[] pathSegments = metadataPath.split(ContentConstants.METADATA_PATH_SEPARATOR);
439            
440            MetadataDefinition definition = _contentTypesHelper.getMetadataDefinition(pathSegments[0], content);
441            if (definition != null)
442            {
443                return _getRepeaterValues(content, content.getMetadataHolder(), definition, "", metadataPath, defaultLocale, resolveReferences);
444            }
445            
446            return null;
447        }
448        else
449        {
450            Object value = _contentHelper.getMetadataValue(content, metadataPath, defaultLocale, resolveReferences);
451            return _transformValue(content, metadataPath, value, null, defaultLocale);
452        }
453    }
454    
455    /**
456     * Get the value(s) of a metadata - transformed into JSONified value - of a content at given path.
457     * 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'.
458     * @param content The initial content.
459     * @param fullMetadataPath The path to the metadata, separated by '/'
460     * @param type The metadata type.
461     * @param multiple <code>true</code> if the metadata is multiple.
462     * @param enumerator The metadata enumerator. Can be null.
463     * @param defaultLocale The default locale to resolve multilingual values if the content's language is null. Can be null.
464     * @param full <code>true</code> to generate full representation of metadata's values : the references will be resolved and the label of enumerated values will be returned.
465     * @return The final value, tranformed for search.
466     */
467    public Object getMetadataValue(Content content, String fullMetadataPath, MetadataType type, boolean multiple, Enumerator enumerator, Locale defaultLocale, boolean full)
468    {
469        // If full, extract content or resource values as Maps. If not, the values are kept as content/resources IDs.
470        // If full, extract enumerated values as Maps of <key,label>. If not, the values are kept as entry IDs.
471        
472        MetadataDefinition metadataDefinition = _contentTypesHelper.getMetadataDefinition(fullMetadataPath, content);
473        if (metadataDefinition != null && metadataDefinition instanceof RepeaterDefinition)
474        {
475            String[] pathSegments = fullMetadataPath.split(ContentConstants.METADATA_PATH_SEPARATOR);
476            MetadataDefinition definition = pathSegments.length > 1 ? _contentTypesHelper.getMetadataDefinition(pathSegments[0], content) : metadataDefinition;
477            return _getRepeaterValues(content, content.getMetadataHolder(), definition, fullMetadataPath, fullMetadataPath, defaultLocale, full);
478        }
479        else
480        {
481            Object rawValue = _contentHelper.getMetadataValue(content, fullMetadataPath, defaultLocale, full);
482            
483            if (!multiple && rawValue instanceof Collection<?>)
484            {
485                if (((Collection<?>) rawValue).iterator().hasNext())
486                {
487                    rawValue = ((Collection<?>) rawValue).iterator().next();
488                }
489                else
490                {
491                    rawValue = null;
492                }
493            }
494            
495            return _transformValue(content, fullMetadataPath, rawValue, full ? enumerator : null, defaultLocale);
496        }
497    }
498
499    /**
500     * Transform the raw value into a JSONified value, understandable for search UI
501     * @param content the content
502     * @param metadataPath The path to the metadata, separated by '/'
503     * @param rawValue The typed value
504     * @param enumerator The enumerator. Can be null
505     * @param defaultLocale Locale to use for localized values if content's language is null.
506     * @return The transformed value
507     */
508    public Object _transformValue(Content content, String metadataPath, Object rawValue, Enumerator enumerator, Locale defaultLocale)
509    {
510        if (rawValue == null)
511        {
512            return null;
513        }
514        else if (rawValue instanceof Resource)
515        {
516            return _transformValue((Resource) rawValue);
517        }
518        else if (rawValue instanceof Content)
519        {
520            return _transformValue(content, (Content) rawValue, defaultLocale);
521        }
522        else if (rawValue instanceof UserIdentity)
523        {
524            return _transformValue((UserIdentity) rawValue);
525        }
526        else if (rawValue instanceof BinaryMetadata)
527        {
528            return _transformValue(content, metadataPath, (BinaryMetadata) rawValue);
529        }
530        else if (rawValue instanceof RichText)
531        {
532            return _transformValue((RichText) rawValue);
533        }
534        else if (rawValue instanceof MultilingualString)
535        {
536            return _transformValue((MultilingualString) rawValue);
537        }
538        else if (rawValue instanceof Collection<?>)
539        {
540            List<Object> tranformedValues = new ArrayList<>();
541            for (Object object : (Collection<?>) rawValue)
542            {
543                tranformedValues.add(_transformValue(content, metadataPath, object, enumerator, defaultLocale));
544            }
545            return tranformedValues;
546        }
547        else if (rawValue instanceof Date)
548        {
549            return DateUtils.dateToString((Date) rawValue);
550        }
551        else if (enumerator != null)
552        {
553            Map<String, Object> tranformedValue = new HashMap<>();
554            tranformedValue.put("value", rawValue);
555            
556            try
557            {
558                tranformedValue.put("label", enumerator.getEntry(rawValue.toString()));
559            }
560            catch (Exception e)
561            {
562                tranformedValue.put("label", rawValue);
563            }
564            
565            return tranformedValue;
566        }
567        else
568        {
569            return rawValue;
570        }
571    }
572    
573    private Object _transformValue(Resource resource)
574    {
575        Map<String, Object> info = new HashMap<>();
576        
577        info.put("type", "explorer");
578        info.put("id", resource.getId());
579        info.put("filename", resource.getName());
580        info.put("mime-type", resource.getMimeType());
581        info.put("size", resource.getLength());
582        info.put("lastModified", resource.getLastModified());
583        info.put("viewUrl", ResolveURIComponent.resolve("explorer", resource.getId(), false));
584        info.put("downloadUrl", ResolveURIComponent.resolve("explorer", resource.getId(), true));
585        
586        return info;
587    }
588    
589    private Object _transformValue(Content content, Content value, Locale defaultLocale)
590    {
591        Map<String, Object> info = new HashMap<>();
592        
593        Locale locale = content.getLanguage() != null ? new Locale(content.getLanguage()) : defaultLocale;
594        info.put("id", value.getId());
595        info.put("title", value.getTitle(locale));
596        info.put("isSimple", _contentHelper.isSimple(value));
597        
598        return info;
599    }
600    
601    private Object _transformValue(UserIdentity user)
602    {
603        return _userHelper.user2json(user, true);
604    }
605    
606    private Object _transformValue(MultilingualString multilingualString)
607    {
608        return MultilingualStringHelper.toJson(multilingualString);
609    }
610    
611    private Object _transformValue(Content content, String metadataPath, BinaryMetadata binary)
612    {
613        Map<String, Object> info = new HashMap<>();
614        
615        info.put("type", "metadata");
616        info.put("id", EditContentFunction.UNTOUCHED_BINARY);
617        info.put("mime-type", binary.getMimeType());
618        info.put("filename", binary.getFilename());
619        info.put("size", binary.getLength());
620        info.put("lastModified", binary.getLastModified());
621        info.put("viewUrl", ResolveURIComponent.resolve("metadata", metadataPath + "?objectId=" + content.getId(), false));
622        info.put("downloadUrl", ResolveURIComponent.resolve("metadata", metadataPath + "?objectId=" + content.getId(), true));
623        
624        return info;
625    }
626    
627    private Object _transformValue(RichText richText)
628    {
629        Map<String, Object> info = new HashMap<>();
630        
631        info.put("type", "metadata");
632        info.put("mime-type", richText.getMimeType());
633        info.put("size", richText.getLength());
634        info.put("lastModified", richText.getLastModified());
635
636        SAXParser saxParser = null;
637        try (InputStream is = richText.getInputStream())
638        {
639            RichTextHandler txtHandler = new RichTextHandler(100);
640            saxParser = (SAXParser) _manager.lookup(SAXParser.ROLE);
641            saxParser.parse(new InputSource(is), txtHandler);
642            
643            String excerpt = txtHandler.getValue();
644            info.put("content", excerpt);
645        }
646        catch (Exception e)
647        {
648            getLogger().error("Cannot extract excerpt of a RichText value ", e);
649            info.put("content", "");
650        }
651        finally
652        {
653            _manager.release(saxParser);
654        }
655        
656        return info;
657    }
658       
659    private Object _getRepeaterValues(Content content, CompositeMetadata metadataHolder, MetadataDefinition definition, String prefix, String metadataPath, Locale defaultLocale, boolean resolveReferences)
660    {
661        String[] pathSegments = metadataPath.split(ContentConstants.METADATA_PATH_SEPARATOR);
662        
663        String metadataName = pathSegments[0];
664        
665        if (!metadataHolder.hasMetadata(metadataName) || definition == null)
666        {
667            return null;
668        }
669        
670        MetadataType type = definition.getType();
671        switch (type)
672        {
673            case COMPOSITE:
674                return _getCompositeRepeaterValues(content, metadataHolder, definition, defaultLocale, resolveReferences, pathSegments, prefix, metadataName);
675            
676            case CONTENT:
677                return _getContentRepeaterValues(content, metadataHolder, definition, metadataPath, defaultLocale, resolveReferences, pathSegments, metadataName);
678                
679            default:
680                if (pathSegments.length == 1)
681                {
682                    Locale locale = content.getLanguage() != null ? new Locale(content.getLanguage()) : defaultLocale; 
683                    Object rawValue = _contentHelper.getSimpleMetadataValue(metadataHolder, definition, metadataName, locale, resolveReferences);
684                    return _transformValue(content, prefix + ContentConstants.METADATA_PATH_SEPARATOR + metadataPath, rawValue, null, defaultLocale);
685                }
686                
687                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));
688        }
689        
690    }
691
692    private Object _getContentRepeaterValues(Content content, CompositeMetadata metadataHolder, MetadataDefinition definition, String metadataPath, Locale defaultLocale,
693            boolean resolveReferences, String[] pathSegments, String metadataName)
694    {
695        if (pathSegments.length > 1)
696        {
697            if (definition.isMultiple())
698            {
699                List<Object> values = new ArrayList<>();
700                String[] refContentIds = metadataHolder.getStringArray(metadataName, new String[0]);
701                for (String refContentId : refContentIds)
702                {
703                    Content refContent = _resolver.resolveById(refContentId);
704                    Object remoteValue = getMetadataValue(refContent, StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), defaultLocale, resolveReferences);
705                    if (remoteValue != null && remoteValue instanceof Collection<?>)
706                    {
707                        values.addAll((Collection<?>) remoteValue);
708                    }
709                    else if (remoteValue != null)
710                    {
711                        values.add(remoteValue);
712                    }
713                }
714                return values;
715            }
716            else
717            {
718                String refContentId = metadataHolder.getString(metadataName);
719                Content refContent = _resolver.resolveById(refContentId);
720                Object rawValue = getMetadataValue(refContent, StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), defaultLocale, resolveReferences);
721                return _transformValue(content, metadataPath, rawValue, null, defaultLocale);
722            }   
723        }
724        else
725        {
726            Object rawValue = _contentHelper.getSimpleMetadataValue(metadataHolder, definition, metadataName, defaultLocale, resolveReferences);
727            return _transformValue(content, metadataPath, rawValue, null, defaultLocale);
728        }
729    }
730
731    private Object _getCompositeRepeaterValues(Content content, CompositeMetadata metadataHolder, MetadataDefinition definition, Locale locale, boolean resolveReferences, String[] pathSegments, String prefix, String metadataName)
732    {
733        if (definition instanceof RepeaterDefinition)
734        {
735            return _getRepeaterRepeaterValue(content, metadataHolder, definition, locale, resolveReferences, pathSegments, prefix, metadataName);
736        }
737        else
738        {
739            // Composite.
740            CompositeMetadata subMetadataHolder = metadataHolder.getCompositeMetadata(metadataName);
741            if (pathSegments.length > 1)
742            {
743                MetadataDefinition subMetadataDef = definition.getMetadataDefinition(pathSegments[1]);
744                return _getRepeaterValues(content, subMetadataHolder, subMetadataDef, prefix, StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), locale, resolveReferences);
745            }
746            else
747            {
748                Map<String, Object> values = new HashMap<>();
749                for (String subMetadataName : subMetadataHolder.getMetadataNames())
750                {
751                    MetadataDefinition submetadataDefinition = definition.getMetadataDefinition(subMetadataName);
752                    Object entryValue = _getRepeaterValues(content, subMetadataHolder, submetadataDefinition, prefix, subMetadataName, locale, resolveReferences);
753                    if (entryValue != null)
754                    {
755                        if (submetadataDefinition != null && submetadataDefinition.getType().equals(MetadataType.COMPOSITE) && !(submetadataDefinition instanceof RepeaterDefinition) && entryValue instanceof Map)
756                        {
757                            // add and prefix each entry of the composite
758                            @SuppressWarnings("unchecked")
759                            Map<String, Object> map = (Map<String, Object>) entryValue;
760                            values.putAll(map.entrySet().stream().collect(Collectors.toMap(e -> metadataName + ContentConstants.METADATA_PATH_SEPARATOR + e.getKey(), e -> e.getValue())));
761                        }
762                        else
763                        {
764                            values.put(metadataName + ContentConstants.METADATA_PATH_SEPARATOR + subMetadataName, entryValue);
765                        }
766                    }
767                }
768                return values;
769            }
770        }
771    }
772
773    @SuppressWarnings("unchecked")
774    private Object _getRepeaterRepeaterValue(Content content, CompositeMetadata metadataHolder, MetadataDefinition definition, Locale locale, boolean resolveReferences, String[] pathSegments, String prefix, String metadataName)
775    {
776        // Repeater: get and sort the entry names.
777        CompositeMetadata repeater = metadataHolder.getCompositeMetadata(metadataName);
778        String[] entries = repeater.getMetadataNames();
779        Arrays.sort(entries, MetadataManager.REPEATER_ENTRY_COMPARATOR);
780        
781        
782        if (pathSegments.length > 1)
783        {
784            Map<String, Object> values = new HashMap<>();
785            for (String entryName : entries)
786            {
787                CompositeMetadata entry = repeater.getCompositeMetadata(entryName);
788                
789                Object entryValue = _getRepeaterValues(content, entry, definition.getMetadataDefinition(pathSegments[1]), prefix + ContentConstants.METADATA_PATH_SEPARATOR + entryName, StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), locale, resolveReferences);
790                if (entryValue != null)
791                {
792                    values.put(_i18nUtils.translate(((RepeaterDefinition) definition).getLabel(), locale.getLanguage()), entryValue);
793                }
794            }
795            
796            return values;
797        }
798        else
799        {
800            List<Map<String, Object>> values = new LinkedList<>();
801            
802            int i = 0;
803            for (String entryName : entries)
804            {
805                i++;
806                CompositeMetadata entry = repeater.getCompositeMetadata(entryName);
807                
808                Map<String, Object> compositeValues = new HashMap<>();
809                for (String subMetadataName : entry.getMetadataNames())
810                {
811                    MetadataDefinition submetadataDefinition = definition.getMetadataDefinition(subMetadataName);
812                    Object entryValue = _getRepeaterValues(content, entry, submetadataDefinition, prefix + ContentConstants.METADATA_PATH_SEPARATOR + entryName, subMetadataName, locale, resolveReferences);
813                    if (entryValue != null)
814                    {
815                        if (submetadataDefinition != null && submetadataDefinition.getType().equals(MetadataType.COMPOSITE) && !(submetadataDefinition instanceof RepeaterDefinition) && entryValue instanceof Map)
816                        {
817                            // if submetadata was a composite, add all composite child as a flat list of values 
818                            compositeValues.putAll((Map< ? extends String, ? extends Object>) entryValue);
819                        }
820                        else
821                        {
822                            compositeValues.put(subMetadataName, entryValue);
823                        }
824                    }
825                }
826                
827                Map<String, Object> repeaterValues = new HashMap<>();
828                repeaterValues.put("label", ((RepeaterDefinition) definition).getLabel());
829                repeaterValues.put("position", i);
830                repeaterValues.put("values", compositeValues);
831                String headerLabel = ((RepeaterDefinition) definition).getHeaderLabel();
832                if (headerLabel != null)
833                {
834                    repeaterValues.put("header-label", headerLabel);
835                }
836                values.add(repeaterValues);
837            }
838            
839            return values;
840        }
841    }
842}