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.ContentType;
036import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
037import org.ametys.cms.contenttype.ContentTypesHelper;
038import org.ametys.cms.repository.Content;
039import org.ametys.cms.search.model.ResultField;
040import org.ametys.cms.search.model.SearchModel;
041import org.ametys.cms.search.model.SystemProperty;
042import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
043import org.ametys.runtime.model.ModelItem;
044import org.ametys.runtime.model.ModelItemContainer;
045import org.ametys.runtime.model.ModelItemGroup;
046import org.ametys.runtime.plugin.component.AbstractLogEnabled;
047
048/**
049 * Component creating content values extractors from {@link SearchModel}s or content type IDs.
050 */
051public class ContentValuesExtractorFactory extends AbstractLogEnabled implements Component, Serviceable
052{
053    
054    /** The component role. */
055    public static final String ROLE = ContentValuesExtractorFactory.class.getName();
056    
057    /** The content type extension point. */
058    protected ContentTypeExtensionPoint _cTypeEP;
059    
060    /** The content type helper. */
061    protected ContentTypesHelper _cTypeHelper;
062    
063    /** The content type helper. */
064    protected ContentSearchHelper _searchHelper;
065    
066    /** The system property extension point. */
067    protected SystemPropertyExtensionPoint _sysPropEP;
068    
069    @Override
070    public void service(ServiceManager manager) throws ServiceException
071    {
072        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
073        _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
074        _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE);
075        _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
076    }
077    
078    /**
079     * Create a ContentValuesExtractor from a search model.
080     * @param searchModel The reference search model.
081     * @return a ContentValuesExtractor backed by the given search model.
082     */
083    public SearchModelContentValuesExtractor create(SearchModel searchModel)
084    {
085        return new SearchModelContentValuesExtractor(searchModel);
086    }
087    
088    /**
089     * Create a simple ContentValuesExtractor from a list of content types.
090     * @param contentTypes The content types to search on.
091     * @return a ContentValuesExtractor referencing the given content types.
092     */
093    public SimpleContentValuesExtractor create(Collection<String> contentTypes)
094    {
095        return create(contentTypes, Collections.emptyList());
096    }
097    
098    /**
099     * Create a simple ContentValuesExtractor from a list of content types.
100     * @param contentTypes The content types to search on.
101     * @param fields The fields to extract.
102     * @return a ContentValuesExtractor referencing the given content types.
103     */
104    public SimpleContentValuesExtractor create(Collection<String> contentTypes, List<String> fields)
105    {
106        return new SimpleContentValuesExtractor(contentTypes, fields);
107    }
108    
109    /**
110     * A ContentValuesExtractor backed by a {@link SearchModel}.
111     */
112    public class SearchModelContentValuesExtractor
113    {
114        private SearchModel _searchModel;
115        private boolean _fullValues;
116        
117        /**
118         * Build a ContentValuesExtractor referencing a {@link SearchModel}.
119         * @param searchModel the {@link SearchModel}.
120         */
121        public SearchModelContentValuesExtractor(SearchModel searchModel)
122        {
123            _searchModel = searchModel;
124            _fullValues = false;
125        }
126        
127        /**
128         * Whether to return full values or not.
129         * @param fullValues true to return full values, false otherwise.
130         * @return The ContentValuesExtractor itself.
131         */
132        public SearchModelContentValuesExtractor setFullValues(boolean fullValues)
133        {
134            _fullValues = fullValues;
135            return this;
136        }
137        
138        /**
139         * Get the values from the given content.
140         * @param content The content.
141         * @param defaultLocale The default locale for localized values if the content's language is null. Can be null.
142         * @return the extracted values.
143         */
144        public Map<String, Object> getValues(Content content, Locale defaultLocale)
145        {
146            return getValues(content, defaultLocale, Collections.emptyMap());
147        }
148        
149        /**
150         * Get the values from the given content.
151         * @param content The content.
152         * @param defaultLocale The default locale for localized values if the content's language is null. Can be null.
153         * @param contextualParameters The search contextual parameters.
154         * @return the extracted values.
155         */
156        public Map<String, Object> getValues(Content content, Locale defaultLocale, 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, defaultLocale) : field.getValue(content, defaultLocale);
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            if (CollectionUtils.isEmpty(fieldNames))
218            {
219                return getAllFields();
220            }
221            else
222            {
223                return getFields(fieldNames);
224            }
225        }
226        
227        /**
228         * Retrieves a {@link Map} with all possible fields
229         * @return A {@link Map} with all possible fields
230         */
231        protected Map<String, Object> getAllFields()
232        {
233            LinkedHashMap<String, Object> fields = new LinkedHashMap<>();
234            
235            if (_contentTypes.isEmpty())
236            {
237                // Add only title.
238                fields.put("title", _cTypeHelper.getTitleAttributeDefinition());
239            }
240            else
241            {
242                // Add all attributes.
243                for (String id : _contentTypes)
244                {
245                    fields.putAll(getAllAttributes(_cTypeEP.getExtension(id)));
246                }
247            }
248            
249            // Add system properties
250            for (String propName : _sysPropEP.getDisplayProperties())
251            {
252                fields.put(propName, _sysPropEP.getExtension(propName));
253            }
254            
255            return fields;
256        }
257        
258        /**
259         * Retrieves a {@link Map} with the fields corresponding to the given name
260         * @param fieldNames The names of the fields to retrieve
261         * @return a {@link Map} with the fields
262         */
263        protected Map<String, Object> getFields(List<String> fieldNames)
264        {
265            LinkedHashMap<String, Object> fields = new LinkedHashMap<>();
266            
267            for (String fieldName : fieldNames)
268            {
269                if (_sysPropEP.hasExtension(fieldName))
270                {
271                    if (_sysPropEP.isDisplayable(fieldName))
272                    {
273                        fields.put(fieldName, _sysPropEP.getExtension(fieldName));
274                    }
275                    else
276                    {
277                        throw new IllegalArgumentException("The system property '" + fieldName + "' is not displayable.");
278                    }
279                }
280                else
281                {
282                    String modelItemPath = fieldName;
283                    
284                    Iterator<String> ids = _contentTypes.iterator();
285                    ModelItem modelItem = null;
286                    while (ids.hasNext() && modelItem == null)
287                    {
288                        ContentType cType = _cTypeEP.getExtension(ids.next());
289                        if (cType.hasModelItem(modelItemPath))
290                        {
291                            modelItem = cType.getModelItem(modelItemPath);
292                        }
293                    }
294                    
295                    // Take the standard title metadata definition if no specific content type is defined.
296                    if (modelItem == null && fieldName.equals(Content.ATTRIBUTE_TITLE) && _contentTypes.isEmpty())
297                    {
298                        modelItem = _cTypeHelper.getTitleAttributeDefinition();
299                    }
300                    
301                    if (modelItem != null)
302                    {
303                        fields.put(fieldName, modelItem);
304                    }
305                    else
306                    {
307                        throw new IllegalArgumentException("The field '" + fieldName + "' can't be found in the given content types.");
308                    }
309                }
310            }
311            
312            return fields;
313        }
314        
315        /**
316         * Retrieve all the attributes present in the given container.
317         * @param container The model item container.
318         * @return All the attributes present in the given container.
319         */
320        protected Map<String, Object> getAllAttributes(ModelItemContainer container)
321        {
322            LinkedHashMap<String, Object> fields = new LinkedHashMap<>();
323            
324            for (ModelItem definition : container.getModelItems())
325            {
326                if (definition instanceof ModelItemGroup)
327                {
328                    // Composite or repeater: add nothing at this level and recurse.
329                    fields.putAll(getAllAttributes((ModelItemGroup) definition));
330                }
331                else
332                {
333                    // Add the attribute to the fields map.
334                    fields.put(definition.getPath(), definition);
335                }
336            }
337            
338            return fields;
339        }
340        
341        /**
342         * Whether to return full values or not.
343         * @param fullValues true to return full values, false otherwise.
344         * @return The ContentValuesExtractor itself.
345         */
346        public SimpleContentValuesExtractor setFullValues(boolean fullValues)
347        {
348            _fullValues = fullValues;
349            return this;
350        }
351        
352        /**
353         * Get the values from the given content.
354         * @param content The content.
355         * @param defaultLocale The default locale for localized values if the content's language is null. Can be null.
356         * @return the extracted values.
357         */
358        public Map<String, Object> getValues(Content content, Locale defaultLocale)
359        {
360            return getValues(content, defaultLocale, Collections.emptyMap());
361        }
362        
363        /**
364         * Get the values from the given content.
365         * @param content The content.
366         * @param defaultLocale The default locale for localized values if the content's language is null. Can be null.
367         * @param contextualParameters The search contextual parameters.
368         * @return the extracted values.
369         */
370        public Map<String, Object> getValues(Content content, Locale defaultLocale, 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, defaultLocale);
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         * @param defaultLocale The default locale for localized values if the content's language is null. Can be null.
394         * @return The value.
395         */
396        protected Object getValue(Content content, String fieldName, Object field, Locale defaultLocale)
397        {
398            Object value = null;
399            
400            if (field instanceof SystemProperty)
401            {
402                value = ((SystemProperty) field).getJsonValue(content, _fullValues);
403            }
404            else if (field instanceof ModelItem)
405            {
406                value = _searchHelper.getAttributeValue(content, fieldName, (ModelItem) field, defaultLocale, _fullValues);
407            }
408            
409            return value;
410        }
411    }
412}