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