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());
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("criteria");
181        if (view == null)
182        {
183            view = cType.getView("main");
184        }
185
186        addCriteriaComponents(cType, view);
187        
188        if (_hierarchicalReferenceTableContentsHelper.isHierarchical(cType) && _hierarchicalReferenceTableContentsHelper.supportCandidates(cType))
189        {
190            addExcludeCandidateSystemCriterionComponent(cType);
191        }
192
193        addSystemCriterionComponent(cType, "contributor");
194        
195        if (!cType.isMultilingual())
196        {
197            addSystemCriterionComponent(cType, "contentLanguage");
198        }
199    }
200
201    /**
202     * Add criteria components to the manager.
203     * @param cType the simple content type.
204     * @param viewContainer the view item container
205     * @throws ConfigurationException if a configuration error occurs.
206     * @throws ComponentException if a component cannot be initialized.
207     */
208    protected void addCriteriaComponents(ContentType cType, ViewItemContainer viewContainer) throws ConfigurationException, ComponentException
209    {
210        for (ViewItem viewItem : viewContainer.getViewItems())
211        {
212            if (viewItem instanceof ViewItemContainer)
213            {
214                addCriteriaComponents(cType, (ViewItemContainer) viewItem);
215            }
216            else if (viewItem instanceof ModelViewItem)
217            {
218                ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition();
219                if (_filterModelItemForCriteria(modelItem))
220                {
221                    String modelItemPath = modelItem.getPath();
222                    
223                    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;
224                    addAttributeCriterionComponent(cType, modelItemPath, operator);
225                    
226                    if (Content.ATTRIBUTE_TITLE.equals(modelItem.getName()))
227                    {
228                        addLikeTitleCriterionComponent(cType, modelItemPath);
229                    }
230                }
231            }
232        }
233    }
234    
235    /** Add criteria component to exclude the candidates
236     * @param cType the simple content type
237     */
238    protected void addExcludeCandidateSystemCriterionComponent(ContentType cType)
239    {
240        DefaultConfiguration conf = (DefaultConfiguration) getSystemCriteriaConfiguration(Set.of(cType.getId()), "mixins", null);
241        
242        DefaultConfiguration defaultValueConf = new DefaultConfiguration("default-value");
243        defaultValueConf.setValue("org.ametys.cms.referencetable.mixin.Candidate");
244        conf.addChild(defaultValueConf);
245        
246        DefaultConfiguration opConf = new DefaultConfiguration("test-operator");
247        opConf.setValue(Operator.NE.getName());
248        conf.addChild(opConf);
249        
250        DefaultConfiguration widgetConf = new DefaultConfiguration("widget");
251        widgetConf.setValue("edition.hidden");
252        conf.addChild(widgetConf);
253        
254        _searchCriteriaManager.addComponent("cms", null, "mixins", SystemSearchUICriterion.class, conf);
255        _searchCriteriaRoles.add("mixins");
256    }
257    
258    /**
259     * Returns <code>true</code> if the model item can be used as criteria
260     * @param modelItem the model item
261     * @return <code>true</code> if the model item can be used as criteria
262     */
263    @SuppressWarnings("static-access")
264    protected boolean _filterModelItemForCriteria(ModelItem modelItem)
265    {
266        String typeId = modelItem.getType().getId();
267        switch (typeId)
268        {
269            case ModelItemTypeConstants.STRING_TYPE_ID:
270            case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID:
271            case ModelItemTypeConstants.DATE_TYPE_ID:
272            case ModelItemTypeConstants.DATETIME_TYPE_ID:
273            case ModelItemTypeConstants.LONG_TYPE_ID:
274            case ModelItemTypeConstants.DOUBLE_TYPE_ID:
275            case ModelItemTypeConstants.BOOLEAN_TYPE_ID:
276            case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID:
277            case ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID:
278                return true;
279            case ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID:
280            case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID:
281            case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID:
282            case ModelItemTypeConstants.GEOCODE_ELEMENT_TYPE_ID:
283            case ModelItemTypeConstants.COMPOSITE_TYPE_ID:
284            case ModelItemTypeConstants.REPEATER_TYPE_ID:
285            default:
286                return false;
287        }
288    }
289    
290    /**
291     * Returns <code>true</code> if model item can be used as column search UI
292     * @param modelItem the model item
293     * @return <code>true</code> if model item can be used as column search UI
294     */
295    @SuppressWarnings("static-access")
296    protected boolean _filterModelItemForColumn(ModelItem modelItem)
297    {
298        String typeId = modelItem.getType().getId();
299        switch (typeId)
300        {
301            case ModelItemTypeConstants.STRING_TYPE_ID:
302            case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID:
303            case ModelItemTypeConstants.DATE_TYPE_ID:
304            case ModelItemTypeConstants.DATETIME_TYPE_ID:
305            case ModelItemTypeConstants.LONG_TYPE_ID:
306            case ModelItemTypeConstants.DOUBLE_TYPE_ID:
307            case ModelItemTypeConstants.BOOLEAN_TYPE_ID:
308            case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID:
309            case ModelItemTypeConstants.GEOCODE_ELEMENT_TYPE_ID:
310            case ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID:
311                return true;
312            case ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID:
313            case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID:
314            case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID:
315            case ModelItemTypeConstants.COMPOSITE_TYPE_ID:
316            case ModelItemTypeConstants.REPEATER_TYPE_ID:
317            default:
318                return false;
319        }
320    }
321    
322    /**
323     * Add the title attribute criterion component to the manager, with a 'LIKE' operator and an hidden widget
324     * @param contentType the simple content type.
325     * @param attributePath the attribute path.
326     * @throws ConfigurationException if a configuration error occurs.
327     * @throws ComponentException if a component cannot be initialized.
328     */
329    protected void addLikeTitleCriterionComponent(ContentType contentType, String attributePath) throws ConfigurationException, ComponentException
330    {
331        DefaultConfiguration originalConf = new DefaultConfiguration("criteria");
332        DefaultConfiguration widgetConf = new DefaultConfiguration("widget");
333        widgetConf.setValue("edition.hidden");
334        originalConf.addChild(widgetConf);
335        Configuration conf = getIndexingFieldCriteriaConfiguration(originalConf, Set.of(contentType.getId()), attributePath, Operator.LIKE, null);
336        
337        String role = attributePath + "1";
338        _searchCriteriaManager.addComponent("cms", null, role, IndexingFieldSearchUICriterion.class, conf);
339        _searchCriteriaRoles.add(role);
340    }
341    
342    /**
343     * Add an attribute criterion component to the manager. 
344     * @param contentType the simple content type.
345     * @param attributePath the attribute path.
346     * @param operator the criterion operator.
347     * @throws ConfigurationException if a configuration error occurs.
348     * @throws ComponentException if a component cannot be initialized.
349     */
350    protected void addAttributeCriterionComponent(ContentType contentType, String attributePath, Operator operator) throws ConfigurationException, ComponentException
351    {
352        DefaultConfiguration conf = (DefaultConfiguration) getIndexingFieldCriteriaConfiguration(Set.of(contentType.getId()), attributePath, operator, null);
353        ModelItem metadataDefinition = contentType.getModelItem(attributePath);
354        
355        if (
356                ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(metadataDefinition.getType().getId()) 
357                && contentType.isReferenceTable() 
358                && contentType.getParentAttributeDefinition()
359                    .map(ModelItem::getPath)
360                    .map(parent -> parent.equals(attributePath))
361                    .orElse(false)
362            )
363        {
364            DefaultConfiguration widgetConfig = (DefaultConfiguration) conf.getChild("widget");
365            widgetConfig.setValue("edition.select-referencetable-content");
366            
367            DefaultConfiguration widgetParamsConfig = (DefaultConfiguration) conf.getChild("widget-params");
368            
369            DefaultConfiguration allowAutopostingParamsConfig = (DefaultConfiguration) widgetParamsConfig.getChild("param");
370            allowAutopostingParamsConfig.setAttribute("name", "allowToggleAutoposting");
371            allowAutopostingParamsConfig.setValue(true);
372            widgetParamsConfig.addChild(allowAutopostingParamsConfig);
373            
374            conf.addChild(widgetConfig);
375            conf.addChild(widgetParamsConfig);
376        }
377        _searchCriteriaManager.addComponent("cms", null, attributePath, IndexingFieldSearchUICriterion.class, conf);
378        _searchCriteriaRoles.add(attributePath);
379    }
380    
381    /**
382     * Add a system criterion component to the manager.
383     * @param contentType the simple content type.
384     * @param property the system property.
385     * @throws ConfigurationException if a configuration error occurs.
386     * @throws ComponentException if a component cannot be initialized.
387     */
388    protected void addSystemCriterionComponent(ContentType contentType, String property) throws ConfigurationException, ComponentException
389    {
390        DefaultConfiguration conf = (DefaultConfiguration) getSystemCriteriaConfiguration(Set.of(contentType.getId()), property, null);
391        
392        if (property.equals("contentLanguage"))
393        {
394            // FIXME Is this configuration should be provided by Language system property itself ?
395            // FIXME For now the simple contents are only created for language 'fr'
396            DefaultConfiguration widgetConf = new DefaultConfiguration("widget");
397            widgetConf.setValue("edition.select-language");
398            conf.addChild(widgetConf);
399            
400            DefaultConfiguration defaultConf = new DefaultConfiguration("default-value");
401            defaultConf.setValue("CURRENT");
402            conf.addChild(defaultConf);
403            
404            DefaultConfiguration validConf = new DefaultConfiguration("validation");
405            DefaultConfiguration mandatoryConf = new DefaultConfiguration("mandatory");
406            mandatoryConf.setValue(true);
407            validConf.addChild(mandatoryConf);
408            conf.addChild(validConf);
409        }
410        
411        _searchCriteriaManager.addComponent("cms", null, property, SystemSearchUICriterion.class, conf);
412        _searchCriteriaRoles.add(property);
413    }
414    
415    /**
416     * Lookup all the criteria.
417     * @param cType the simple content type.
418     * @return the search criteria list.
419     * @throws ComponentException if a component cannot be looked up.
420     */
421    protected List<SearchUICriterion> getSearchUICriteria(ContentType cType) throws ComponentException
422    {
423        List<SearchUICriterion> criteria = new ArrayList<>();
424        
425        for (String role : _searchCriteriaRoles)
426        {
427            SearchUICriterion criterion = _searchCriteriaManager.lookup(role);
428            criteria.add(criterion);
429        }
430        
431        return criteria;
432    }
433    
434    /**
435     * Add column components to the manager.
436     * @param cType the simple content type.
437     * @throws ConfigurationException if a configuration error occurs.
438     * @throws ComponentException if a component cannot be initialized.
439     */
440    protected void addColumnComponents(ContentType cType) throws ConfigurationException, ComponentException
441    {
442        View view = cType.getView("columns");
443        if (view == null)
444        {
445            view = cType.getView("main");
446        }
447        
448        addColumnComponents(cType, view);
449        
450        addSystemColumnComponent(cType, "contributor");
451        addSystemColumnComponent(cType, "lastModified");
452        
453        if (!cType.isMultilingual())
454        {
455            addSystemColumnComponent(cType, "contentLanguage");
456        }
457    }
458
459    /**
460     * Add column components to the manager.
461     * @param cType the simple content type.
462     * @param viewContainer the view item container
463     * @throws ConfigurationException if a configuration error occurs.
464     * @throws ComponentException if a component cannot be initialized.
465     */
466    protected void addColumnComponents(ContentType cType, ViewItemContainer viewContainer) throws ConfigurationException, ComponentException
467    {
468        for (ViewItem viewItem : viewContainer.getViewItems())
469        {
470            if (viewItem instanceof ViewItemContainer)
471            {
472                addColumnComponents(cType, (ViewItemContainer) viewItem);
473            }
474            else if (viewItem instanceof ModelViewItem)
475            {
476                ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition();
477                
478                if (_filterModelItemForColumn(modelItem))
479                {
480                    addAttributeColumnComponent(cType, modelItem.getPath());
481                }
482            }
483        }
484    }
485    
486    /**
487     * Add an attribute column component to the manager.
488     * @param contentType the simple content type.
489     * @param attributePath the attribute path.
490     * @throws ConfigurationException if a configuration error occurs.
491     * @throws ComponentException if a component cannot be initialized.
492     */
493    protected void addAttributeColumnComponent(ContentType contentType, String attributePath) throws ConfigurationException, ComponentException
494    {
495        Configuration columnConf = getMetadataColumnConfiguration(Set.of(contentType.getId()), attributePath);
496        _searchColumnManager.addComponent("cms", null, attributePath, MetadataSearchUIColumn.class, columnConf);
497        _searchColumnRoles.add(attributePath);
498    }
499    
500    /**
501     * Add a system column component to the manager.
502     * @param contentType the simple content type.
503     * @param property the system property.
504     * @throws ConfigurationException if a configuration error occurs.
505     * @throws ComponentException if a component cannot be initialized.
506     */
507    protected void addSystemColumnComponent(ContentType contentType, String property) throws ConfigurationException, ComponentException
508    {
509        Configuration conf = getSystemColumnConfiguration(Set.of(contentType.getId()), property);
510        _searchColumnManager.addComponent("cms", null, property, SystemSearchUIColumn.class, conf);
511        _searchColumnRoles.add(property);
512    }
513    
514    /**
515     * Lookup all the columns.
516     * @return the search column list.
517     * @throws ComponentException if a component cannot be looked up.
518     */
519    protected List<SearchUIColumn> getColumns() throws ComponentException
520    {
521        List<SearchUIColumn> columns = new ArrayList<>();
522        
523        for (String columnRole : _searchColumnRoles)
524        {
525            SearchUIColumn column = _searchColumnManager.lookup(columnRole);
526            columns.add(column);
527        }
528        
529        return columns;
530    }
531
532}