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