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