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