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.Collections;
020import java.util.List;
021import java.util.Map;
022import java.util.Optional;
023import java.util.Set;
024
025import org.apache.avalon.framework.component.ComponentException;
026import org.apache.avalon.framework.configuration.Configurable;
027import org.apache.avalon.framework.configuration.Configuration;
028import org.apache.avalon.framework.configuration.ConfigurationException;
029import org.apache.avalon.framework.configuration.DefaultConfiguration;
030import org.apache.avalon.framework.context.Context;
031import org.apache.avalon.framework.context.ContextException;
032import org.apache.avalon.framework.context.Contextualizable;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.avalon.framework.service.Serviceable;
036
037import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper;
038import org.ametys.cms.contenttype.ContentType;
039import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
040import org.ametys.cms.data.type.ModelItemTypeConstants;
041import org.ametys.cms.repository.Content;
042import org.ametys.cms.search.model.SearchCriterionHelper;
043import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
044import org.ametys.cms.search.query.Query.Operator;
045import org.ametys.cms.search.ui.model.impl.IndexingFieldSearchUICriterion;
046import org.ametys.cms.search.ui.model.impl.SystemSearchUICriterion;
047import org.ametys.runtime.model.ModelItem;
048import org.ametys.runtime.model.ModelViewItem;
049import org.ametys.runtime.model.View;
050import org.ametys.runtime.model.ViewElement;
051import org.ametys.runtime.model.ViewItem;
052import org.ametys.runtime.model.ViewItemAccessor;
053import org.ametys.runtime.model.ViewItemContainer;
054import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
055
056/**
057 * Generic implementation of {@link SearchUIModel} for reference tables
058 * The search tool model automatically declares simple first level attributes as criteria and columns.
059 */
060public class ReferenceTableSearchUIModel extends AbstractSearchUIModel implements Serviceable, Contextualizable, Configurable
061{
062    /** ComponentManager for {@link SearchUICriterion}s. */
063    protected ThreadSafeComponentManager<SearchUICriterion> _searchCriteriaManager;
064    
065    /** The search criteria roles. */
066    protected List<String> _searchCriteriaRoles;
067    
068    /** The search column roles. */
069    protected List<String> _searchColumnRoles;
070    
071    /** The context. */
072    protected Context _context;
073    
074    /** The service manager */
075    protected ServiceManager _manager;
076    
077    /** The helper component for hierarchical reference tables */
078    protected HierarchicalReferenceTablesHelper _hierarchicalReferenceTableContentsHelper;
079    
080    /** The content type extension point */
081    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
082    
083    /** The systemPropertyExtension point */
084    protected SystemPropertyExtensionPoint _systemPropertyExtensionPoint;
085    
086    /** The search criterion helper */
087    protected SearchCriterionHelper _searchCriterionHelper;
088    
089    public void contextualize(Context context) throws ContextException
090    {
091        _context = context;
092    }
093    
094    public void service(ServiceManager manager) throws ServiceException
095    {
096        _manager = manager;
097        
098        _hierarchicalReferenceTableContentsHelper = (HierarchicalReferenceTablesHelper) manager.lookup(HierarchicalReferenceTablesHelper.ROLE);
099        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
100        _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
101        _searchCriterionHelper = (SearchCriterionHelper) manager.lookup(SearchCriterionHelper.ROLE);
102    }
103    
104    @Override
105    public void configure(Configuration configuration) throws ConfigurationException
106    {
107        try
108        {
109            String cTypeId = configuration.getChild("contentType").getValue(null);
110            if (cTypeId != null)
111            {
112                setContentTypes(Collections.singleton(cTypeId));
113            }
114            
115            _searchCriteriaManager = new ThreadSafeComponentManager<>();
116            _searchCriteriaManager.setLogger(getLogger());
117            _searchCriteriaManager.contextualize(_context);
118            _searchCriteriaManager.service(_manager);
119        }
120        catch (Exception e)
121        {
122            throw new ConfigurationException("Unable to create local component managers.", configuration, e);
123        }
124    }
125    
126    @Override
127    public Set<String> getExcludedContentTypes(Map<String, Object> contextualParameters)
128    {
129        return Collections.emptySet();
130    }
131    
132    @Override
133    public Map<String, SearchUICriterion> getCriteria(Map<String, Object> contextualParameters)
134    {
135        try
136        {
137            if (_searchCriteria == null)
138            {
139                String cTypeId = getContentTypes(contextualParameters).iterator().next();
140                ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId);
141                
142                _searchCriteriaRoles = new ArrayList<>();
143                
144                addCriteriaComponents(cType);
145                _searchCriteriaManager.initialize();
146                setCriteria(getSearchUICriteria(cType));
147            }
148        }
149        catch (Exception e)
150        {
151            throw new RuntimeException("Impossible to initialize criteria components.", e);
152        }
153        
154        return _searchCriteria;
155    }
156    
157    @Override
158    public Map<String, SearchUICriterion> getFacetedCriteria(Map<String, Object> contextualParameters)
159    {
160        return Collections.emptyMap();
161    }
162    
163    @Override
164    public Map<String, SearchUICriterion> getAdvancedCriteria(Map<String, Object> contextualParameters)
165    {
166        return Collections.emptyMap();
167    }
168    
169    @Override
170    public ViewItemContainer getResultItems(Map<String, Object> contextualParameters)
171    {
172        if (_resultItems == null)
173        {
174            String contentTypeId = getContentTypes(contextualParameters).iterator().next();
175            ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
176            
177            ViewItemContainer resultItems = _getResultItems(contentType);
178            setResultItems(resultItems);
179        }
180        
181        return _resultItems;
182    }
183    
184    /**
185     * Add criteria components to the manager.
186     * @param cType the simple content type.
187     * @throws ConfigurationException if a configuration error occurs.
188     * @throws ComponentException if a component cannot be initialized.
189     */
190    protected void addCriteriaComponents(ContentType cType) throws ConfigurationException, ComponentException
191    {
192        View view = Optional.ofNullable(cType.getView("criteria"))
193                            .orElse(cType.getView("main"));
194
195        addCriteriaComponents(cType, view);
196        
197        if (_hierarchicalReferenceTableContentsHelper.isHierarchical(cType) && _hierarchicalReferenceTableContentsHelper.supportCandidates(cType))
198        {
199            addExcludeCandidateSystemCriterionComponent(cType);
200        }
201
202        addSystemCriterionComponent(cType, "contributor");
203        
204        if (!cType.isMultilingual())
205        {
206            addSystemCriterionComponent(cType, "contentLanguage");
207        }
208    }
209
210    /**
211     * Add criteria components to the manager.
212     * @param cType the simple content type.
213     * @param viewContainer the view item container
214     * @throws ConfigurationException if a configuration error occurs.
215     * @throws ComponentException if a component cannot be initialized.
216     */
217    protected void addCriteriaComponents(ContentType cType, ViewItemContainer viewContainer) throws ConfigurationException, ComponentException
218    {
219        for (ViewItem viewItem : viewContainer.getViewItems())
220        {
221            if (viewItem instanceof ViewItemContainer)
222            {
223                addCriteriaComponents(cType, (ViewItemContainer) viewItem);
224            }
225            else if (viewItem instanceof ModelViewItem)
226            {
227                ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition();
228                if (_filterModelItemForCriteria(modelItem))
229                {
230                    String modelItemPath = modelItem.getPath();
231                    
232                    Operator operator = org.ametys.runtime.model.type.ModelItemTypeConstants.STRING_TYPE_ID.equals(modelItem.getType().getId()) || ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(modelItem.getType().getId()) ? Operator.SEARCH : Operator.EQ;
233                    addAttributeCriterionComponent(cType, modelItemPath, operator);
234                    
235                    if (Content.ATTRIBUTE_TITLE.equals(modelItem.getName()))
236                    {
237                        addLikeTitleCriterionComponent(cType, modelItemPath);
238                    }
239                }
240            }
241        }
242    }
243    
244    /** Add criteria component to exclude the candidates
245     * @param cType the simple content type
246     * @throws ConfigurationException if an error occurs while creating the criteria configuration
247     */
248    protected void addExcludeCandidateSystemCriterionComponent(ContentType cType) throws ConfigurationException
249    {
250        DefaultConfiguration conf = (DefaultConfiguration) _searchCriterionHelper.getSystemCriteriaConfiguration(this, Optional.empty(), Set.of(cType.getId()), "mixins", Optional.empty());
251        
252        DefaultConfiguration defaultValueConf = new DefaultConfiguration("default-value");
253        defaultValueConf.setValue("org.ametys.cms.referencetable.mixin.Candidate");
254        conf.addChild(defaultValueConf);
255        
256        DefaultConfiguration opConf = new DefaultConfiguration("test-operator");
257        opConf.setValue(Operator.NE.getName());
258        conf.addChild(opConf);
259        
260        DefaultConfiguration widgetConf = new DefaultConfiguration("widget");
261        widgetConf.setValue("edition.hidden");
262        conf.addChild(widgetConf);
263        
264        _searchCriteriaManager.addComponent("cms", null, "mixins", SystemSearchUICriterion.class, conf);
265        _searchCriteriaRoles.add("mixins");
266    }
267    
268    /**
269     * Returns <code>true</code> if the model item can be used as criteria
270     * @param modelItem the model item
271     * @return <code>true</code> if the model item can be used as criteria
272     */
273    @SuppressWarnings("static-access")
274    protected boolean _filterModelItemForCriteria(ModelItem modelItem)
275    {
276        String typeId = modelItem.getType().getId();
277        switch (typeId)
278        {
279            case ModelItemTypeConstants.STRING_TYPE_ID:
280            case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID:
281            case ModelItemTypeConstants.DATE_TYPE_ID:
282            case ModelItemTypeConstants.DATETIME_TYPE_ID:
283            case ModelItemTypeConstants.LONG_TYPE_ID:
284            case ModelItemTypeConstants.DOUBLE_TYPE_ID:
285            case ModelItemTypeConstants.BOOLEAN_TYPE_ID:
286            case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID:
287            case ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID:
288            case ModelItemTypeConstants.USER_ELEMENT_TYPE_ID:
289                return true;
290            case ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID:
291            case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID:
292            case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID:
293            case ModelItemTypeConstants.GEOCODE_ELEMENT_TYPE_ID:
294            case ModelItemTypeConstants.COMPOSITE_TYPE_ID:
295            case ModelItemTypeConstants.REPEATER_TYPE_ID:
296            default:
297                return false;
298        }
299    }
300    
301    /**
302     * Add the title attribute criterion component to the manager, with a 'LIKE' operator and an hidden widget
303     * @param contentType the simple content type.
304     * @param attributePath the attribute path.
305     * @throws ConfigurationException if a configuration error occurs.
306     * @throws ComponentException if a component cannot be initialized.
307     */
308    protected void addLikeTitleCriterionComponent(ContentType contentType, String attributePath) throws ConfigurationException, ComponentException
309    {
310        DefaultConfiguration originalConf = new DefaultConfiguration("criteria");
311        DefaultConfiguration widgetConf = new DefaultConfiguration("widget");
312        widgetConf.setValue("edition.hidden");
313        originalConf.addChild(widgetConf);
314        Configuration conf = _searchCriterionHelper.getIndexingFieldCriteriaConfiguration(this, Optional.of(originalConf), Set.of(contentType.getId()), attributePath, Optional.of(Operator.LIKE), Optional.empty());
315        
316        String role = attributePath + "1";
317        _searchCriteriaManager.addComponent("cms", null, role, IndexingFieldSearchUICriterion.class, conf);
318        _searchCriteriaRoles.add(role);
319    }
320    
321    /**
322     * Add an attribute criterion component to the manager. 
323     * @param contentType the simple content type.
324     * @param attributePath the attribute path.
325     * @param operator the criterion operator.
326     * @throws ConfigurationException if a configuration error occurs.
327     * @throws ComponentException if a component cannot be initialized.
328     */
329    protected void addAttributeCriterionComponent(ContentType contentType, String attributePath, Operator operator) throws ConfigurationException, ComponentException
330    {
331        DefaultConfiguration conf = (DefaultConfiguration) _searchCriterionHelper.getIndexingFieldCriteriaConfiguration(this, Optional.empty(), Set.of(contentType.getId()), attributePath, Optional.ofNullable(operator), Optional.empty());
332        ModelItem metadataDefinition = contentType.getModelItem(attributePath);
333        
334        if (
335                ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(metadataDefinition.getType().getId()) 
336                && contentType.isReferenceTable() 
337                && contentType.getParentAttributeDefinition()
338                    .map(ModelItem::getPath)
339                    .map(parent -> parent.equals(attributePath))
340                    .orElse(false)
341            )
342        {
343            DefaultConfiguration widgetConfig = (DefaultConfiguration) conf.getChild("widget");
344            widgetConfig.setValue("edition.select-referencetable-content");
345            
346            DefaultConfiguration widgetParamsConfig = (DefaultConfiguration) conf.getChild("widget-params");
347            
348            DefaultConfiguration allowAutopostingParamsConfig = (DefaultConfiguration) widgetParamsConfig.getChild("param");
349            allowAutopostingParamsConfig.setAttribute("name", "allowToggleAutoposting");
350            allowAutopostingParamsConfig.setValue(true);
351            widgetParamsConfig.addChild(allowAutopostingParamsConfig);
352            
353            conf.addChild(widgetConfig);
354            conf.addChild(widgetParamsConfig);
355        }
356        _searchCriteriaManager.addComponent("cms", null, attributePath, IndexingFieldSearchUICriterion.class, conf);
357        _searchCriteriaRoles.add(attributePath);
358    }
359    
360    /**
361     * Add a system criterion component to the manager.
362     * @param contentType the simple content type.
363     * @param property the system property.
364     * @throws ConfigurationException if a configuration error occurs.
365     * @throws ComponentException if a component cannot be initialized.
366     */
367    protected void addSystemCriterionComponent(ContentType contentType, String property) throws ConfigurationException, ComponentException
368    {
369        DefaultConfiguration conf = (DefaultConfiguration) _searchCriterionHelper.getSystemCriteriaConfiguration(this, Optional.empty(), Set.of(contentType.getId()), property, Optional.empty());
370        
371        if (property.equals("contentLanguage"))
372        {
373            // FIXME Is this configuration should be provided by Language system property itself ?
374            // FIXME For now the simple contents are only created for language 'fr'
375            DefaultConfiguration widgetConf = new DefaultConfiguration("widget");
376            widgetConf.setValue("edition.select-language");
377            conf.addChild(widgetConf);
378            
379            DefaultConfiguration defaultConf = new DefaultConfiguration("default-value");
380            defaultConf.setValue("CURRENT");
381            conf.addChild(defaultConf);
382            
383            DefaultConfiguration validConf = new DefaultConfiguration("validation");
384            DefaultConfiguration mandatoryConf = new DefaultConfiguration("mandatory");
385            mandatoryConf.setValue(true);
386            validConf.addChild(mandatoryConf);
387            conf.addChild(validConf);
388        }
389        
390        _searchCriteriaManager.addComponent("cms", null, property, SystemSearchUICriterion.class, conf);
391        _searchCriteriaRoles.add(property);
392    }
393    
394    /**
395     * Lookup all the criteria.
396     * @param cType the simple content type.
397     * @return the search criteria list.
398     * @throws ComponentException if a component cannot be looked up.
399     */
400    protected List<SearchUICriterion> getSearchUICriteria(ContentType cType) throws ComponentException
401    {
402        List<SearchUICriterion> criteria = new ArrayList<>();
403        
404        for (String role : _searchCriteriaRoles)
405        {
406            SearchUICriterion criterion = _searchCriteriaManager.lookup(role);
407            criteria.add(criterion);
408        }
409        
410        return criteria;
411    }
412    
413    /**
414     * Retrieves the result items for the given content type
415     * @param contentType The content type
416     * @return the result items
417     */
418    protected ViewItemContainer _getResultItems(ContentType contentType)
419    {
420        View view = Optional.ofNullable(contentType.getView("columns"))
421                            .orElse(contentType.getView("main"));
422        
423        View resultItems = new View();
424        resultItems.addViewItems(_copyAndFilterViewItemsForColumns(view.getViewItems()));
425        
426        resultItems.addViewItem(SearchUIColumnHelper.createModelItemColumn(_systemPropertyExtensionPoint.getExtension("contributor")));
427        resultItems.addViewItem(SearchUIColumnHelper.createModelItemColumn(_systemPropertyExtensionPoint.getExtension("lastModified")));
428        
429        if (!contentType.isMultilingual())
430        {
431            resultItems.addViewItem(SearchUIColumnHelper.createModelItemColumn(_systemPropertyExtensionPoint.getExtension("contentLanguage")));
432        }
433        
434        return resultItems;
435    }
436    
437    /**
438     * Copy the given view items and filter to keep only items that can be used in {@link SearchUIColumn}s.
439     * Also copy the children of view item accessors
440     * @param viewItems the view items to copy
441     * @return the view items copies
442     */
443    protected List<ViewItem> _copyAndFilterViewItemsForColumns(List<ViewItem> viewItems)
444    {
445        List<ViewItem> copies = new ArrayList<>();
446        
447        for (ViewItem viewItem : viewItems)
448        {
449            if (!(viewItem instanceof ViewElement) || _filterModelItemForColumn(((ViewElement) viewItem).getDefinition()))
450            {
451                ViewItem copy = viewItem.createInstance();
452                if (viewItem instanceof ViewItemAccessor viewItemAccessor && !viewItemAccessor.getViewItems().isEmpty())
453                {
454                    assert copy instanceof ViewItemAccessor;
455                    ((ViewItemAccessor) copy).addViewItems(_copyAndFilterViewItemsForColumns(viewItemAccessor.getViewItems()));
456                }
457                else if (viewItem instanceof ModelViewItem modelViewItem)
458                {
459                    // If the view item is a leaf, create a column
460                    ModelItem modelItem = modelViewItem.getDefinition();
461                    copy = SearchUIColumnHelper.createModelItemColumn(modelItem);
462                }
463                
464                viewItem.copyTo(copy);
465                copies.add(copy);
466            }
467        }
468        
469        return copies;
470    }
471    
472    /**
473     * Returns <code>true</code> if model item can be used as column search UI
474     * @param modelItem the model item
475     * @return <code>true</code> if model item can be used as column search UI
476     */
477    @SuppressWarnings("static-access")
478    protected boolean _filterModelItemForColumn(ModelItem modelItem)
479    {
480        String typeId = modelItem.getType().getId();
481        switch (typeId)
482        {
483            case ModelItemTypeConstants.STRING_TYPE_ID:
484            case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID:
485            case ModelItemTypeConstants.DATE_TYPE_ID:
486            case ModelItemTypeConstants.DATETIME_TYPE_ID:
487            case ModelItemTypeConstants.LONG_TYPE_ID:
488            case ModelItemTypeConstants.DOUBLE_TYPE_ID:
489            case ModelItemTypeConstants.BOOLEAN_TYPE_ID:
490            case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID:
491            case ModelItemTypeConstants.GEOCODE_ELEMENT_TYPE_ID:
492            case ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID:
493            case ModelItemTypeConstants.USER_ELEMENT_TYPE_ID:
494                return true;
495            case ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID:
496            case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID:
497            case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID:
498            default:
499                return false;
500        }
501    }
502}