001/*
002 *  Copyright 2013 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.ui.model;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.LinkedHashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.context.Context;
028import org.apache.avalon.framework.context.ContextException;
029import org.apache.avalon.framework.context.Contextualizable;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.cocoon.ProcessingException;
034import org.apache.commons.collections.MapUtils;
035
036import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
037import org.ametys.cms.search.model.SearchModel;
038import org.ametys.cms.search.model.SystemSearchCriterion;
039import org.ametys.cms.search.ui.model.impl.SystemSearchUICriterion;
040import org.ametys.core.ui.Callable;
041import org.ametys.runtime.i18n.I18nizableText;
042import org.ametys.runtime.parameter.Enumerator;
043import org.ametys.runtime.parameter.ParameterHelper;
044import org.ametys.runtime.parameter.Validator;
045import org.ametys.runtime.plugin.component.AbstractLogEnabled;
046
047/**
048 * Client interaction helper for {@link SearchUIModel}.
049 */
050public class SearchUIModelHelper extends AbstractLogEnabled implements Component, Serviceable, Contextualizable
051{
052    /** The component role. */
053    public static final String ROLE = SearchUIModelHelper.class.getName();
054    
055    private SearchUIModelExtensionPoint _searchModelManager;
056    private ServiceManager _serviceManager;
057    private ContentTypeExtensionPoint _cTypeEP;
058    private Context _context;
059    
060    
061    @Override
062    public void service(ServiceManager smanager) throws ServiceException
063    {
064        _serviceManager = smanager;
065        _searchModelManager = (SearchUIModelExtensionPoint) smanager.lookup(SearchUIModelExtensionPoint.ROLE);
066        _cTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
067    }
068    
069    @Override
070    public void contextualize(Context context) throws ContextException
071    {
072        _context = context;
073    }
074    
075    /**
076     * Get all the real content types that a model works on (the included content types, minus the excluded types).
077     * @param model the search model.
078     * @param contextualParameters the contextual parameters.
079     * @return a Set of the content type IDs.
080     */
081    public Set<String> getAllContentTypes(SearchModel model, Map<String, Object> contextualParameters)
082    {
083        Set<String> allContentTypes = new HashSet<>();
084        
085        Set<String> modelCTypes = model.getContentTypes(contextualParameters);
086        Set<String> modelExcludedCTypes = model.getExcludedContentTypes(contextualParameters);
087        
088        if (modelCTypes.isEmpty())
089        {
090            // Empty "declared" content types: the model works on all content types.
091            allContentTypes.addAll(_cTypeEP.getExtensionsIds());
092        }
093        else
094        {
095            // Otherwise, add all declared content types and their sub-types.
096            for (String cTypeId : modelCTypes)
097            {
098                allContentTypes.add(cTypeId);
099                allContentTypes.addAll(_cTypeEP.getSubTypes(cTypeId));
100            }
101        }
102        
103        // Remove all excluded types.
104        allContentTypes.removeAll(modelExcludedCTypes);
105        
106        return allContentTypes;
107    }
108    
109    /**
110     * Get the search model configuration as JSON object
111     * @param modelId The id of search model
112     * @param restrictedContentTypes The restricted content types. Can be null.
113     * @param contextualParameters the contextual parameters
114     * @return The search model configuration in a Map
115     * @throws ProcessingException if an error occurred
116     */
117    @Callable
118    public Map<String, Object> getSearchModelConfiguration(String modelId, List<String> restrictedContentTypes, Map<String, Object> contextualParameters) throws ProcessingException
119    {
120        SearchUIModel model = _searchModelManager.getExtension(modelId);
121        if (model == null)
122        {
123            throw new IllegalArgumentException("The search model '" + modelId + "' does not exists");
124        }
125        
126        // TODO Replace DynamicWrappedSearchUIModel?
127        if (restrictedContentTypes != null)
128        {
129            model = new DynamicWrappedSearchUIModel(model, restrictedContentTypes, _cTypeEP, getLogger(), _context, _serviceManager);
130        }
131        
132        return getSearchModelInfo(model, contextualParameters);
133        
134    }
135    
136    /**
137     * Return information on a {@link SearchUIModel} object serialized in a Map.
138     * @param model The search model.
139     * @param contextualParameters The contextual parameters
140     * @return the detailed information serialized in a Map.
141     * @throws ProcessingException if an error occurs.
142     */
143    public Map<String, Object> getSearchModelInfo(SearchUIModel model, Map<String, Object> contextualParameters) throws ProcessingException
144    {
145        Map<String, Object> jsonObject = new HashMap<>();
146        
147        jsonObject.put("pageSize", model.getPageSize(contextualParameters));
148        jsonObject.put("workspace", model.getWorkspace(contextualParameters));
149        jsonObject.put("searchUrl", model.getSearchUrl(contextualParameters));
150        jsonObject.put("searchUrlPlugin", model.getSearchUrlPlugin(contextualParameters));
151        jsonObject.put("exportCSVUrl", model.getExportCSVUrl(contextualParameters));
152        jsonObject.put("exportCSVUrlPlugin", model.getExportCSVUrlPlugin(contextualParameters));
153        jsonObject.put("exportXMLUrl", model.getExportXMLUrl(contextualParameters));
154        jsonObject.put("exportXMLUrlPlugin", model.getExportXMLUrlPlugin(contextualParameters));
155        jsonObject.put("printUrl", model.getPrintUrl(contextualParameters));
156        jsonObject.put("printUrlPlugin", model.getPrintUrlPlugin(contextualParameters));
157        
158        jsonObject.put("simple-criteria", getCriteriaListInfo(model.getCriteria(contextualParameters)));
159        jsonObject.put("advanced-criteria", getAdvancedCriteriaListInfo(model.getAdvancedCriteria(contextualParameters)));
160        jsonObject.put("columns", getColumnListInfo(model.getResultFields(contextualParameters)));
161        
162        jsonObject.put("hasFacets", !model.getFacetedCriteria(contextualParameters).isEmpty());
163        
164        return jsonObject;
165    }
166    
167    /**
168     * Return information on a list of {@link SearchUIColumn}, serialized as a Map.
169     * @param columns the list of search columns.
170     * @return the detailed information serialized in a Map.
171     * @throws ProcessingException if an error occurs.
172     */
173    public List<Object> getColumnListInfo(Map<String, ? extends SearchUIColumn> columns)  throws ProcessingException
174    {
175        List<Object> jsonObject = new ArrayList<>();
176        
177        for (SearchUIColumn column : columns.values())
178        {
179            jsonObject.add(getColumnInfo(column));
180        }
181        
182        return jsonObject;
183    }
184    
185    /**
186     * Return information on a {@link SearchUIColumn}, serialized as a Map.
187     * @param column the search column.
188     * @return the detailed information serialized in a Map.
189     * @throws ProcessingException if an error occurs.
190     */
191    public Map<String, Object> getColumnInfo(SearchUIColumn column) throws ProcessingException
192    {
193//        Map<String, Object> jsonObject = _parameter2JsonObject(column);
194        Map<String, Object> jsonObject = new HashMap<>();
195        
196        // TODO Why replace . ?
197        jsonObject.put("id", column.getId().replace('.', '/'));
198        
199        jsonObject.put("label", column.getLabel());
200        jsonObject.put("description", column.getDescription());
201        jsonObject.put("type", column.getType().name());
202        
203        jsonObject.put("hidden", column.isHidden());
204        jsonObject.put("renderer", column.getRenderer());
205        jsonObject.put("converter", column.getConverter());
206        jsonObject.put("width", column.getWidth());
207        jsonObject.put("editable", column.isEditable());
208        jsonObject.put("contentType", column.getContentTypeId());
209        jsonObject.put("sortable", column.isSortable());
210        jsonObject.put("defaultSorter", column.getDefaultSorter());
211        jsonObject.put("multiple", column.isMultiple());
212        
213        _putEnumerator(jsonObject, column.getEnumerator());
214        _putValidator(jsonObject, column.getValidator());
215        
216        String widget = column.getWidget();
217        if (widget != null)
218        {
219            jsonObject.put("widget", widget);
220        }
221        
222        Map<String, I18nizableText> widgetParameters = column.getWidgetParameters();
223        if (widgetParameters != null && !widgetParameters.isEmpty())
224        {
225            jsonObject.put("widget-params", column.getWidgetParameters());
226        }
227        
228        return jsonObject;
229    }
230    
231    /**
232     * Return information on a list of {@link SearchUICriterion}, serialized as a Map.
233     * @param criteria a map of search criteria.
234     * @return the detailed information serialized in a Map.
235     * @throws ProcessingException if an error occurs.
236     */
237    @SuppressWarnings("unchecked")
238    public Map<String, Object> getCriteriaListInfo(Map<String, ? extends SearchUICriterion> criteria) throws ProcessingException
239    {
240        Map<String, Object> jsonObject = new LinkedHashMap<>();
241        
242        // TODO Test
243        Map<I18nizableText, List<SearchUICriterion>> criteriaByGroup = _getCriteriaByGroup(criteria);
244//        Map<I18nizableText, List<SearchUICriterion>> criteriaByGroup = criteria.values().stream()
245//                .collect(Collectors.groupingBy(SearchUICriterion::getGroup));
246        
247        for (I18nizableText group : criteriaByGroup.keySet())
248        {
249            Map<String, Object> elements = new LinkedHashMap<>();
250            
251            for (SearchUICriterion sc : criteriaByGroup.get(group))
252            {
253                elements.put(sc.getId(), getCriterionInfo(sc));
254            }
255            
256            Map<String, Object> fieldset = new LinkedHashMap<>();
257            if (group != null)
258            {
259                fieldset.put("label", group);
260            }
261            fieldset.put("role", "fieldset");
262            fieldset.put("elements", elements);
263            
264            if (!jsonObject.containsKey("fieldsets"))
265            {
266                jsonObject.put("fieldsets", new ArrayList<Map<String, Object>>());
267            }
268            
269            ((List<Map<String, Object>>) jsonObject.get("fieldsets")).add(fieldset);
270            
271        }
272        return jsonObject;
273    }
274    
275    /**
276     * Return information on a list of advanced {@link SearchUICriterion}, serialized as a Map.
277     * @param criteria A map of advanced search criteria.
278     * @return the detailed information serialized in a Map.
279     * @throws ProcessingException if an error occurs.
280     */
281    public Map<String, Object> getAdvancedCriteriaListInfo(Map<String, ? extends SearchUICriterion> criteria) throws ProcessingException
282    {
283        // No groups for advanced search
284        Map<String, Object> jsonObject = new LinkedHashMap<>();
285        Map<String, Object> criteriaObject = new LinkedHashMap<>();
286        
287        for (SearchUICriterion sc : criteria.values())
288        {
289            if (sc instanceof SystemSearchCriterion)
290            {
291                SystemSearchUICriterion sysCrit = (SystemSearchUICriterion) sc;
292                if (sysCrit.getSystemPropertyId().equals("contentLanguage"))
293                {
294                    // Separate the language from the other criteria since it is mandatory
295                    jsonObject.put("language", getCriterionInfo(sc));
296                }
297                else
298                {
299                    criteriaObject.put(sc.getId(), getCriterionInfo(sc));
300                }
301            }
302            else
303            {
304                criteriaObject.put(sc.getId(), getCriterionInfo(sc));
305            }
306        }
307        
308        if (MapUtils.isNotEmpty(criteriaObject))
309        {
310            jsonObject.put("criteria", criteriaObject);
311        }
312        
313        return jsonObject;
314    }
315    
316    /**
317     * Return information on a {@link SearchUICriterion}, serialized as a Map.
318     * @param criterion a search criterion.
319     * @return the detailed information serialized in a Map.
320     * @throws ProcessingException if an error occurs.
321     */
322    public Map<String, Object> getCriterionInfo(SearchUICriterion criterion) throws ProcessingException
323    {
324        Map<String, Object> jsonObject = new HashMap<>();
325        
326        // TODO Parameter interface
327        _putCriterionParameter(jsonObject, criterion);
328        
329        jsonObject.put("multiple", criterion.isMultiple());
330        jsonObject.put("hidden", criterion.isHidden());
331        
332        String contentTypeId = criterion.getContentTypeId();
333        if (contentTypeId != null)
334        {
335            jsonObject.put("contentType", contentTypeId);
336        }
337        
338        jsonObject.put("criterionProperty", criterion.getFieldId());
339        // TODO use operator.getName()?
340        if (criterion.getOperator() != null)
341        {
342            jsonObject.put("criterionOperator", criterion.getOperator().toString().toLowerCase());
343        }
344        
345//        if (criterion.getValue() != null)
346//        {
347//            jsonObject.put("value", criterion.getValue());
348//        }
349        
350        return jsonObject;
351    }
352    
353    private Map<I18nizableText, List<SearchUICriterion>> _getCriteriaByGroup(Map<String, ? extends SearchUICriterion> criteria)
354    {
355        Map<I18nizableText, List<SearchUICriterion>> criteriaByGroup = new LinkedHashMap<>();
356        
357        for (SearchUICriterion sc : criteria.values())
358        {
359            I18nizableText group = sc.getGroup();
360            
361            if (!criteriaByGroup.containsKey(group))
362            {
363                criteriaByGroup.put(group, new ArrayList<SearchUICriterion>());
364            }
365            
366            criteriaByGroup.get(group).add(sc);
367        }
368        
369        return criteriaByGroup;
370    }
371    
372    private void _putCriterionParameter(Map<String, Object> jsonObject, SearchUICriterion criterion) throws ProcessingException
373    {
374        jsonObject.put("id", criterion.getId());
375        jsonObject.put("label", criterion.getLabel());
376        jsonObject.put("description", criterion.getDescription());
377        jsonObject.put("type", criterion.getType().name());
378        
379        Object defaultValue = criterion.getDefaultValue();
380        if (defaultValue != null)
381        {
382            jsonObject.put("default-value", defaultValue);
383        }
384        
385        _putEnumerator(jsonObject, criterion.getEnumerator());
386        _putValidator(jsonObject, criterion.getValidator());
387        
388        String widget = criterion.getWidget();
389        if (widget != null)
390        {
391            jsonObject.put("widget", widget);
392        }
393        
394        Map<String, I18nizableText> widgetParameters = criterion.getWidgetParameters();
395        if (widgetParameters != null && !widgetParameters.isEmpty())
396        {
397            jsonObject.put("widget-params", criterion.getWidgetParameters());
398        }
399    }
400    
401    private void _putEnumerator(Map<String, Object> jsonObject, Enumerator enumerator) throws ProcessingException
402    {
403        if (enumerator != null)
404        {
405            try
406            {
407                List<Map<String, Object>> options = new ArrayList<>();
408                
409                for (Map.Entry<Object, I18nizableText> entry : enumerator.getEntries().entrySet())
410                {
411                    String valueAsString = ParameterHelper.valueToString(entry.getKey());
412                    I18nizableText entryLabel = entry.getValue();
413                    
414                    Map<String, Object> option = new HashMap<>();
415                    option.put("label", entryLabel != null ? entryLabel : valueAsString);
416                    option.put("value", valueAsString);
417                    options.add(option);
418                }
419                
420                jsonObject.put("enumeration", options);
421            }
422            catch (Exception e)
423            {
424                throw new ProcessingException("Unable to enumerate entries with enumerator: " + enumerator, e);
425            }
426        }
427    }
428    
429    private void _putValidator(Map<String, Object> jsonObject, Validator validator)
430    {
431        if (validator != null)
432        {
433            Map<String, Object> val2Json = validator.toJson();
434            if (val2Json.containsKey("mandatory"))
435            {
436                // Override mandatory property
437                val2Json.put("mandatory", true);
438            }
439            jsonObject.put("validation", val2Json);
440        }
441    }
442}