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.setAttribute("id", initialConfiguration.getAttribute("id"));
138        finalConfiguration.addChild(initialConfiguration.getChild("label"));
139        finalConfiguration.addChild(initialConfiguration.getChild("criteriaPosition"));
140        finalConfiguration.addChild(buildParametersConfiguration(initialConfiguration));
141        return finalConfiguration;
142    }
143    
144    /**
145     * Builds the configuration object for parameters
146     * @param initialConfiguration The source configuration
147     * @return the configuration object for parameters
148     * @throws ConfigurationException If some values or attributes cannot be retrieved
149     */
150    protected Configuration buildParametersConfiguration(Configuration initialConfiguration) throws ConfigurationException
151    {
152        Configuration sourceParameter = initialConfiguration.getChild("contentTypeParameter");
153        
154        DefaultConfiguration parameters = new DefaultConfiguration("parameters");
155        DefaultConfiguration targetParameter = new DefaultConfiguration("parameter");
156        parameters.addChild(targetParameter);
157        targetParameter.setAttribute("name", _parameterSubContentTypeUniqueName);
158        targetParameter.setAttribute("type", "string");
159        targetParameter.setAttribute("multiple", "true");
160        targetParameter.setAttribute("reloadCriteriaOnChange", "true");
161        if (sourceParameter.getAttribute("order", null) != null)
162        {
163            targetParameter.setAttribute("order", sourceParameter.getAttribute("order"));
164        }
165        
166        targetParameter.addChild(sourceParameter.getChild("label"));
167        targetParameter.addChild(sourceParameter.getChild("description"));
168        
169        DefaultConfiguration disableConditions = new DefaultConfiguration("disable-conditions");
170        targetParameter.addChild(disableConditions);
171        disableConditions.setAttribute("type", "and");
172        DefaultConfiguration disableCondition = new DefaultConfiguration("condition");
173        disableConditions.addChild(disableCondition);
174        disableCondition.setAttribute("id", "returnables");
175        disableCondition.setAttribute("operator", "neq");
176        disableCondition.setValue(_associatedContentReturnableRole);
177        
178        DefaultConfiguration widget = new DefaultConfiguration("widget");
179        targetParameter.addChild(widget);
180        widget.setValue("edition.select-content-types");
181        
182        DefaultConfiguration widgetParams = new DefaultConfiguration("widget-params");
183        targetParameter.addChild(widgetParams);
184        DefaultConfiguration emptyTextParam = new DefaultConfiguration("param");
185        widgetParams.addChild(emptyTextParam);
186        emptyTextParam.setAttribute("name", "emptyText");
187        emptyTextParam.setAttribute("i18n", "true");
188        emptyTextParam.setValue("plugin.cms:WIDGET_COMBOBOX_ALL_OPTIONS");
189        
190        DefaultConfiguration enumeration = new DefaultConfiguration("enumeration");
191        targetParameter.addChild(enumeration);
192        DefaultConfiguration customEnumerator = new DefaultConfiguration("custom-enumerator");
193        enumeration.addChild(customEnumerator);
194        customEnumerator.setAttribute("class", ContentTypeEnumerator.class.getName());
195        DefaultConfiguration excludePrivate = new DefaultConfiguration("excludePrivate");
196        customEnumerator.addChild(excludePrivate);
197        excludePrivate.setValue("false");
198        DefaultConfiguration contentTypeConf = new DefaultConfiguration("contentTypes");
199        customEnumerator.addChild(contentTypeConf);
200        contentTypeConf.setValue(String.join(",", _baseContentTypes));
201        
202        return parameters;
203    }
204    
205    @Override
206    public void initialize() throws Exception
207    {
208        _initCriterionDefinitionPrefix();
209        super.initialize();
210    }
211    
212    @Override
213    protected String associatedContentReturnableRole()
214    {
215        return _associatedContentReturnableRole;
216    }
217    
218    @Override
219    protected String getCriterionDefinitionPrefix()
220    {
221        return _criterionDefinitionPrefix;
222    }
223    
224    @Override
225    public Collection<Returnable> relationsWith()
226    {
227        return Collections.singleton(_associatedContentReturnable);
228    }
229
230    @Override
231    protected Collection<String> getContentTypes(AdditionalParameterValueMap additionalParameterValues)
232    {
233        Collection<String> contentTypes = additionalParameterValues.getValue(_parameterSubContentTypeUniqueName);
234        return CollectionUtils.isNotEmpty(contentTypes)
235                ? contentTypes
236                : _baseContentTypes;
237    }
238    
239    @Override
240    public Optional<Query> joinQuery(Query queryOnCriterion, SearchCriterionDefinition criterion, Collection<Returnable> returnables, AdditionalParameterValueMap additionalParameters)
241    {
242        if (returnables.contains(_associatedContentReturnable))
243        {
244            return Optional.of(queryOnCriterion);
245        }
246        else
247        {
248            return Optional.empty();
249        }
250    }
251    
252    /**
253     * Initializes {@link #_avalonRole} field from configuration
254     * @param configuration The configuration
255     * @throws ConfigurationException If a configuration value cannot be retrieved
256     */
257    protected void _initAvalonRole(Configuration configuration) throws ConfigurationException
258    {
259        _avalonRole = configuration.getAttribute("id");
260    }
261    
262    /**
263     * Initializes {@link #_shortAvalonRole} field
264     */
265    protected void _initShortAvalonRole()
266    {
267        // Pray for shortId to be unique. If not, method visibilities are protected for implementing you own logic.
268        _shortAvalonRole = StringUtils.substringAfterLast(_avalonRole, ".");
269    }
270    
271    /**
272     * Initializes {@link #_parameterSubContentTypeUniqueName} field
273     */
274    protected void _initParameterSubContentTypeUniqueName()
275    {
276        _parameterSubContentTypeUniqueName = _shortAvalonRole + __PARAMETER_SUB_CONTENT_TYPES_SUFFIX;
277    }
278    
279    /**
280     * Initializes {@link #_associatedContentReturnableRole} field from configuration
281     * @param configuration The configuration
282     * @throws ConfigurationException If a configuration value cannot be retrieved
283     */
284    protected void _configureAssociatedContentReturnableRole(Configuration configuration) throws ConfigurationException
285    {
286        _associatedContentReturnableRole = configuration.getChild("associatedReturnable").getValue();
287    }
288    
289    /**
290     * Initializes {@link #_baseContentTypes} field from configuration
291     * @param configuration The configuration
292     * @throws ConfigurationException If a configuration value cannot be retrieved
293     */
294    protected void _configureBaseContentTypes(Configuration configuration) throws ConfigurationException
295    {
296        Configuration[] baseContentTypeConfs = configuration.getChildren("baseContentType");
297        _baseContentTypes = Stream.of(baseContentTypeConfs)
298                .map(LambdaUtils.wrap(Configuration::getValue))
299                .peek(cTypeId ->
300                {
301                    if (!_isHandledContentType(cTypeId))
302                    {
303                        getLogger().warn("The baseContentType '{}' for '{}' does not exist or is not private", cTypeId, _avalonRole);
304                    }
305                })
306                .filter(this::_isHandledContentType)
307                .collect(Collectors.toList());
308        
309        if (_baseContentTypes.isEmpty())
310        {
311            throw new IllegalArgumentException("The baseContentType configurations are missing or are not valid.");
312        }
313    }
314    
315    private boolean _isHandledContentType(String cTypeId)
316    {
317        return _cTypeEP.hasExtension(cTypeId) && _cTypeEP.getExtension(cTypeId).isPrivate();
318    }
319    
320    /**
321     * Initializes {@link #_criterionDefinitionPrefix} field
322     */
323    protected void _initCriterionDefinitionPrefix()
324    {
325        _criterionDefinitionPrefix = _shortAvalonRole + "$";
326    }
327}