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