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 = _getModel(modelId, restrictedContentTypes);
121        return getSearchModelInfo(model, contextualParameters);
122        
123    }
124    
125    /**
126     * Get the column configurations of search model as JSON object
127     * @param modelId The id of search model
128     * @param restrictedContentTypes The restricted content types. Can be null.
129     * @param contextualParameters the contextual parameters
130     * @return The column configurations in a List
131     * @throws ProcessingException if an error occurred
132     */
133    @Callable
134    public List<Object> getColumnConfigurations(String modelId, List<String> restrictedContentTypes, Map<String, Object> contextualParameters) throws ProcessingException
135    {
136        SearchUIModel model = _getModel(modelId, restrictedContentTypes);
137        return getColumnListInfo(model.getResultFields(contextualParameters));
138    }
139    
140    private SearchUIModel _getModel(String modelId, List<String> restrictedContentTypes)
141    {
142        SearchUIModel model = _searchModelManager.getExtension(modelId);
143        if (model == null)
144        {
145            throw new IllegalArgumentException("The search model '" + modelId + "' does not exists");
146        }
147        
148        // TODO Replace DynamicWrappedSearchUIModel?
149        if (restrictedContentTypes != null)
150        {
151            model = new DynamicWrappedSearchUIModel(model, restrictedContentTypes, _cTypeEP, getLogger(), _context, _serviceManager);
152        }
153        
154        return model;
155    }
156    
157    /**
158     * Return information on a {@link SearchUIModel} object serialized in a Map.
159     * @param model The search model.
160     * @param contextualParameters The contextual parameters
161     * @return the detailed information serialized in a Map.
162     * @throws ProcessingException if an error occurs.
163     */
164    public Map<String, Object> getSearchModelInfo(SearchUIModel model, Map<String, Object> contextualParameters) throws ProcessingException
165    {
166        Map<String, Object> jsonObject = new HashMap<>();
167        
168        jsonObject.put("pageSize", model.getPageSize(contextualParameters));
169        jsonObject.put("workspace", model.getWorkspace(contextualParameters));
170        jsonObject.put("searchUrl", model.getSearchUrl(contextualParameters));
171        jsonObject.put("searchUrlPlugin", model.getSearchUrlPlugin(contextualParameters));
172        jsonObject.put("exportCSVUrl", model.getExportCSVUrl(contextualParameters));
173        jsonObject.put("exportCSVUrlPlugin", model.getExportCSVUrlPlugin(contextualParameters));
174        jsonObject.put("exportDOCUrl", model.getExportDOCUrl(contextualParameters));
175        jsonObject.put("exportDOCUrlPlugin", model.getExportDOCUrlPlugin(contextualParameters));
176        jsonObject.put("exportXMLUrl", model.getExportXMLUrl(contextualParameters));
177        jsonObject.put("exportXMLUrlPlugin", model.getExportXMLUrlPlugin(contextualParameters));
178        jsonObject.put("printUrl", model.getPrintUrl(contextualParameters));
179        jsonObject.put("printUrlPlugin", model.getPrintUrlPlugin(contextualParameters));
180        jsonObject.put("summaryView", model.getSummaryView());
181        
182        jsonObject.put("simple-criteria", getCriteriaListInfo(model.getCriteria(contextualParameters)));
183        jsonObject.put("advanced-criteria", getAdvancedCriteriaListInfo(model.getAdvancedCriteria(contextualParameters)));
184        jsonObject.put("columns", getColumnListInfo(model.getResultFields(contextualParameters)));
185        
186        jsonObject.put("hasFacets", !model.getFacetedCriteria(contextualParameters).isEmpty());
187        
188        return jsonObject;
189    }
190    
191    /**
192     * Return information on a list of {@link SearchUIColumn}, serialized as a Map.
193     * @param columns the list of search columns.
194     * @return the detailed information serialized in a Map.
195     * @throws ProcessingException if an error occurs.
196     */
197    public List<Object> getColumnListInfo(Map<String, ? extends SearchUIColumn> columns)  throws ProcessingException
198    {
199        List<Object> jsonObject = new ArrayList<>();
200        
201        for (SearchUIColumn column : columns.values())
202        {
203            jsonObject.add(getColumnInfo(column));
204        }
205        
206        return jsonObject;
207    }
208    
209    /**
210     * Return information on a {@link SearchUIColumn}, serialized as a Map.
211     * @param column the search column.
212     * @return the detailed information serialized in a Map.
213     * @throws ProcessingException if an error occurs.
214     */
215    public Map<String, Object> getColumnInfo(SearchUIColumn column) throws ProcessingException
216    {
217//        Map<String, Object> jsonObject = _parameter2JsonObject(column);
218        Map<String, Object> jsonObject = new HashMap<>();
219        
220        // TODO Why replace . ?
221        jsonObject.put("id", column.getId().replace('.', '/'));
222        
223        jsonObject.put("label", column.getLabel());
224        jsonObject.put("description", column.getDescription());
225        jsonObject.put("type", column.getType().name());
226        
227        jsonObject.put("hidden", column.isHidden());
228        jsonObject.put("renderer", column.getRenderer());
229        jsonObject.put("converter", column.getConverter());
230        jsonObject.put("width", column.getWidth());
231        jsonObject.put("editable", column.isEditable());
232        jsonObject.put("contentType", column.getContentTypeId());
233        jsonObject.put("sortable", column.isSortable());
234        jsonObject.put("defaultSorter", column.getDefaultSorter());
235        jsonObject.put("multiple", column.isMultiple());
236        
237        _putEnumerator(jsonObject, column.getEnumerator());
238        _putValidator(jsonObject, column.getValidator());
239        
240        String widget = column.getWidget();
241        if (widget != null)
242        {
243            jsonObject.put("widget", widget);
244        }
245        
246        Map<String, I18nizableText> widgetParameters = column.getWidgetParameters();
247        if (widgetParameters != null && !widgetParameters.isEmpty())
248        {
249            jsonObject.put("widget-params", column.getWidgetParameters());
250        }
251        
252        return jsonObject;
253    }
254    
255    /**
256     * Return information on a list of {@link SearchUICriterion}, serialized as a Map.
257     * @param criteria a map of search criteria.
258     * @return the detailed information serialized in a Map.
259     * @throws ProcessingException if an error occurs.
260     */
261    @SuppressWarnings("unchecked")
262    public Map<String, Object> getCriteriaListInfo(Map<String, ? extends SearchUICriterion> criteria) throws ProcessingException
263    {
264        Map<String, Object> jsonObject = new LinkedHashMap<>();
265        
266        // TODO Test
267        Map<I18nizableText, List<SearchUICriterion>> criteriaByGroup = _getCriteriaByGroup(criteria);
268//        Map<I18nizableText, List<SearchUICriterion>> criteriaByGroup = criteria.values().stream()
269//                .collect(Collectors.groupingBy(SearchUICriterion::getGroup));
270        
271        for (I18nizableText group : criteriaByGroup.keySet())
272        {
273            Map<String, Object> elements = new LinkedHashMap<>();
274            
275            for (SearchUICriterion sc : criteriaByGroup.get(group))
276            {
277                elements.put(sc.getId(), getCriterionInfo(sc));
278            }
279            
280            Map<String, Object> fieldset = new LinkedHashMap<>();
281            if (group != null)
282            {
283                fieldset.put("label", group);
284            }
285            fieldset.put("role", "fieldset");
286            fieldset.put("elements", elements);
287            
288            if (!jsonObject.containsKey("fieldsets"))
289            {
290                jsonObject.put("fieldsets", new ArrayList<Map<String, Object>>());
291            }
292            
293            ((List<Map<String, Object>>) jsonObject.get("fieldsets")).add(fieldset);
294            
295        }
296        return jsonObject;
297    }
298    
299    /**
300     * Return information on a list of advanced {@link SearchUICriterion}, serialized as a Map.
301     * @param criteria A map of advanced search criteria.
302     * @return the detailed information serialized in a Map.
303     * @throws ProcessingException if an error occurs.
304     */
305    public Map<String, Object> getAdvancedCriteriaListInfo(Map<String, ? extends SearchUICriterion> criteria) throws ProcessingException
306    {
307        // No groups for advanced search
308        Map<String, Object> jsonObject = new LinkedHashMap<>();
309        Map<String, Object> criteriaObject = new LinkedHashMap<>();
310        
311        for (SearchUICriterion sc : criteria.values())
312        {
313            if (sc instanceof SystemSearchCriterion)
314            {
315                SystemSearchUICriterion sysCrit = (SystemSearchUICriterion) sc;
316                if (sysCrit.getSystemPropertyId().equals("contentLanguage"))
317                {
318                    // Separate the language from the other criteria since it is mandatory
319                    jsonObject.put("language", getCriterionInfo(sc));
320                }
321                else
322                {
323                    criteriaObject.put(sc.getId(), getCriterionInfo(sc));
324                }
325            }
326            else
327            {
328                criteriaObject.put(sc.getId(), getCriterionInfo(sc));
329            }
330        }
331        
332        if (MapUtils.isNotEmpty(criteriaObject))
333        {
334            jsonObject.put("criteria", criteriaObject);
335        }
336        
337        return jsonObject;
338    }
339    
340    /**
341     * Return information on a {@link SearchUICriterion}, serialized as a Map.
342     * @param criterion a search criterion.
343     * @return the detailed information serialized in a Map.
344     * @throws ProcessingException if an error occurs.
345     */
346    public Map<String, Object> getCriterionInfo(SearchUICriterion criterion) throws ProcessingException
347    {
348        Map<String, Object> jsonObject = new HashMap<>();
349        
350        // TODO Parameter interface
351        _putCriterionParameter(jsonObject, criterion);
352        
353        jsonObject.put("multiple", criterion.isMultiple());
354        jsonObject.put("hidden", criterion.isHidden());
355        
356        String contentTypeId = criterion.getContentTypeId();
357        if (contentTypeId != null)
358        {
359            jsonObject.put("contentType", contentTypeId);
360        }
361        
362        jsonObject.put("criterionProperty", criterion.getFieldId());
363        // TODO use operator.getName()?
364        if (criterion.getOperator() != null)
365        {
366            jsonObject.put("criterionOperator", criterion.getOperator().toString().toLowerCase());
367        }
368        
369//        if (criterion.getValue() != null)
370//        {
371//            jsonObject.put("value", criterion.getValue());
372//        }
373        
374        return jsonObject;
375    }
376    
377    private Map<I18nizableText, List<SearchUICriterion>> _getCriteriaByGroup(Map<String, ? extends SearchUICriterion> criteria)
378    {
379        Map<I18nizableText, List<SearchUICriterion>> criteriaByGroup = new LinkedHashMap<>();
380        
381        for (SearchUICriterion sc : criteria.values())
382        {
383            I18nizableText group = sc.getGroup();
384            
385            if (!criteriaByGroup.containsKey(group))
386            {
387                criteriaByGroup.put(group, new ArrayList<SearchUICriterion>());
388            }
389            
390            criteriaByGroup.get(group).add(sc);
391        }
392        
393        return criteriaByGroup;
394    }
395    
396    private void _putCriterionParameter(Map<String, Object> jsonObject, SearchUICriterion criterion) throws ProcessingException
397    {
398        jsonObject.put("id", criterion.getId());
399        jsonObject.put("label", criterion.getLabel());
400        jsonObject.put("description", criterion.getDescription());
401        jsonObject.put("type", criterion.getType().name());
402        
403        Object defaultValue = criterion.getDefaultValue();
404        if (defaultValue != null)
405        {
406            jsonObject.put("default-value", defaultValue);
407        }
408        
409        _putEnumerator(jsonObject, criterion.getEnumerator());
410        _putValidator(jsonObject, criterion.getValidator());
411        
412        String widget = criterion.getWidget();
413        if (widget != null)
414        {
415            jsonObject.put("widget", widget);
416        }
417        
418        Map<String, I18nizableText> widgetParameters = criterion.getWidgetParameters();
419        if (widgetParameters != null && !widgetParameters.isEmpty())
420        {
421            jsonObject.put("widget-params", criterion.getWidgetParameters());
422        }
423    }
424    
425    private void _putEnumerator(Map<String, Object> jsonObject, Enumerator enumerator) throws ProcessingException
426    {
427        if (enumerator != null)
428        {
429            try
430            {
431                List<Map<String, Object>> options = new ArrayList<>();
432                
433                for (Map.Entry<Object, I18nizableText> entry : enumerator.getEntries().entrySet())
434                {
435                    String valueAsString = ParameterHelper.valueToString(entry.getKey());
436                    I18nizableText entryLabel = entry.getValue();
437                    
438                    Map<String, Object> option = new HashMap<>();
439                    option.put("label", entryLabel != null ? entryLabel : valueAsString);
440                    option.put("value", valueAsString);
441                    options.add(option);
442                }
443                
444                jsonObject.put("enumeration", options);
445                jsonObject.put("enumerationConfig", enumerator.getConfiguration());
446            }
447            catch (Exception e)
448            {
449                throw new ProcessingException("Unable to enumerate entries with enumerator: " + enumerator, e);
450            }
451        }
452    }
453    
454    private void _putValidator(Map<String, Object> jsonObject, Validator validator)
455    {
456        if (validator != null)
457        {
458            Map<String, Object> val2Json = validator.getConfiguration();
459            if (val2Json.containsKey("mandatory"))
460            {
461                // Override mandatory property
462                val2Json.put("mandatory", true);
463            }
464            jsonObject.put("validation", val2Json);
465        }
466    }
467}