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