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.util.Collection;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.Iterator;
023import java.util.LinkedHashMap;
024import java.util.List;
025import java.util.Locale;
026import java.util.Map;
027import java.util.Set;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.commons.collections.CollectionUtils;
034
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.MetadataDefinitionHolder;
041import org.ametys.cms.contenttype.MetadataType;
042import org.ametys.cms.contenttype.RepeaterDefinition;
043import org.ametys.cms.repository.Content;
044import org.ametys.cms.search.model.ResultField;
045import org.ametys.cms.search.model.SearchModel;
046import org.ametys.cms.search.model.SystemProperty;
047import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
048import org.ametys.runtime.parameter.Enumerator;
049import org.ametys.runtime.plugin.component.AbstractLogEnabled;
050
051/**
052 * Component creating content values extractors from {@link SearchModel}s or content type IDs.
053 */
054public class ContentValuesExtractorFactory extends AbstractLogEnabled implements Component, Serviceable
055{
056    
057    /** The component role. */
058    public static final String ROLE = ContentValuesExtractorFactory.class.getName();
059    
060    /** The content type extension point. */
061    protected ContentTypeExtensionPoint _cTypeEP;
062    
063    /** The content type helper. */
064    protected ContentTypesHelper _cTypeHelper;
065    
066    /** The content type helper. */
067    protected ContentSearchHelper _searchHelper;
068    
069    /** The system property extension point. */
070    protected SystemPropertyExtensionPoint _sysPropEP;
071    
072    @Override
073    public void service(ServiceManager manager) throws ServiceException
074    {
075        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
076        _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
077        _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE);
078        _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
079    }
080    
081    /**
082     * Create a ContentValuesExtractor from a search model.
083     * @param searchModel The reference search model.
084     * @return a ContentValuesExtractor backed by the given search model.
085     */
086    public SearchModelContentValuesExtractor create(SearchModel searchModel)
087    {
088        return new SearchModelContentValuesExtractor(searchModel);
089    }
090    
091    /**
092     * Create a simple ContentValuesExtractor from a list of content types.
093     * @param contentTypes The content types to search on.
094     * @return a ContentValuesExtractor referencing the given content types.
095     */
096    public SimpleContentValuesExtractor create(Collection<String> contentTypes)
097    {
098        return create(contentTypes, Collections.emptyList());
099    }
100    
101    /**
102     * Create a simple ContentValuesExtractor from a list of content types.
103     * @param contentTypes The content types to search on.
104     * @param fields The fields to extract.
105     * @return a ContentValuesExtractor referencing the given content types.
106     */
107    public SimpleContentValuesExtractor create(Collection<String> contentTypes, List<String> fields)
108    {
109        return new SimpleContentValuesExtractor(contentTypes, fields);
110    }
111    
112    /**
113     * A ContentValuesExtractor backed by a {@link SearchModel}.
114     */
115    public class SearchModelContentValuesExtractor
116    {
117        private SearchModel _searchModel;
118        private boolean _fullValues;
119        
120        /**
121         * Build a ContentValuesExtractor referencing a {@link SearchModel}.
122         * @param searchModel the {@link SearchModel}.
123         */
124        public SearchModelContentValuesExtractor(SearchModel searchModel)
125        {
126            _searchModel = searchModel;
127            _fullValues = false;
128        }
129        
130        /**
131         * Whether to return full values or not.
132         * @param fullValues true to return full values, false otherwise.
133         * @return The ContentValuesExtractor itself.
134         */
135        public SearchModelContentValuesExtractor setFullValues(boolean fullValues)
136        {
137            _fullValues = fullValues;
138            return this;
139        }
140        
141        /**
142         * Get the values from the given content.
143         * @param content The content.
144         * @param defaultLocale The default locale for localized values if the content's language is null. Can be null.
145         * @return the extracted values.
146         */
147        public Map<String, Object> getValues(Content content, Locale defaultLocale)
148        {
149            return getValues(content, defaultLocale, Collections.emptyMap());
150        }
151        
152        /**
153         * Get the values from the given content.
154         * @param content The content.
155         * @param defaultLocale The default locale for localized values if the content's language is null. Can be null.
156         * @param contextualParameters The search contextual parameters.
157         * @return the extracted values.
158         */
159        public Map<String, Object> getValues(Content content, Locale defaultLocale, Map<String, Object> contextualParameters)
160        {
161            Map<String, Object> properties = new HashMap<>();
162            
163            Map<String, ? extends ResultField> resultFields = _searchModel.getResultFields(contextualParameters);
164            
165            for (ResultField field : resultFields.values())
166            {
167                Object value = _fullValues ? field.getFullValue(content, defaultLocale) : field.getValue(content, defaultLocale);
168                
169                if (value != null)
170                {
171                    putPropertyValue(properties, field, value);
172                }
173            }
174            
175            return properties;
176        }
177        
178        /**
179         * Put a result value at its right place in the properties map.
180         * @param properties the properties map to fill.
181         * @param column the search column.
182         * @param value the result value.
183         */
184        protected void putPropertyValue(Map<String, Object> properties, ResultField column, Object value)
185        {
186            String id = column.getId();
187            properties.put(id.replace('.', '/'), value);  
188        }
189        
190    }
191    
192    /**
193     * A simple ContentValuesExtractor on a list of content types.
194     */
195    public class SimpleContentValuesExtractor
196    {
197        private Set<String> _contentTypes;
198        private Map<String, Object> _fields;
199        private boolean _fullValues;
200        
201        /**
202         * Build a simple ContentValuesExtractor on a list of content types.
203         * @param contentTypes The content types, can be empty.
204         * @param fieldNames The field names.
205         */
206        public SimpleContentValuesExtractor(Collection<String> contentTypes, List<String> fieldNames)
207        {
208            this._contentTypes = contentTypes != null ? new HashSet<>(contentTypes) : Collections.emptySet();
209            this._fields = initializeFields(fieldNames);
210            this._fullValues = false;
211        }
212        
213        /**
214         * Initialize the fields from the field names.
215         * @param fieldNames The field names.
216         * @return The fields definitions (either MetadataDefinition or SystemProperty), indexed by field name.
217         */
218        protected Map<String, Object> initializeFields(List<String> fieldNames)
219        {
220            LinkedHashMap<String, Object> fields = new LinkedHashMap<>();
221            
222            if (CollectionUtils.isEmpty(fieldNames))
223            {
224                addAllFields(fields);
225            }
226            else
227            {
228                addFields(fields, fieldNames);
229            }
230            
231            return fields;
232        }
233        
234        /**
235         * Add all possible fields to the map.
236         * @param fields The map of field definitions to fill.
237         */
238        protected void addAllFields(Map<String, Object> fields)
239        {
240            // Add all metadatas
241            if (_contentTypes.isEmpty())
242            {
243                // Add only title.
244                fields.put("title", new MetadataReference(ContentTypesHelper.getTitleMetadataDefinition(), false));
245            }
246            else
247            {
248                // Add all metadatas.
249                for (String id : _contentTypes)
250                {
251                    addAllMetadatas(fields, _cTypeEP.getExtension(id), "", false);
252                }
253            }
254            
255            // Add system properties
256            for (String propName : _sysPropEP.getDisplayProperties())
257            {
258                fields.put(propName, _sysPropEP.getExtension(propName));
259            }
260        }
261        
262        /**
263         * Add the given fields to the map.
264         * @param fields The map of field definitions to fill.
265         * @param fieldNames The fields to add.
266         */
267        protected void addFields(Map<String, Object> fields, List<String> fieldNames)
268        {
269            for (String field : fieldNames)
270            {
271                if (_sysPropEP.hasExtension(field))
272                {
273                    if (_sysPropEP.isDisplayable(field))
274                    {
275                        fields.put(field, _sysPropEP.getExtension(field));
276                    }
277                    else
278                    {
279                        throw new IllegalArgumentException("The system property '" + field + "' is not displayable.");
280                    }
281                }
282                else
283                {
284                    String metadataPath = field;
285                    
286                    Iterator<String> ids = _contentTypes.iterator();
287                    MetadataReference metaRef = null;
288                    while (ids.hasNext() && metaRef == null)
289                    {
290                        ContentType cType = _cTypeEP.getExtension(ids.next());
291                        List<MetadataDefinition> metaDefs = _cTypeHelper.getMetadataDefinitionPath(metadataPath, cType);
292                        if (CollectionUtils.isNotEmpty(metaDefs))
293                        {
294                            MetadataDefinition metaDef = metaDefs.get(metaDefs.size() - 1);
295                            metaRef = new MetadataReference(metaDef, isMultiple(metaDefs));
296                        }
297                    }
298                    
299                    // Take the standard title metadata definition if no specific content type is defined.
300                    if (metaRef == null && field.equals("title") && _contentTypes.isEmpty())
301                    {
302                        metaRef = new MetadataReference(ContentTypesHelper.getTitleMetadataDefinition(), false);
303                    }
304                    
305                    if (metaRef != null)
306                    {
307                        fields.put(field, metaRef);
308                    }
309                    else
310                    {
311                        throw new IllegalArgumentException("The field '" + field + "' can't be found in the given content types.");
312                    }
313                }
314            }
315        }
316        
317        /**
318         * Add all the metadata present in a holder.
319         * @param fields The field map to fill.
320         * @param defHolder The definition holder.
321         * @param prefix The metadata path prefix.
322         * @param parentIsMultiple true if the parent is multiple.
323         */
324        protected void addAllMetadatas(Map<String, Object> fields, MetadataDefinitionHolder defHolder, String prefix, boolean parentIsMultiple)
325        {
326            for (String name : defHolder.getMetadataNames())
327            {
328                MetadataDefinition definition = defHolder.getMetadataDefinition(name);
329                String path = prefix + name;
330                // The metadata is considered multiple if any of its parent is multiple or it's itself multiple.
331                boolean multiple = parentIsMultiple || definition.isMultiple() || definition instanceof RepeaterDefinition;
332                
333                if (definition.getType() == MetadataType.COMPOSITE)
334                {
335                    // Composite or repeater: add nothing at this level and recurse.
336                    addAllMetadatas(fields, definition, path + ContentConstants.METADATA_PATH_SEPARATOR, multiple);
337                }
338                else if (!fields.containsKey(path))
339                {
340                    // Add the metadata to the field map (if it's not present already).
341                    fields.put(path, new MetadataReference(definition, multiple));
342                }
343            }
344        }
345        
346        /**
347         * Whether to return full values or not.
348         * @param fullValues true to return full values, false otherwise.
349         * @return The ContentValuesExtractor itself.
350         */
351        public SimpleContentValuesExtractor setFullValues(boolean fullValues)
352        {
353            _fullValues = fullValues;
354            return this;
355        }
356        
357        /**
358         * Get the values from the given content.
359         * @param content The content.
360         * @param defaultLocale The default locale for localized values if the content's language is null. Can be null.
361         * @return the extracted values.
362         */
363        public Map<String, Object> getValues(Content content, Locale defaultLocale)
364        {
365            return getValues(content, defaultLocale, Collections.emptyMap());
366        }
367        
368        /**
369         * Get the values from the given content.
370         * @param content The content.
371         * @param defaultLocale The default locale for localized values if the content's language is null. Can be null.
372         * @param contextualParameters The search contextual parameters.
373         * @return the extracted values.
374         */
375        public Map<String, Object> getValues(Content content, Locale defaultLocale, Map<String, Object> contextualParameters)
376        {
377            Map<String, Object> properties = new LinkedHashMap<>();
378            
379            for (String fieldName : _fields.keySet())
380            {
381                Object field = _fields.get(fieldName);
382                
383                Object value = getValue(content, fieldName, field, defaultLocale);
384                if (value != null)
385                {
386                    properties.put(fieldName, value);
387                }
388            }
389            
390            return properties;
391        }
392        
393        /**
394         * Get a value from the Content.
395         * @param content The content.
396         * @param fieldName The field name.
397         * @param field The field definition.
398         * @param defaultLocale The default locale for localized values if the content's language is null. Can be null.
399         * @return The value.
400         */
401        protected Object getValue(Content content, String fieldName, Object field, Locale defaultLocale)
402        {
403            Object value = null;
404            
405            if (field instanceof SystemProperty)
406            {
407                value = ((SystemProperty) field).getJsonValue(content, _fullValues);
408            }
409            else if (field instanceof MetadataReference)
410            {
411                String metadataPath = fieldName;
412                MetadataReference metaRef = (MetadataReference) field;
413                value = _searchHelper.getMetadataValue(content, metadataPath, metaRef.getType(), metaRef.isMultiple(), metaRef.getEnumerator(), defaultLocale, _fullValues);
414            }
415            
416            return value;
417        }
418
419        /**
420         * Test if any metadata definition in a chain is multiple or a repeater.
421         * @param metaDefs The metadata definition chain.
422         * @return <code>true</code> if any metadata definition in a chain is multiple or a repeater.
423         */
424        protected boolean isMultiple(List<MetadataDefinition> metaDefs)
425        {
426            for (MetadataDefinition def : metaDefs)
427            {
428                if (def.isMultiple() || def instanceof RepeaterDefinition)
429                {
430                    return true;
431                }
432            }
433            
434            return false;
435        }
436        
437    }
438    
439    /**
440     * Represents a metadata definition and a multiple status, which can be different
441     * from the metadata definition's own multiple status.
442     */
443    class MetadataReference
444    {
445        private MetadataDefinition _definition;
446        
447        private boolean _multiple;
448        
449        /**
450         * Build a MetadataReference object.
451         * @param definition The reference MetadataDefinition.
452         * @param multiple The multiple status.
453         */
454        public MetadataReference(MetadataDefinition definition, boolean multiple)
455        {
456            this._definition = definition;
457            this._multiple = multiple;
458        }
459        
460        /**
461         * Get the reference MetadataDefinition.
462         * @return the reference MetadataDefinition.
463         */
464        public MetadataDefinition getDefinition()
465        {
466            return _definition;
467        }
468        
469        /**
470         * Get the multiple status.
471         * @return The multiple status.
472         */
473        public boolean isMultiple()
474        {
475            return _multiple;
476        }
477        
478        /**
479         * Get the metadata type.
480         * @return the metadata type.
481         */
482        public MetadataType getType()
483        {
484            return _definition.getType();
485        }
486        
487        /**
488         * Get the metadata enumerator.
489         * @return the metadata enumerator.
490         */
491        public Enumerator getEnumerator()
492        {
493            return _definition.getEnumerator();
494        }
495    }
496}