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.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032
033import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper;
034import org.ametys.cms.contenttype.AbstractMetadataSetElement;
035import org.ametys.cms.contenttype.ContentType;
036import org.ametys.cms.contenttype.MetadataDefinition;
037import org.ametys.cms.contenttype.MetadataDefinitionReference;
038import org.ametys.cms.contenttype.MetadataSet;
039import org.ametys.cms.contenttype.MetadataType;
040import org.ametys.cms.search.query.Query.Operator;
041import org.ametys.cms.search.ui.model.impl.IndexingFieldSearchUICriterion;
042import org.ametys.cms.search.ui.model.impl.MetadataSearchUIColumn;
043import org.ametys.cms.search.ui.model.impl.SystemSearchUIColumn;
044import org.ametys.cms.search.ui.model.impl.SystemSearchUICriterion;
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        MetadataSet metadataSet = cType.getMetadataSetForEdition("main");
180        for (AbstractMetadataSetElement subMetadataSetElement : metadataSet.getElements())
181        {
182            // Get only simple metadata (ignore composites and repeaters)
183            if (subMetadataSetElement instanceof MetadataDefinitionReference)
184            {
185                String metadataName = ((MetadataDefinitionReference) subMetadataSetElement).getMetadataName();
186                MetadataDefinition metadataDefinition = cType.getMetadataDefinition(metadataName);
187                
188                if (metadataDefinition == null)
189                {
190                    getLogger().warn("The metadata '{}' defined in the metadata set '{}' does not seem to exist.", metadataName, metadataSet);
191                }
192                else if (_filterMetadata(metadataDefinition))
193                {
194                    Operator operator = metadataDefinition.getType().equals(MetadataType.STRING) || metadataDefinition.getType().equals(MetadataType.MULTILINGUAL_STRING) ? Operator.SEARCH : Operator.EQ;
195                    addMetadataCriterionComponent(cType, metadataName, operator);
196                    if ("title".equals(metadataName))
197                    {
198                        addLikeTitleCriterionComponent(cType);
199                    }
200                }
201            }
202        }
203        
204        if (_hierarchicalReferenceTableContentsHelper.isHierarchical(cType) && _hierarchicalReferenceTableContentsHelper.supportCandidates(cType))
205        {
206            addExcludeCandidateSystemCriterionComponent(cType);
207        }
208
209        addSystemCriterionComponent(cType, "contributor");
210        
211        if (!cType.isMultilingual())
212        {
213            addSystemCriterionComponent(cType, "contentLanguage");
214        }
215    }
216    
217    /** Add criteria component to exclude the candidates
218     * @param cType the simple content type
219     */
220    protected void addExcludeCandidateSystemCriterionComponent(ContentType cType)
221    {
222        DefaultConfiguration conf = (DefaultConfiguration) getSystemCriteriaConfiguration(cType.getId(), "mixins", null);
223        
224        DefaultConfiguration defaultValueConf = new DefaultConfiguration("default-value");
225        defaultValueConf.setValue("org.ametys.cms.referencetable.mixin.Candidate");
226        conf.addChild(defaultValueConf);
227        
228        DefaultConfiguration opConf = new DefaultConfiguration("test-operator");
229        opConf.setValue(Operator.NE.getName());
230        conf.addChild(opConf);
231        
232        DefaultConfiguration widgetConf = new DefaultConfiguration("widget");
233        widgetConf.setValue("edition.hidden");
234        conf.addChild(widgetConf);
235        
236        _searchCriteriaManager.addComponent("cms", null, "mixins", SystemSearchUICriterion.class, conf);
237        _searchCriteriaRoles.add("mixins");
238    }
239    
240    /**
241     * Returns true if metadata can be used as criteria and column search UI
242     * @param metadataDefinition the metadata definition
243     * @return <code>true</code>
244     */
245    protected boolean _filterMetadata(MetadataDefinition metadataDefinition)
246    {
247        MetadataType type = metadataDefinition.getType();
248        switch (type)
249        {
250            case STRING:
251            case MULTILINGUAL_STRING:
252            case DATE:
253            case DATETIME:
254            case LONG:
255            case DOUBLE:
256            case BOOLEAN:
257            case CONTENT:
258                return true;
259            case BINARY:
260            case RICH_TEXT:
261            case REFERENCE:
262            case FILE:
263            case GEOCODE:
264            case COMPOSITE:
265            case SUB_CONTENT:
266            default:
267                return false;
268        }
269    }
270    
271    /**
272     * Add the title metadata criterion component to the manager, with a 'LIKE' operator and an hidden widget
273     * @param contentType the simple content type.
274     * @throws ConfigurationException if a configuration error occurs.
275     * @throws ComponentException if a component cannot be initialized.
276     */
277    protected void addLikeTitleCriterionComponent(ContentType contentType) throws ConfigurationException, ComponentException
278    {
279        String metadataName = "title";
280        DefaultConfiguration originalConf = new DefaultConfiguration("criteria");
281        DefaultConfiguration widgetConf = new DefaultConfiguration("widget");
282        widgetConf.setValue("edition.hidden");
283        originalConf.addChild(widgetConf);
284        Configuration conf = getIndexingFieldCriteriaConfiguration(originalConf, contentType.getId(), metadataName, Operator.LIKE, null);
285        
286        String role = metadataName + "1";
287        _searchCriteriaManager.addComponent("cms", null, role, IndexingFieldSearchUICriterion.class, conf);
288        _searchCriteriaRoles.add(role);
289    }
290    
291    /**
292     * Add a metadata criterion component to the manager. 
293     * @param contentType the simple content type.
294     * @param metadataName the metadata name.
295     * @param operator the criterion operator.
296     * @throws ConfigurationException if a configuration error occurs.
297     * @throws ComponentException if a component cannot be initialized.
298     */
299    protected void addMetadataCriterionComponent(ContentType contentType, String metadataName, Operator operator) throws ConfigurationException, ComponentException
300    {
301        DefaultConfiguration conf = (DefaultConfiguration) getIndexingFieldCriteriaConfiguration(contentType.getId(), metadataName, operator, null);
302        MetadataDefinition metadataDefinition = contentType.getMetadataDefinition(metadataName);
303        
304        if (
305                metadataDefinition.getType() == MetadataType.CONTENT 
306                && contentType.isReferenceTable() 
307                && Optional.ofNullable(contentType.getParentMetadata())
308                    .map(MetadataDefinition::getId)
309                    .map(parent -> parent.equals(metadataName))
310                    .orElse(false)
311            )
312        {
313            DefaultConfiguration widgetConfig = (DefaultConfiguration) conf.getChild("widget");
314            widgetConfig.setValue("edition.select-referencetable-content");
315            
316            DefaultConfiguration widgetParamsConfig = (DefaultConfiguration) conf.getChild("widget-params");
317            
318            DefaultConfiguration allowAutopostingParamsConfig = (DefaultConfiguration) widgetParamsConfig.getChild("param");
319            allowAutopostingParamsConfig.setAttribute("name", "allowToggleAutoposting");
320            allowAutopostingParamsConfig.setValue(true);
321            widgetParamsConfig.addChild(allowAutopostingParamsConfig);
322            
323            conf.addChild(widgetConfig);
324            conf.addChild(widgetParamsConfig);
325        }
326        _searchCriteriaManager.addComponent("cms", null, metadataName, IndexingFieldSearchUICriterion.class, conf);
327        _searchCriteriaRoles.add(metadataName);
328    }
329    
330    /**
331     * Add a system criterion component to the manager.
332     * @param contentType the simple content type.
333     * @param property the system property.
334     * @throws ConfigurationException if a configuration error occurs.
335     * @throws ComponentException if a component cannot be initialized.
336     */
337    protected void addSystemCriterionComponent(ContentType contentType, String property) throws ConfigurationException, ComponentException
338    {
339        DefaultConfiguration conf = (DefaultConfiguration) getSystemCriteriaConfiguration(contentType.getId(), property, null);
340        
341        if (property.equals("contentLanguage"))
342        {
343            // FIXME Is this configuration should be provided by Language system property itself ?
344            // FIXME For now the simple contents are only created for language 'fr'
345            DefaultConfiguration widgetConf = new DefaultConfiguration("widget");
346            widgetConf.setValue("edition.select-language");
347            conf.addChild(widgetConf);
348            
349            DefaultConfiguration defaultConf = new DefaultConfiguration("default-value");
350            defaultConf.setValue("CURRENT");
351            conf.addChild(defaultConf);
352            
353            DefaultConfiguration validConf = new DefaultConfiguration("validation");
354            DefaultConfiguration mandatoryConf = new DefaultConfiguration("mandatory");
355            mandatoryConf.setValue(true);
356            validConf.addChild(mandatoryConf);
357            conf.addChild(validConf);
358        }
359        
360        _searchCriteriaManager.addComponent("cms", null, property, SystemSearchUICriterion.class, conf);
361        _searchCriteriaRoles.add(property);
362    }
363    
364    /**
365     * Lookup all the criteria.
366     * @param cType the simple content type.
367     * @return the search criteria list.
368     * @throws ComponentException if a component cannot be looked up.
369     */
370    protected List<SearchUICriterion> getSearchUICriteria(ContentType cType) throws ComponentException
371    {
372        List<SearchUICriterion> criteria = new ArrayList<>();
373        
374        for (String role : _searchCriteriaRoles)
375        {
376            SearchUICriterion criterion = _searchCriteriaManager.lookup(role);
377            criteria.add(criterion);
378        }
379        
380        return criteria;
381    }
382    
383    /**
384     * Add column components to the manager.
385     * @param cType the simple content type.
386     * @throws ConfigurationException if a configuration error occurs.
387     * @throws ComponentException if a component cannot be initialized.
388     */
389    protected void addColumnComponents(ContentType cType) throws ConfigurationException, ComponentException
390    {
391        MetadataSet metadataSet = cType.getMetadataSetForEdition("main");
392        for (AbstractMetadataSetElement subMetadataSetElement : metadataSet.getElements())
393        {
394            // Get only simple metadata (ignore composites and repeaters)
395            if (subMetadataSetElement instanceof MetadataDefinitionReference)
396            {
397                String metadataName = ((MetadataDefinitionReference) subMetadataSetElement).getMetadataName();
398                MetadataDefinition metadataDefinition = cType.getMetadataDefinition(metadataName);
399                
400                if (metadataDefinition == null)
401                {
402                    getLogger().warn("The metadata '{}' defined in the metadata set '{}' does not seem to exist.", metadataName, metadataSet);
403                }
404                else if (_filterMetadata(metadataDefinition))
405                {
406                    addMetadataColumnComponent(cType, metadataName);
407                }
408            }
409        }
410        
411        addSystemColumnComponent(cType, "contributor");
412        addSystemColumnComponent(cType, "lastModified");
413        
414        if (!cType.isMultilingual())
415        {
416            addSystemColumnComponent(cType, "contentLanguage");
417        }
418    }
419    
420    /**
421     * Add a metadata column component to the manager. 
422     * @param contentType the simple content type.
423     * @param metadataName the metadata name.
424     * @throws ConfigurationException if a configuration error occurs.
425     * @throws ComponentException if a component cannot be initialized.
426     */
427    protected void addMetadataColumnComponent(ContentType contentType, String metadataName) throws ConfigurationException, ComponentException
428    {
429        Configuration columnConf = getMetadataColumnConfiguration(contentType.getId(), metadataName);
430        _searchColumnManager.addComponent("cms", null, metadataName, MetadataSearchUIColumn.class, columnConf);
431        _searchColumnRoles.add(metadataName);
432    }
433    
434    /**
435     * Add a system column component to the manager.
436     * @param contentType the simple content type.
437     * @param property the system property.
438     * @throws ConfigurationException if a configuration error occurs.
439     * @throws ComponentException if a component cannot be initialized.
440     */
441    protected void addSystemColumnComponent(ContentType contentType, String property) throws ConfigurationException, ComponentException
442    {
443        Configuration conf = getSystemColumnConfiguration(contentType.getId(), property);
444        _searchColumnManager.addComponent("cms", null, property, SystemSearchUIColumn.class, conf);
445        _searchColumnRoles.add(property);
446    }
447    
448    /**
449     * Lookup all the columns.
450     * @param cType the simple content type.
451     * @return the search column list.
452     * @throws ComponentException if a component cannot be looked up.
453     */
454    protected List<SearchUIColumn> getColumns(ContentType cType) throws ComponentException
455    {
456        List<SearchUIColumn> columns = new ArrayList<>();
457        
458        for (String columnRole : _searchColumnRoles)
459        {
460            SearchUIColumn column = _searchColumnManager.lookup(columnRole);
461            columns.add(column);
462        }
463        
464        return columns;
465    }
466
467}