001/*
002 *  Copyright 2019 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.web.frontoffice.search.metamodel.impl;
017
018import java.util.Collection;
019import java.util.Collections;
020import java.util.Optional;
021import java.util.stream.Collectors;
022import java.util.stream.Stream;
023
024import org.apache.avalon.framework.configuration.Configuration;
025import org.apache.avalon.framework.configuration.ConfigurationException;
026import org.apache.avalon.framework.configuration.DefaultConfiguration;
027import org.apache.commons.collections4.CollectionUtils;
028import org.apache.commons.lang3.StringUtils;
029
030import org.ametys.cms.contenttype.ContentType;
031import org.ametys.cms.contenttype.ContentTypeEnumerator;
032import org.ametys.cms.search.query.Query;
033import org.ametys.core.util.LambdaUtils;
034import org.ametys.web.frontoffice.search.metamodel.AdditionalParameterValueMap;
035import org.ametys.web.frontoffice.search.metamodel.Returnable;
036import org.ametys.web.frontoffice.search.metamodel.SearchCriterionDefinition;
037
038/**
039 * This class is a generic searchable to be used to search on a {@link ContentType#isPrivate() private} {@link ContentType}.
040 * <br>You <b>must</b> associate it with a {@link Returnable} which is or extends {@link PrivateContentReturnable}
041 * <pre>
042 * &lt;extension point="org.ametys.web.frontoffice.search.metamodel.SearchableExtensionPoint"
043 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;id="my.searchable.id"
044 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;class="org.ametys.web.frontoffice.search.metamodel.impl.PrivateContentSearchable"&gt;
045 * &nbsp;&nbsp;&nbsp;&nbsp;&lt;label i18n="true"&gt;...&lt;/label&gt;
046 * &nbsp;&nbsp;&nbsp;&nbsp;&lt;criteriaPosition&gt;11&lt;/criteriaPosition&gt;
047 * &nbsp;&nbsp;&nbsp;&nbsp;&lt;associatedReturnable&gt;my.returnable.id&lt;/associatedReturnable&gt;
048 * &nbsp;&nbsp;&nbsp;&nbsp;&lt;baseContentType&gt;my.content.type.1&lt;/baseContentType&gt;
049 * &nbsp;&nbsp;&nbsp;&nbsp;&lt;baseContentType&gt;my.content.type.2&lt;/baseContentType&gt;
050 * &nbsp;&nbsp;&nbsp;&nbsp;&lt;contentTypeParameter order="99"&gt;
051 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;label i18n="true"&gt;...&lt;/label&gt;
052 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;description i18n="true"&gt;...&lt;/description&gt;
053 * &nbsp;&nbsp;&nbsp;&nbsp;&lt;/contentTypeParameter&gt;
054 * &lt;/extension&gt;
055 * </pre>
056 */
057public class PrivateContentSearchable extends AbstractContentBasedSearchable
058{
059    private static final String __PARAMETER_SUB_CONTENT_TYPES_SUFFIX = "-subContentTypes";
060    
061    /** The Avalon role of this Searchable */
062    protected String _avalonRole;
063    /** The "short version" of the Avalon role of this Searchable */
064    protected String _shortAvalonRole;
065    /** The unique name accross the whole application for the parameter for this searchable to select the sub content types. */
066    protected String _parameterSubContentTypeUniqueName;
067    /** The associated content returnable role */
068    protected String _associatedContentReturnableRole;
069    /** The {@link #getCriterionDefinitionPrefix() prefix} for criterion definitions */
070    protected String _criterionDefinitionPrefix;
071    /** The base private content types of this searchable */
072    protected Collection<String> _baseContentTypes;
073    
074    @Override
075    public void configure(Configuration configuration) throws ConfigurationException
076    {
077        _initAvalonRole(configuration);
078        _initShortAvalonRole();
079        _initParameterSubContentTypeUniqueName();
080        _configureAssociatedContentReturnableRole(configuration);
081        _configureBaseContentTypes(configuration);
082        Configuration finalConfiguration = buildFinalConfiguration(configuration);
083        super.configure(finalConfiguration);
084    }
085    
086    /**
087     * Builds the final configuration object which will be passed to the superclass, from the "real" XML configuration
088     * @param initialConfiguration The source configuration
089     * @return the built configuration object
090     * @throws ConfigurationException If some values or attributes cannot be retrieved
091     */
092    protected Configuration buildFinalConfiguration(Configuration initialConfiguration) throws ConfigurationException
093    {
094        /*
095         * From the input configuration:
096         * 
097         * <extension point="org.ametys.web.frontoffice.search.metamodel.SearchableExtensionPoint"
098         *            id="{org.ametys...MyCustomSearchable}"
099         *            class="org.ametys.web.frontoffice.search.metamodel.impl.PrivateContentSearchable">
100         *     <label i18n="true">...</label>
101         *     <criteriaPosition>...</criteriaPosition>
102         *     <associatedReturnable>{org.ametys...MyCustomAssociatedReturnable}</associatedReturnable>
103         *     <baseContentType>my.content.type.1</baseContentType>
104         *     <baseContentType>my.content.type.2</baseContentType>
105         *     <contentTypeParameter order="10">
106         *         <label i18n="true">...</label>
107         *         <description i18n="true">...</description>
108         *      </contentTypeParameter>
109         * </extension>
110         * 
111         * generates an automatic configuration:
112         * 
113         * <extension>
114         *     <label i18n="true">...</label>
115         *     <criteriaPosition>...</criteriaPosition>
116         *     <parameters>
117         *         <parameter name="{MyCustomSearchable}$subContentTypes" type="string" multiple="true" reloadCriteriaOnChange="true" order="10">
118         *             <label i18n="true">...</label>
119         *             <disable-conditions type="and">
120         *                 <condition id="returnables" operator="neq">{org.ametys...MyCustomAssociatedReturnable}</condition>
121         *             </disable-conditions>
122         *             <widget>edition.select-content-types</widget>
123         *             <widget-params>
124         *                 <param name="emptyText" i18n="true">plugin.cms:WIDGET_COMBOBOX_ALL_OPTIONS</param>
125         *             </widget-params>
126         *             <enumeration>
127         *                 <custom-enumerator class="org.ametys.cms.contenttype.ContentTypeEnumerator">
128         *                     <excludePrivate>false</excludePrivate>
129         *                     <contentTypes>my.content.type.1,my.content.type.2</contentTypes>
130         *                  </custom-enumerator>
131         *              </enumeration>
132         *          </parameter>
133         *     </parameters>
134         * </extension>
135         */
136        DefaultConfiguration finalConfiguration = new DefaultConfiguration("extension");
137        finalConfiguration.addChild(initialConfiguration.getChild("label"));
138        finalConfiguration.addChild(initialConfiguration.getChild("criteriaPosition"));
139        finalConfiguration.addChild(buildParametersConfiguration(initialConfiguration));
140        return finalConfiguration;
141    }
142    
143    /**
144     * Builds the configuration object for parameters
145     * @param initialConfiguration The source configuration
146     * @return the configuration object for parameters
147     * @throws ConfigurationException If some values or attributes cannot be retrieved
148     */
149    protected Configuration buildParametersConfiguration(Configuration initialConfiguration) throws ConfigurationException
150    {
151        Configuration sourceParameter = initialConfiguration.getChild("contentTypeParameter");
152        
153        DefaultConfiguration parameters = new DefaultConfiguration("parameters");
154        DefaultConfiguration targetParameter = new DefaultConfiguration("parameter");
155        parameters.addChild(targetParameter);
156        targetParameter.setAttribute("name", _parameterSubContentTypeUniqueName);
157        targetParameter.setAttribute("type", "string");
158        targetParameter.setAttribute("multiple", "true");
159        targetParameter.setAttribute("reloadCriteriaOnChange", "true");
160        if (sourceParameter.getAttribute("order", null) != null)
161        {
162            targetParameter.setAttribute("order", sourceParameter.getAttribute("order"));
163        }
164        
165        targetParameter.addChild(sourceParameter.getChild("label"));
166        targetParameter.addChild(sourceParameter.getChild("description"));
167        
168        DefaultConfiguration disableConditions = new DefaultConfiguration("disable-conditions");
169        targetParameter.addChild(disableConditions);
170        disableConditions.setAttribute("type", "and");
171        DefaultConfiguration disableCondition = new DefaultConfiguration("condition");
172        disableConditions.addChild(disableCondition);
173        disableCondition.setAttribute("id", "returnables");
174        disableCondition.setAttribute("operator", "neq");
175        disableCondition.setValue(_associatedContentReturnableRole);
176        
177        DefaultConfiguration widget = new DefaultConfiguration("widget");
178        targetParameter.addChild(widget);
179        widget.setValue("edition.select-content-types");
180        
181        DefaultConfiguration widgetParams = new DefaultConfiguration("widget-params");
182        targetParameter.addChild(widgetParams);
183        DefaultConfiguration emptyTextParam = new DefaultConfiguration("param");
184        widgetParams.addChild(emptyTextParam);
185        emptyTextParam.setAttribute("name", "emptyText");
186        emptyTextParam.setAttribute("i18n", "true");
187        emptyTextParam.setValue("plugin.cms:WIDGET_COMBOBOX_ALL_OPTIONS");
188        
189        DefaultConfiguration enumeration = new DefaultConfiguration("enumeration");
190        targetParameter.addChild(enumeration);
191        DefaultConfiguration customEnumerator = new DefaultConfiguration("custom-enumerator");
192        enumeration.addChild(customEnumerator);
193        customEnumerator.setAttribute("class", ContentTypeEnumerator.class.getName());
194        DefaultConfiguration excludePrivate = new DefaultConfiguration("excludePrivate");
195        customEnumerator.addChild(excludePrivate);
196        excludePrivate.setValue("false");
197        DefaultConfiguration contentTypeConf = new DefaultConfiguration("contentTypes");
198        customEnumerator.addChild(contentTypeConf);
199        contentTypeConf.setValue(String.join(",", _baseContentTypes));
200        
201        return parameters;
202    }
203    
204    @Override
205    public void initialize() throws Exception
206    {
207        _initCriterionDefinitionPrefix();
208        super.initialize();
209    }
210    
211    @Override
212    protected String associatedContentReturnableRole()
213    {
214        return _associatedContentReturnableRole;
215    }
216    
217    @Override
218    protected String getCriterionDefinitionPrefix()
219    {
220        return _criterionDefinitionPrefix;
221    }
222    
223    @Override
224    public Collection<Returnable> relationsWith()
225    {
226        return Collections.singleton(_associatedContentReturnable);
227    }
228
229    @Override
230    protected Collection<String> getContentTypes(AdditionalParameterValueMap additionalParameterValues)
231    {
232        Collection<String> contentTypes = additionalParameterValues.getValue(_parameterSubContentTypeUniqueName);
233        return CollectionUtils.isNotEmpty(contentTypes)
234                ? contentTypes
235                : _baseContentTypes;
236    }
237    
238    @Override
239    public Optional<Query> joinQuery(Query queryOnCriterion, SearchCriterionDefinition criterion, Collection<Returnable> returnables, AdditionalParameterValueMap additionalParameters)
240    {
241        if (returnables.contains(_associatedContentReturnable))
242        {
243            return Optional.of(queryOnCriterion);
244        }
245        else
246        {
247            return Optional.empty();
248        }
249    }
250    
251    /**
252     * Initializes {@link #_avalonRole} field from configuration
253     * @param configuration The configuration
254     * @throws ConfigurationException If a configuration value cannot be retrieved
255     */
256    protected void _initAvalonRole(Configuration configuration) throws ConfigurationException
257    {
258        _avalonRole = configuration.getAttribute("id");
259    }
260    
261    /**
262     * Initializes {@link #_shortAvalonRole} field
263     */
264    protected void _initShortAvalonRole()
265    {
266        // Pray for shortId to be unique. If not, method visibilities are protected for implementing you own logic.
267        _shortAvalonRole = StringUtils.substringAfterLast(_avalonRole, ".");
268    }
269    
270    /**
271     * Initializes {@link #_parameterSubContentTypeUniqueName} field
272     */
273    protected void _initParameterSubContentTypeUniqueName()
274    {
275        _parameterSubContentTypeUniqueName = _shortAvalonRole + __PARAMETER_SUB_CONTENT_TYPES_SUFFIX;
276    }
277    
278    /**
279     * Initializes {@link #_associatedContentReturnableRole} field from configuration
280     * @param configuration The configuration
281     * @throws ConfigurationException If a configuration value cannot be retrieved
282     */
283    protected void _configureAssociatedContentReturnableRole(Configuration configuration) throws ConfigurationException
284    {
285        _associatedContentReturnableRole = configuration.getChild("associatedReturnable").getValue();
286    }
287    
288    /**
289     * Initializes {@link #_baseContentTypes} field from configuration
290     * @param configuration The configuration
291     * @throws ConfigurationException If a configuration value cannot be retrieved
292     */
293    protected void _configureBaseContentTypes(Configuration configuration) throws ConfigurationException
294    {
295        Configuration[] baseContentTypeConfs = configuration.getChildren("baseContentType");
296        _baseContentTypes = Stream.of(baseContentTypeConfs)
297                .map(LambdaUtils.wrap(Configuration::getValue))
298                .peek(cTypeId ->
299                {
300                    if (!_isHandledContentType(cTypeId))
301                    {
302                        getLogger().warn("The baseContentType '{}' for '{}' does not exist or is not private", cTypeId, _avalonRole);
303                    }
304                })
305                .filter(this::_isHandledContentType)
306                .collect(Collectors.toList());
307        
308        if (_baseContentTypes.isEmpty())
309        {
310            throw new IllegalArgumentException("The baseContentType configurations are missing or are not valid.");
311        }
312    }
313    
314    private boolean _isHandledContentType(String cTypeId)
315    {
316        return _cTypeEP.hasExtension(cTypeId) && _cTypeEP.getExtension(cTypeId).isPrivate();
317    }
318    
319    /**
320     * Initializes {@link #_criterionDefinitionPrefix} field
321     */
322    protected void _initCriterionDefinitionPrefix()
323    {
324        _criterionDefinitionPrefix = _shortAvalonRole + "$";
325    }
326}