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.configuration.Configurable;
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.commons.lang3.tuple.ImmutablePair;
032
033import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper;
034import org.ametys.cms.contenttype.ContentType;
035import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
036import org.ametys.cms.data.type.ModelItemTypeConstants;
037import org.ametys.cms.repository.Content;
038import org.ametys.cms.search.model.SearchModelCriterionDefinition;
039import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
040import org.ametys.cms.search.query.Query.Operator;
041import org.ametys.cms.search.ui.model.impl.DefaultSearchUIModel;
042import org.ametys.runtime.i18n.I18nizableText;
043import org.ametys.runtime.model.ElementDefinition;
044import org.ametys.runtime.model.ModelItem;
045import org.ametys.runtime.model.ModelViewItem;
046import org.ametys.runtime.model.SimpleViewItemGroup;
047import org.ametys.runtime.model.View;
048import org.ametys.runtime.model.ViewElement;
049import org.ametys.runtime.model.ViewItem;
050import org.ametys.runtime.model.ViewItemAccessor;
051import org.ametys.runtime.model.ViewItemContainer;
052import org.ametys.runtime.model.ViewItemGroup;
053import org.ametys.runtime.parameter.DefaultValidator;
054
055/**
056 * Generic implementation of {@link SearchUIModel} for reference tables
057 * The search tool model automatically declares simple first level attributes as criteria and columns.
058 */
059public class ReferenceTableSearchUIModel extends DefaultSearchUIModel implements Serviceable, Configurable
060{
061    /** The helper component for hierarchical reference tables */
062    protected HierarchicalReferenceTablesHelper _hierarchicalReferenceTableContentsHelper;
063    
064    /** The content type extension point */
065    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
066    
067    /** The systemPropertyExtension point */
068    protected SystemPropertyExtensionPoint _systemPropertyExtensionPoint;
069    
070    /** The helper for {@link SearchUIModel} criterion definition */
071    protected SearchModelCriterionViewItemHelper _searchModelCriterionViewItemHelper;
072    
073    public void service(ServiceManager manager) throws ServiceException
074    {
075        _hierarchicalReferenceTableContentsHelper = (HierarchicalReferenceTablesHelper) manager.lookup(HierarchicalReferenceTablesHelper.ROLE);
076        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
077        _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
078        _searchModelCriterionViewItemHelper = (SearchModelCriterionViewItemHelper) manager.lookup(SearchModelCriterionViewItemHelper.ROLE);
079    }
080    
081    @Override
082    public void configure(Configuration configuration) throws ConfigurationException
083    {
084        try
085        {
086            String cTypeId = configuration.getChild("contentType").getValue(null);
087            if (cTypeId != null)
088            {
089                setContentTypes(Collections.singleton(cTypeId));
090            }
091        }
092        catch (Exception e)
093        {
094            throw new ConfigurationException("Unable to create local component managers.", configuration, e);
095        }
096    }
097    
098    @Override
099    public Set<String> getExcludedContentTypes(Map<String, Object> contextualParameters)
100    {
101        return Collections.emptySet();
102    }
103    
104    @Override
105    public ViewItemContainer getCriteria(Map<String, Object> contextualParameters)
106    {
107        if (super.getCriteria(contextualParameters).getViewItems().isEmpty())
108        {
109            String contentTypeId = getContentTypes(contextualParameters).iterator().next();
110            ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
111            
112            ViewItemContainer criteria = _getCriteria(contentType);
113            setCriteria(criteria);
114        }
115        
116        return super.getCriteria(contextualParameters);
117    }
118    
119    @Override
120    public ViewItemContainer getFacetedCriteria(Map<String, Object> contextualParameters)
121    {
122        if (super.getFacetedCriteria(contextualParameters).getViewItems().isEmpty())
123        {
124            setFacetedCriteria(new View());
125        }
126        
127        return super.getFacetedCriteria(contextualParameters);
128    }
129    
130    @Override
131    public ViewItemContainer getAdvancedCriteria(Map<String, Object> contextualParameters)
132    {
133        if (super.getAdvancedCriteria(contextualParameters).getViewItems().isEmpty())
134        {
135            setAdvancedCriteria(new View());
136        }
137        
138        return super.getAdvancedCriteria(contextualParameters);
139    }
140    
141    @Override
142    public ViewItemContainer getResultItems(Map<String, Object> contextualParameters)
143    {
144        if (super.getResultItems(contextualParameters).getViewItems().isEmpty())
145        {
146            String contentTypeId = getContentTypes(contextualParameters).iterator().next();
147            ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
148            
149            ViewItemContainer resultItems = _getResultItems(contentType);
150            setResultItems(resultItems);
151        }
152        
153        return super.getResultItems(contextualParameters);
154    }
155    
156    /**
157     * Retrieves the criteria for the given content type
158     * @param contentType The content type
159     * @return the criteria
160     */
161    protected ViewItemContainer _getCriteria(ContentType contentType)
162    {
163        View view = Optional.ofNullable(contentType.getView("criteria"))
164                            .orElse(contentType.getView("main"));
165        
166        View criteria = new View();
167        SimpleViewItemGroup group = new SimpleViewItemGroup();
168        group.setRole(ViewItemGroup.FIELDSET_ROLE);
169        criteria.addViewItem(group);
170        
171        group.addViewItems(_createReferencingCriteria(contentType, view.getViewItems()));
172        
173        if (_hierarchicalReferenceTableContentsHelper.isHierarchical(contentType) && _hierarchicalReferenceTableContentsHelper.supportCandidates(contentType))
174        {
175            SearchModelCriterionViewItem excludeCandidateCriterion = createExcludeCandidateCriterion();
176            group.addViewItem(excludeCandidateCriterion);
177        }
178
179        SearchModelCriterionViewItem contributorCriterion = _searchModelCriterionViewItemHelper.createReferencingCriterionViewItem(this, "contributor");
180        group.addViewItem(contributorCriterion);
181        
182        if (!contentType.isMultilingual())
183        {
184            SearchModelCriterionViewItem contentLanguageCriterion = createContentLanguageCriterion();
185            group.addViewItem(contentLanguageCriterion);
186        }
187        
188        return criteria;
189    }
190    
191    /**
192     * Copy the given view items and filter to keep only items that can be used in {@link SearchUIColumn}s.
193     * Also copy the children of view item accessors
194     * @param contentType the content type
195     * @param viewItems the view items to copy
196     * @return the view items copies
197     */
198    protected List<ViewItem> _createReferencingCriteria(ContentType contentType, List<ViewItem> viewItems)
199    {
200        List<ViewItem> criteria = new ArrayList<>();
201        
202        for (ViewItem viewItem : viewItems)
203        {
204            if (viewItem instanceof ViewItemContainer viewItemContainer)
205            {
206                criteria.addAll(_createReferencingCriteria(contentType, viewItemContainer.getViewItems()));
207            }
208            else if (viewItem instanceof ViewElement viewElement && _filterModelItemForCriteria(viewElement.getDefinition()))
209            {
210                ElementDefinition definition = viewElement.getDefinition();
211                ViewItem criterion = createReferencingCriterionViewItem(contentType, definition);
212                if (criterion != null)
213                {
214                    criteria.add(criterion);
215                }
216                
217                if (Content.ATTRIBUTE_TITLE.equals(definition.getName()))
218                {
219                    ViewItem likeTitleCriterion = createLikeTitleCriterion(definition);
220                    criteria.add(likeTitleCriterion);
221                }
222            }
223        }
224        
225        return criteria;
226    }
227
228    /** 
229     * Retrieves the criteria used to exclude candidates
230     * @return the criteria used to exclude candidates
231     */
232    protected SearchModelCriterionViewItem createExcludeCandidateCriterion()
233    {
234        SearchModelCriterionViewItem criterionViewItem = _searchModelCriterionViewItemHelper.createReferencingCriterionViewItem(this, "mixins");
235        
236        @SuppressWarnings("unchecked")
237        SearchModelCriterionDefinition<String> criterion = (SearchModelCriterionDefinition<String>) criterionViewItem.getDefinition();
238        criterion.setOperator(Operator.NE);
239        criterion.setWidget("edition.hidden");
240        criterion.setParsedDefaultValues(List.of(new ImmutablePair<>(null, "org.ametys.cms.referencetable.mixin.Candidate")));
241        
242        return criterionViewItem;
243    }
244    
245    /**
246     * Returns <code>true</code> if the model item can be used as criteria
247     * @param modelItem the model item
248     * @return <code>true</code> if the model item can be used as criteria
249     */
250    @SuppressWarnings("static-access")
251    protected boolean _filterModelItemForCriteria(ModelItem modelItem)
252    {
253        String typeId = modelItem.getType().getId();
254        switch (typeId)
255        {
256            case ModelItemTypeConstants.STRING_TYPE_ID:
257            case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID:
258            case ModelItemTypeConstants.DATE_TYPE_ID:
259            case ModelItemTypeConstants.DATETIME_TYPE_ID:
260            case ModelItemTypeConstants.LONG_TYPE_ID:
261            case ModelItemTypeConstants.DOUBLE_TYPE_ID:
262            case ModelItemTypeConstants.BOOLEAN_TYPE_ID:
263            case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID:
264            case ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID:
265            case ModelItemTypeConstants.USER_ELEMENT_TYPE_ID:
266                return true;
267            case ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID:
268            case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID:
269            case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID:
270            case ModelItemTypeConstants.GEOCODE_ELEMENT_TYPE_ID:
271            case ModelItemTypeConstants.COMPOSITE_TYPE_ID:
272            case ModelItemTypeConstants.REPEATER_TYPE_ID:
273            default:
274                return false;
275        }
276    }
277    
278    /**
279     * Retrieves the title attribute criterion, with a 'LIKE' operator and an hidden widget
280     * @param titleReference the title reference
281     * @return the like title criterion 
282     */
283    protected SearchModelCriterionViewItem createLikeTitleCriterion(ElementDefinition titleReference)
284    {
285        SearchModelCriterionViewItem criterionViewItem = _searchModelCriterionViewItemHelper.createReferencingCriterionViewItem(this, titleReference, Content.ATTRIBUTE_TITLE);
286        
287        SearchModelCriterionDefinition criterion = (SearchModelCriterionDefinition) criterionViewItem.getDefinition();
288        criterion.setOperator(Operator.LIKE);
289        criterion.setWidget("edition.hidden");
290        
291        return criterionViewItem;
292    }
293    
294    /**
295     * Retrieves a {@link SearchModelCriterionViewItem} for referencing the given definition
296     * @param contentType the simple content type.
297     * @param reference the referenced definition.
298     * @return the criterion
299     */
300    protected SearchModelCriterionViewItem createReferencingCriterionViewItem(ContentType contentType, ElementDefinition reference)
301    {
302        SearchModelCriterionViewItem criterionViewItem = _searchModelCriterionViewItemHelper.createReferencingCriterionViewItem(this, reference, reference.getPath());
303        SearchModelCriterionDefinition criterion = (SearchModelCriterionDefinition) criterionViewItem.getDefinition();
304        
305        if (
306                ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(reference.getType().getId()) 
307                && contentType.isReferenceTable() 
308                && contentType.getParentAttributeDefinition()
309                    .map(ModelItem::getPath)
310                    .map(parent -> parent.equals(reference.getPath()))
311                    .orElse(false)
312            )
313        {
314            criterion.setWidget("edition.select-referencetable-content");
315            
316            Map<String, I18nizableText> widgetParameters = criterion.getWidgetParameters();
317            widgetParameters.put("allowToggleAutoposting", new I18nizableText(String.valueOf(true)));
318        }
319        
320        return criterionViewItem;
321    }
322    
323    /** 
324     * Retrieves the criteria for content language
325     * @return the criteria for content language
326     */
327    protected SearchModelCriterionViewItem createContentLanguageCriterion()
328    {
329        SearchModelCriterionViewItem criterionViewItem = _searchModelCriterionViewItemHelper.createReferencingCriterionViewItem(this, "contentLanguage");
330        
331        // FIXME For now the simple contents are only created for language 'fr'
332        @SuppressWarnings("unchecked")
333        ElementDefinition<String> criterion = (ElementDefinition<String>) criterionViewItem.getDefinition();
334        criterion.setWidget("edition.select-language");
335        criterion.setParsedDefaultValues(List.of(new ImmutablePair<>(null, "CURRENT")));
336        criterion.setValidator(new DefaultValidator(null, true));
337        
338        return criterionViewItem;
339    }
340    
341    /**
342     * Retrieves the result items for the given content type
343     * @param contentType The content type
344     * @return the result items
345     */
346    protected ViewItemContainer _getResultItems(ContentType contentType)
347    {
348        View view = Optional.ofNullable(contentType.getView("columns"))
349                            .orElse(contentType.getView("main"));
350        
351        View resultItems = new View();
352        resultItems.addViewItems(_copyAndFilterViewItemsForColumns(view.getViewItems()));
353        
354        resultItems.addViewItem(SearchUIColumnHelper.createModelItemColumn(_systemPropertyExtensionPoint.getExtension("contributor")));
355        resultItems.addViewItem(SearchUIColumnHelper.createModelItemColumn(_systemPropertyExtensionPoint.getExtension("lastModified")));
356        
357        if (!contentType.isMultilingual())
358        {
359            resultItems.addViewItem(SearchUIColumnHelper.createModelItemColumn(_systemPropertyExtensionPoint.getExtension("contentLanguage")));
360        }
361        
362        return resultItems;
363    }
364    
365    /**
366     * Copy the given view items and filter to keep only items that can be used in {@link SearchUIColumn}s.
367     * Also copy the children of view item accessors
368     * @param viewItems the view items to copy
369     * @return the view items copies
370     */
371    protected List<ViewItem> _copyAndFilterViewItemsForColumns(List<ViewItem> viewItems)
372    {
373        List<ViewItem> copies = new ArrayList<>();
374        
375        for (ViewItem viewItem : viewItems)
376        {
377            if (!(viewItem instanceof ViewElement) || _filterModelItemForColumn(((ViewElement) viewItem).getDefinition()))
378            {
379                ViewItem copy = viewItem.createInstance();
380                if (viewItem instanceof ViewItemAccessor viewItemAccessor && !viewItemAccessor.getViewItems().isEmpty())
381                {
382                    assert copy instanceof ViewItemAccessor;
383                    ((ViewItemAccessor) copy).addViewItems(_copyAndFilterViewItemsForColumns(viewItemAccessor.getViewItems()));
384                }
385                else if (viewItem instanceof ModelViewItem modelViewItem)
386                {
387                    // If the view item is a leaf, create a column
388                    ModelItem modelItem = modelViewItem.getDefinition();
389                    copy = SearchUIColumnHelper.createModelItemColumn(modelItem);
390                }
391                
392                viewItem.copyTo(copy);
393                copies.add(copy);
394            }
395        }
396        
397        return copies;
398    }
399    
400    /**
401     * Returns <code>true</code> if model item can be used as column search UI
402     * @param modelItem the model item
403     * @return <code>true</code> if model item can be used as column search UI
404     */
405    @SuppressWarnings("static-access")
406    protected boolean _filterModelItemForColumn(ModelItem modelItem)
407    {
408        String typeId = modelItem.getType().getId();
409        switch (typeId)
410        {
411            case ModelItemTypeConstants.STRING_TYPE_ID:
412            case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID:
413            case ModelItemTypeConstants.DATE_TYPE_ID:
414            case ModelItemTypeConstants.DATETIME_TYPE_ID:
415            case ModelItemTypeConstants.LONG_TYPE_ID:
416            case ModelItemTypeConstants.DOUBLE_TYPE_ID:
417            case ModelItemTypeConstants.BOOLEAN_TYPE_ID:
418            case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID:
419            case ModelItemTypeConstants.GEOCODE_ELEMENT_TYPE_ID:
420            case ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID:
421            case ModelItemTypeConstants.USER_ELEMENT_TYPE_ID:
422                return true;
423            case ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID:
424            case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID:
425            case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID:
426            default:
427                return false;
428        }
429    }
430}