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.MetadataResultField;
040import org.ametys.cms.search.model.ResultField;
041import org.ametys.cms.search.model.SearchModel;
042import org.ametys.cms.search.model.SystemProperty;
043import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
044import org.ametys.plugins.repository.data.external.ExternalizableDataProviderExtensionPoint;
045import org.ametys.plugins.repository.model.RepeaterDefinition;
046import org.ametys.runtime.model.ModelItem;
047import org.ametys.runtime.model.ModelItemContainer;
048import org.ametys.runtime.model.ModelItemGroup;
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    /** To determine the externalizable status */
073    protected ExternalizableDataProviderExtensionPoint _externalizableDataProviderEP;
074    
075    @Override
076    public void service(ServiceManager manager) throws ServiceException
077    {
078        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
079        _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
080        _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE);
081        _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
082        _externalizableDataProviderEP = (ExternalizableDataProviderExtensionPoint) manager.lookup(ExternalizableDataProviderExtensionPoint.ROLE);
083    }
084    
085    /**
086     * Create a ContentValuesExtractor from a search model.
087     * @param searchModel The reference search model.
088     * @return a ContentValuesExtractor backed by the given search model.
089     */
090    public SearchModelContentValuesExtractor create(SearchModel searchModel)
091    {
092        return new SearchModelContentValuesExtractor(searchModel);
093    }
094    
095    /**
096     * Create a simple ContentValuesExtractor from a list of content types.
097     * @param contentTypes The content types to search on.
098     * @return a ContentValuesExtractor referencing the given content types.
099     */
100    public SimpleContentValuesExtractor create(Collection<String> contentTypes)
101    {
102        return create(contentTypes, Collections.emptyList());
103    }
104    
105    /**
106     * Create a simple ContentValuesExtractor from a list of content types.
107     * @param contentTypes The content types to search on.
108     * @param fields The fields to extract.
109     * @return a ContentValuesExtractor referencing the given content types.
110     */
111    public SimpleContentValuesExtractor create(Collection<String> contentTypes, List<String> fields)
112    {
113        return new SimpleContentValuesExtractor(contentTypes, fields);
114    }
115    
116    /**
117     * A ContentValuesExtractor 
118     */
119    public interface ContentValuesExtractor
120    {
121        /**
122         * Whether to return full values or not.
123         * @param fullValues true to return full values, false otherwise.
124         * @return The ContentValuesExtractor itself.
125         */
126        public ContentValuesExtractor setFullValues(boolean fullValues);
127        
128        /**
129         * Get the values from the given content.
130         * @param content The content.
131         * @param defaultLocale The default locale for localized values if the content's language is null. Can be null.
132         * @return the extracted values.
133         */
134        public Map<String, Object> getValues(Content content, Locale defaultLocale);
135        
136        /**
137         * Get the values from the given content.
138         * @param content The content.
139         * @param defaultLocale The default locale for localized values if the content's language is null. Can be null.
140         * @param contextualParameters The search contextual parameters.
141         * @return the extracted values.
142         */
143        public Map<String, Object> getValues(Content content, Locale defaultLocale, Map<String, Object> contextualParameters);
144    }
145    
146    /**
147     * A ContentValuesExtractor backed by a {@link SearchModel}.
148     */
149    public class SearchModelContentValuesExtractor implements ContentValuesExtractor
150    {
151        private SearchModel _searchModel;
152        private boolean _fullValues;
153        
154        /**
155         * Build a ContentValuesExtractor referencing a {@link SearchModel}.
156         * @param searchModel the {@link SearchModel}.
157         */
158        public SearchModelContentValuesExtractor(SearchModel searchModel)
159        {
160            _searchModel = searchModel;
161            _fullValues = false;
162        }
163        
164        public SearchModelContentValuesExtractor setFullValues(boolean fullValues)
165        {
166            _fullValues = fullValues;
167            return this;
168        }
169        
170        public Map<String, Object> getValues(Content content, Locale defaultLocale)
171        {
172            return getValues(content, defaultLocale, Collections.emptyMap());
173        }
174        
175        public Map<String, Object> getValues(Content content, Locale defaultLocale, Map<String, Object> contextualParameters)
176        {
177            Map<String, Object> properties = new HashMap<>();
178            
179            Map<String, ? extends ResultField> resultFields = _searchModel.getResultFields(contextualParameters);
180            
181            boolean handleExternalizable = (boolean) contextualParameters.getOrDefault("externalizable", true);
182            
183            for (ResultField field : resultFields.values())
184            {
185                Object value = _fullValues ? field.getFullValue(content, defaultLocale) : field.getValue(content, defaultLocale);
186                
187                if (handleExternalizable && field instanceof MetadataResultField)
188                {
189                    String fieldName = ((MetadataResultField) field).getFieldPath();
190                    
191                    value = _handleExternalizable(value, content, fieldName);
192                }
193                
194                if (value != null)
195                {
196                    putPropertyValue(properties, field, value);
197                }
198            }
199            
200            return properties;
201        }
202        
203        /**
204         * Put a result value at its right place in the properties map.
205         * @param properties the properties map to fill.
206         * @param column the search column.
207         * @param value the result value.
208         */
209        protected void putPropertyValue(Map<String, Object> properties, ResultField column, Object value)
210        {
211            String id = column.getId();
212            properties.put(id.replace('.', '/'), value);  
213        }
214        
215    }
216    
217    /**
218     * A simple ContentValuesExtractor on a list of content types.
219     */
220    public class SimpleContentValuesExtractor implements ContentValuesExtractor
221    {
222        private Set<String> _contentTypes;
223        private Map<String, Object> _fields;
224        private boolean _fullValues;
225        
226        /**
227         * Build a simple ContentValuesExtractor on a list of content types.
228         * @param contentTypes The content types, can be empty.
229         * @param fieldNames The field names.
230         */
231        public SimpleContentValuesExtractor(Collection<String> contentTypes, List<String> fieldNames)
232        {
233            this._contentTypes = contentTypes != null ? new HashSet<>(contentTypes) : Collections.emptySet();
234            this._fields = initializeFields(fieldNames);
235            this._fullValues = false;
236        }
237        
238        /**
239         * Initialize the fields from the field names.
240         * @param fieldNames The field names.
241         * @return The fields definitions (either MetadataDefinition or SystemProperty), indexed by field name.
242         */
243        protected Map<String, Object> initializeFields(List<String> fieldNames)
244        {
245            if (CollectionUtils.isEmpty(fieldNames))
246            {
247                return getAllFields();
248            }
249            else
250            {
251                return getFields(fieldNames);
252            }
253        }
254        
255        /**
256         * Retrieves a {@link Map} with all possible fields
257         * @return A {@link Map} with all possible fields
258         */
259        protected Map<String, Object> getAllFields()
260        {
261            LinkedHashMap<String, Object> fields = new LinkedHashMap<>();
262            
263            if (_contentTypes.isEmpty())
264            {
265                // Add only title.
266                fields.put("title", _cTypeHelper.getTitleAttributeDefinition());
267            }
268            else
269            {
270                // Add all attributes.
271                for (String id : _contentTypes)
272                {
273                    fields.putAll(getAllAttributes(_cTypeEP.getExtension(id)));
274                }
275            }
276            
277            // Add system properties
278            for (String propName : _sysPropEP.getDisplayProperties())
279            {
280                fields.put(propName, _sysPropEP.getExtension(propName));
281            }
282            
283            return fields;
284        }
285        
286        /**
287         * Retrieves a {@link Map} with the fields corresponding to the given name
288         * @param fieldNames The names of the fields to retrieve
289         * @return a {@link Map} with the fields
290         */
291        protected Map<String, Object> getFields(List<String> fieldNames)
292        {
293            LinkedHashMap<String, Object> fields = new LinkedHashMap<>();
294            
295            for (String fieldName : fieldNames)
296            {
297                if (_sysPropEP.hasExtension(fieldName))
298                {
299                    if (_sysPropEP.isDisplayable(fieldName))
300                    {
301                        fields.put(fieldName, _sysPropEP.getExtension(fieldName));
302                    }
303                    else
304                    {
305                        throw new IllegalArgumentException("The system property '" + fieldName + "' is not displayable.");
306                    }
307                }
308                else
309                {
310                    String modelItemPath = fieldName;
311                    
312                    Iterator<String> ids = _contentTypes.iterator();
313                    ModelItem modelItem = null;
314                    while (ids.hasNext() && modelItem == null)
315                    {
316                        ContentType cType = _cTypeEP.getExtension(ids.next());
317                        if (cType.hasModelItem(modelItemPath))
318                        {
319                            modelItem = cType.getModelItem(modelItemPath);
320                        }
321                    }
322                    
323                    // Take the standard title metadata definition if no specific content type is defined.
324                    if (modelItem == null && fieldName.equals(Content.ATTRIBUTE_TITLE) && _contentTypes.isEmpty())
325                    {
326                        modelItem = _cTypeHelper.getTitleAttributeDefinition();
327                    }
328                    
329                    if (modelItem != null)
330                    {
331                        fields.put(fieldName, modelItem);
332                    }
333                    else
334                    {
335                        throw new IllegalArgumentException("The field '" + fieldName + "' can't be found in the given content types.");
336                    }
337                }
338            }
339            
340            return fields;
341        }
342        
343        /**
344         * Retrieve all the attributes present in the given container.
345         * @param container The model item container.
346         * @return All the attributes present in the given container.
347         */
348        protected Map<String, Object> getAllAttributes(ModelItemContainer container)
349        {
350            LinkedHashMap<String, Object> fields = new LinkedHashMap<>();
351            
352            for (ModelItem definition : container.getModelItems())
353            {
354                if (definition instanceof ModelItemGroup)
355                {
356                    // Composite or repeater: add nothing at this level and recurse.
357                    fields.putAll(getAllAttributes((ModelItemGroup) definition));
358                }
359                else
360                {
361                    // Add the attribute to the fields map.
362                    fields.put(definition.getPath(), definition);
363                }
364            }
365            
366            return fields;
367        }
368        
369        public SimpleContentValuesExtractor setFullValues(boolean fullValues)
370        {
371            _fullValues = fullValues;
372            return this;
373        }
374        
375        public Map<String, Object> getValues(Content content, Locale defaultLocale)
376        {
377            return getValues(content, defaultLocale, Collections.emptyMap());
378        }
379        
380        public Map<String, Object> getValues(Content content, Locale defaultLocale, Map<String, Object> contextualParameters)
381        {
382            Map<String, Object> properties = new LinkedHashMap<>();
383            
384            for (String fieldName : _fields.keySet())
385            {
386                Object field = _fields.get(fieldName);
387                
388                Object value = getValue(content, fieldName, field, defaultLocale, (boolean) contextualParameters.getOrDefault("externalizable", true));
389                if (value != null)
390                {
391                    properties.put(fieldName, value);
392                }
393            }
394            
395            return properties;
396        }
397        
398        /**
399         * Get a value from the Content.
400         * @param content The content.
401         * @param fieldName The field name.
402         * @param field The field definition.
403         * @param defaultLocale The default locale for localized values if the content's language is null. Can be null.
404         * @param handleExternalizable false to simply get current value on externalizable, true to get a json object with current value and status
405         * @return The value.
406         */
407        protected Object getValue(Content content, String fieldName, Object field, Locale defaultLocale, boolean handleExternalizable)
408        {
409            Object value = null;
410            
411            if (field instanceof SystemProperty)
412            {
413                value = ((SystemProperty) field).getJsonValue(content, _fullValues);
414            }
415            else if (field instanceof ModelItem)
416            {
417                value = _searchHelper.getAttributeValue(content, fieldName, (ModelItem) field, defaultLocale, _fullValues);
418                
419                if (handleExternalizable)
420                {
421                    value = _handleExternalizable(value, content, fieldName);
422                }
423            }
424            
425            return value;
426        }
427    }
428    
429    @SuppressWarnings("unchecked")
430    private Object _handleExternalizable(Object value, Content content, String fieldName)
431    {
432        ModelItem definition = content.getDefinition(fieldName);
433        if (definition instanceof RepeaterDefinition)
434        {
435            RepeaterDefinition repeaterDefinition = (RepeaterDefinition) definition;
436            
437            Map<String, Object> repeaterValue = (Map<String, Object>) value;
438            if (repeaterValue != null)
439            {
440                List<Map<String, Object>> entries = (List<Map<String, Object>>) repeaterValue.get("entries");
441                for (int i = 0; i < entries.size(); i++)
442                {
443                    Map<String, Object> entry = entries.get(i);
444                    
445                    Map<String, Object> entryValues = (Map<String, Object>) entry.get("values");
446                    
447                    for (ModelItem modelItem : repeaterDefinition.getChildren())
448                    {
449                        String subFieldName = modelItem.getName();
450                        Object newValue = _handleExternalizable(entryValues.get(subFieldName), content, fieldName + "[" + (i + 1) + "]/" + subFieldName);
451                        entryValues.put(subFieldName, newValue);
452                    }
453                }
454            }
455            
456            return value;
457        }
458        else if (_externalizableDataProviderEP.isDataExternalizable(content, definition))
459        {
460            Map<String, Object> json = new HashMap<>();
461            json.put("value", value);
462            json.put("status", content.getStatus(fieldName).toString().toLowerCase());
463            return json;
464        }
465        else
466        {
467            return value;
468        }
469    }
470}