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