001/*
002 *  Copyright 2023 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.model;
017
018import java.util.Collections;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.Locale;
022import java.util.Map;
023import java.util.Optional;
024import java.util.Set;
025
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.configuration.Configuration;
028import org.apache.avalon.framework.configuration.ConfigurationException;
029import org.apache.avalon.framework.configuration.DefaultConfiguration;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033
034import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
035import org.ametys.cms.contenttype.MetadataType;
036import org.ametys.cms.repository.Content;
037import org.ametys.cms.search.query.Query.Operator;
038import org.ametys.core.user.User;
039import org.ametys.core.user.UserIdentity;
040import org.ametys.core.user.UserManager;
041import org.ametys.core.user.population.UserPopulationDAO;
042import org.ametys.plugins.repository.AmetysObjectResolver;
043import org.ametys.runtime.i18n.I18nizableText;
044import org.ametys.runtime.i18n.I18nizableTextParameter;
045import org.ametys.runtime.parameter.Enumerator;
046import org.ametys.runtime.plugin.component.AbstractLogEnabled;
047
048/**
049 * Helper for {@link SearchCriterion}
050 */
051public class SearchCriterionHelper extends AbstractLogEnabled implements Component, Serviceable
052{
053    /** The component role. */
054    public static final String ROLE = SearchCriterionHelper.class.getName();
055    
056    /** The ametys object resolver. */
057    protected AmetysObjectResolver _resolver;
058    /** The content type extension point */
059    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
060    /** The searchModelHelper */
061    protected SearchModelHelper _searchModelHelper;
062    /** The user manager. */
063    protected UserManager _userManager;
064    /** The user population DAO */
065    protected UserPopulationDAO _userPopulationDAO;
066    
067    public void service(ServiceManager manager) throws ServiceException
068    {
069        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
070        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
071        _searchModelHelper = (SearchModelHelper) manager.lookup(SearchModelHelper.ROLE);
072        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
073        _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE);
074    }
075    
076    /**
077     * Get the configuration of a criteria component.
078     * @param originalConf the optional original criteria configuration.
079     * @param baseContentTypeIds the "base" content type identifiers.
080     * @param path the field path.
081     * @param operator the optional criteria operator
082     * @param group the optional group
083     * @return the configuration to provide to the criteria component.
084     * @throws ConfigurationException if an error occurs.
085     */
086    public Configuration getIndexingFieldCriteriaConfiguration(Optional<Configuration> originalConf, Set<String> baseContentTypeIds, String path, Optional<Operator> operator, Optional<I18nizableText> group) throws ConfigurationException
087    {
088        return _getIndexingFieldCriteriaConfiguration(Optional.empty(), originalConf, baseContentTypeIds, path, operator, group);
089    }
090    
091    /**
092     * Get the configuration of a criteria component.
093     * @param searchModel the search model.
094     * @param originalConf the optional original criteria configuration.
095     * @param baseContentTypeIds the "base" content type identifiers.
096     * @param path the field path.
097     * @param operator the optional criteria operator
098     * @param group the optional group
099     * @return the configuration to provide to the criteria component.
100     * @throws ConfigurationException if an error occurs.
101     */
102    public Configuration getIndexingFieldCriteriaConfiguration(SearchModel searchModel, Optional<Configuration> originalConf, Set<String> baseContentTypeIds, String path, Optional<Operator> operator, Optional<I18nizableText> group) throws ConfigurationException
103    {
104        return _getIndexingFieldCriteriaConfiguration(Optional.ofNullable(searchModel), originalConf, baseContentTypeIds, path, operator, group);
105    }
106    
107    private Configuration _getIndexingFieldCriteriaConfiguration(Optional<SearchModel> searchModel, Optional<Configuration> originalConf, Set<String> baseContentTypeIds, String path, Optional<Operator> operator, Optional<I18nizableText> group) throws ConfigurationException
108    {
109        DefaultConfiguration conf = originalConf.isPresent()
110                ? new DefaultConfiguration(originalConf.get())
111                : new DefaultConfiguration("criteria");
112        
113        DefaultConfiguration metaConf = new DefaultConfiguration("field");
114        conf.addChild(metaConf);
115        metaConf.setAttribute("path", path);
116        
117        if (operator.isPresent())
118        {
119            DefaultConfiguration operatorConf = new DefaultConfiguration("test-operator");
120            conf.addChild(operatorConf);
121            operatorConf.setValue(operator.get().getName());
122        }
123        
124        Configuration contentTypesConfiguration = _getContentTypesConfiguration(searchModel, baseContentTypeIds);
125        conf.addChild(contentTypesConfiguration);
126        
127        if (group.isPresent())
128        {
129            Configuration groupConfiguration = _getGroupConfiguration(group.get());
130            conf.addChild(groupConfiguration);
131        }
132        
133        return conf;
134    }
135    
136    /**
137     * Get the configuration of a system criteria component.
138     * @param originalConf the optional original criteria configuration.
139     * @param baseContentTypeIds the "base" content type identifiers.
140     * @param propertyId the system property identifier.
141     * @param group The optional group.
142     * @return the configuration to provide to the system criterion component.
143     * @throws ConfigurationException if an error occurs.
144     */
145    public Configuration getSystemCriteriaConfiguration(Optional<Configuration> originalConf, Set<String> baseContentTypeIds, String propertyId, Optional<I18nizableText> group) throws ConfigurationException
146    {
147        return _getSystemCriteriaConfiguration(Optional.empty(), originalConf, baseContentTypeIds, propertyId, group);
148    }
149    
150    /**
151     * Get the configuration of a system criteria component.
152     * @param searchModel the search model.
153     * @param originalConf the optional original criteria configuration.
154     * @param baseContentTypeIds the "base" content type identifiers.
155     * @param propertyId the system property identifier.
156     * @param group The optional group.
157     * @return the configuration to provide to the system criterion component.
158     * @throws ConfigurationException if an error occurs.
159     */
160    public Configuration getSystemCriteriaConfiguration(SearchModel searchModel, Optional<Configuration> originalConf, Set<String> baseContentTypeIds, String propertyId, Optional<I18nizableText> group) throws ConfigurationException
161    {
162        return _getSystemCriteriaConfiguration(Optional.ofNullable(searchModel), originalConf, baseContentTypeIds, propertyId, group);
163    }
164    
165    private Configuration _getSystemCriteriaConfiguration(Optional<SearchModel> searchModel, Optional<Configuration> originalConf, Set<String> baseContentTypeIds, String propertyId, Optional<I18nizableText> group) throws ConfigurationException
166    {
167        DefaultConfiguration conf = originalConf.isPresent()
168                ? new DefaultConfiguration(originalConf.get())
169                : new DefaultConfiguration("criteria");
170        
171        DefaultConfiguration propConf = new DefaultConfiguration("systemProperty");
172        propConf.setAttribute("name", propertyId);
173        conf.addChild(propConf);
174        
175        Configuration contentTypesConfiguration = _getContentTypesConfiguration(searchModel, baseContentTypeIds);
176        conf.addChild(contentTypesConfiguration);
177        
178        if (group.isPresent())
179        {
180            Configuration groupConfiguration = _getGroupConfiguration(group.get());
181            conf.addChild(groupConfiguration);
182        }
183        
184        return conf;
185    }
186    
187    /**
188     * Get the configuration of a custom criteria component.
189     * @param searchModel the search model.
190     * @param originalConf the original criteria configuration.
191     * @param baseContentTypeIds the "base" content type identifiers.
192     * @param customCriterionId the custom criterion id
193     * @param group The group. Can be null.
194     * @return the configuration to provide to the custom criterion component.
195     * @throws ConfigurationException if an error occurs.
196     */
197    public Configuration getCustomCriteriaConfiguration(SearchModel searchModel, Optional<Configuration> originalConf, Set<String> baseContentTypeIds, String customCriterionId, Optional<I18nizableText> group) throws ConfigurationException
198    {
199        DefaultConfiguration conf = originalConf.isPresent()
200                ? new DefaultConfiguration(originalConf.get())
201                : new DefaultConfiguration("criteria");
202        
203        DefaultConfiguration customCriterionConf = new DefaultConfiguration("customCriterion");
204        customCriterionConf.setAttribute("id", customCriterionId);
205        conf.addChild(customCriterionConf);
206        
207        Configuration contentTypesConfiguration = _getContentTypesConfiguration(Optional.ofNullable(searchModel), baseContentTypeIds);
208        conf.addChild(contentTypesConfiguration);
209        
210        if (group.isPresent())
211        {
212            Configuration groupConfiguration = _getGroupConfiguration(group.get());
213            conf.addChild(groupConfiguration);
214        }
215        
216        return conf;
217    }
218    
219    private Configuration _getContentTypesConfiguration(Optional<SearchModel> searchModel, Set<String> baseContentTypeIds)
220    {
221        DefaultConfiguration contentTypesConf = new DefaultConfiguration("contentTypes");
222
223        if (searchModel.isPresent())
224        {
225            Set<String> contentTypes = getAllContentTypes(searchModel.get(), Collections.emptyMap());
226            for (String contentType : contentTypes)
227            {
228                DefaultConfiguration cTypeConf = new DefaultConfiguration("type");
229                cTypeConf.setAttribute("id", contentType);
230                contentTypesConf.addChild(cTypeConf);
231            }
232        }
233        
234        if (baseContentTypeIds != null)
235        {
236            for (String baseContentTypeId : baseContentTypeIds)
237            {
238                DefaultConfiguration cTypeConf = new DefaultConfiguration("baseType");
239                cTypeConf.setAttribute("id", baseContentTypeId);
240                contentTypesConf.addChild(cTypeConf);
241            }
242        }
243        
244        return contentTypesConf;
245    }
246    
247    private Configuration _getGroupConfiguration(I18nizableText group)
248    {
249        DefaultConfiguration groupConf = new DefaultConfiguration("group");
250        groupConf.setAttribute("i18n", group.isI18n());
251        groupConf.setValue(group.isI18n() ? group.getCatalogue() + ":" + group.getKey() : group.getLabel());
252        return groupConf;
253    }
254    
255
256    
257    /**
258     * Get all the real content types that a model works on (the included content types, minus the excluded types).
259     * @param model the search model.
260     * @param contextualParameters the contextual parameters.
261     * @return a Set of the content type IDs.
262     */
263    public Set<String> getAllContentTypes(SearchModel model, Map<String, Object> contextualParameters)
264    {
265        Set<String> allContentTypes = new HashSet<>();
266        
267        Set<String> modelCTypes = model.getContentTypes(contextualParameters);
268        Set<String> modelExcludedCTypes = model.getExcludedContentTypes(contextualParameters);
269        
270        if (modelCTypes.isEmpty())
271        {
272            // Empty "declared" content types: the model works on all content types.
273            allContentTypes.addAll(_contentTypeExtensionPoint.getExtensionsIds());
274        }
275        else
276        {
277            // Otherwise, add all declared content types and their sub-types.
278            for (String cTypeId : modelCTypes)
279            {
280                allContentTypes.add(cTypeId);
281                allContentTypes.addAll(_contentTypeExtensionPoint.getSubTypes(cTypeId));
282            }
283        }
284        
285        // Remove all excluded types.
286        allContentTypes.removeAll(modelExcludedCTypes);
287        
288        return allContentTypes;
289    }
290
291    /**
292     * Get the label of a facet value for the given criterion.
293     * @param criterion the criterion
294     * @param value the facet value.
295     * @param currentLocale the current locale
296     * @return the label, or null if the value does not exist.
297     */
298    public I18nizableText getFacetLabel(SearchCriterion criterion, String value, Locale currentLocale)
299    {
300        I18nizableText label = null;
301        
302        try
303        {
304            MetadataType type = criterion.getType();
305            Enumerator enumerator = criterion.getEnumerator();
306            
307            if (type == MetadataType.CONTENT)
308            {
309                Content content = _resolver.resolveById(value);
310                label = new I18nizableText(content.getTitle(currentLocale));
311            }
312            else if (type == MetadataType.USER)
313            {
314                UserIdentity userIdentity = UserIdentity.stringToUserIdentity(value);
315                String login = userIdentity.getLogin();
316                String populationId = userIdentity.getPopulationId();
317                User user = _userManager.getUser(populationId, login);
318                if (user != null)
319                {
320                    // Default i18n key only use login, sortablename and population.
321                    // But we provide more parameters if the user want to override it.
322                    Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
323                    i18nParams.put("login", new I18nizableText(login));
324                    i18nParams.put("firstname", new I18nizableText(user.getFirstName()));
325                    i18nParams.put("lastname", new I18nizableText(user.getLastName()));
326                    i18nParams.put("fullname", new I18nizableText(user.getFullName()));
327                    i18nParams.put("sortablename", new I18nizableText(user.getSortableName()));
328                    i18nParams.put("population", _userPopulationDAO.getUserPopulation(populationId).getLabel());
329                    
330                    label = new I18nizableText("plugin.cms", "PLUGINS_CMS_UITOOL_SEARCH_FACET_USER_LABEL", i18nParams);
331                }
332                else
333                {
334                    Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
335                    i18nParams.put("login", new I18nizableText(login));
336                    i18nParams.put("population", new I18nizableText(populationId));
337                    
338                    label = new I18nizableText("plugin.cms", "PLUGINS_CMS_UITOOL_SEARCH_FACET_UNKNOWN_USER_LABEL", i18nParams);
339                }
340            }
341            else if (enumerator != null)
342            {
343                label = enumerator.getEntry(value);
344            }
345            else if (type == MetadataType.BOOLEAN)
346            {
347                boolean boolValue = "T".equals(value) /* if joined facet, value will be "F" or "T" */ || Boolean.valueOf(value);
348                label = new I18nizableText("plugin.cms", boolValue ? "PLUGINS_CMS_UITOOL_SEARCH_FACET_BOOLEAN_TRUE_LABEL" : "PLUGINS_CMS_UITOOL_SEARCH_FACET_BOOLEAN_FALSE_LABEL");
349            }
350        }
351        catch (Exception e)
352        {
353            // Ignore, just return null.
354            getLogger().warn("Unable to get facel label for value '{}'", value, e);
355        }
356        
357        return label;
358    }
359}