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