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