001/*
002 *  Copyright 2018 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;
017
018import java.util.Arrays;
019import java.util.Collection;
020import java.util.Comparator;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.LinkedHashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Map.Entry;
027import java.util.Objects;
028import java.util.Optional;
029import java.util.Set;
030import java.util.stream.Collectors;
031import java.util.stream.Stream;
032
033import org.apache.avalon.framework.activity.Disposable;
034import org.apache.avalon.framework.activity.Initializable;
035import org.apache.avalon.framework.component.Component;
036import org.apache.avalon.framework.configuration.Configuration;
037import org.apache.avalon.framework.configuration.DefaultConfiguration;
038import org.apache.avalon.framework.context.Context;
039import org.apache.avalon.framework.context.ContextException;
040import org.apache.avalon.framework.context.Contextualizable;
041import org.apache.avalon.framework.service.ServiceException;
042import org.apache.avalon.framework.service.ServiceManager;
043import org.apache.avalon.framework.service.Serviceable;
044
045import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
046import org.ametys.cms.search.model.SystemProperty;
047import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
048import org.ametys.core.util.LambdaUtils;
049import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
050import org.ametys.runtime.model.Enumerator;
051import org.ametys.runtime.model.type.ModelItemTypeConstants;
052import org.ametys.runtime.plugin.component.AbstractLogEnabled;
053import org.ametys.runtime.plugin.component.PluginAware;
054import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
055import org.ametys.web.repository.site.Site;
056import org.ametys.web.service.ServiceExtensionPoint;
057import org.ametys.web.service.ServiceParameter;
058import org.ametys.web.site.SiteEnumerator;
059
060/**
061 * Helper component for drawing search service creation/edition dialog box.
062 */
063public class SearchServiceCreationHelper extends AbstractLogEnabled implements Component, Serviceable, Initializable, Disposable, PluginAware, Contextualizable
064{
065    /** Avalon Role */
066    public static final String ROLE = SearchServiceCreationHelper.class.getName();
067
068    /** The service manager */
069    protected ServiceManager _manager;
070    /** The extension point for {@link Returnable}s */
071    protected ReturnableExtensionPoint _returnableEP;
072    
073    /** The extension point for {@link Searchable}s */
074    protected SearchableExtensionPoint _searchableEP;
075    
076    /** The extension point for services */
077    protected ServiceExtensionPoint _serviceEP;
078    /** The extension point for content types */
079    protected ContentTypeExtensionPoint _cTypeEP;
080    /** The extension point for {@link SystemProperty SystemProperties} */
081    protected SystemPropertyExtensionPoint _systemPropertyEP;
082    /** The enumerator manager */
083    protected ThreadSafeComponentManager<Enumerator> _enumeratorManager;
084    /** The {@link Site} enumerator */
085    protected SiteEnumerator _siteEnumerator;
086    
087    /** The plugin name */
088    protected String _pluginName;
089    
090    /** The context */
091    protected Context _context;
092
093    /** The map returnable -&gt; Collection of searchables */
094    protected Map<Returnable, Collection<Searchable>> _searchablesByReturnable;
095    
096    private Collection<AdditionalSearchServiceParameter> _additionalSearchServiceParameters;
097
098    private boolean _initialized;
099    
100    @Override
101    public void service(ServiceManager manager) throws ServiceException
102    {
103        _manager = manager;
104        _returnableEP = (ReturnableExtensionPoint) manager.lookup(ReturnableExtensionPoint.ROLE);
105        _searchableEP = (SearchableExtensionPoint) manager.lookup(SearchableExtensionPoint.ROLE);
106        _serviceEP = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE);
107        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
108        _systemPropertyEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
109    }
110    
111    @Override
112    public void setPluginInfo(String pluginName, String featureName, String id)
113    {
114        _pluginName = pluginName;
115    }
116    
117    @Override
118    public void contextualize(Context context) throws ContextException
119    {
120        _context = context;
121    }
122    
123    @Override
124    public void initialize() throws Exception
125    {
126        buildTree();
127        createEnumerators();
128        _initialized = true;
129    }
130    
131    @Override
132    public void dispose()
133    {
134        _initialized = false;
135    }
136    
137    /**
138     * Is this creation helper ready ?
139     * @return <code>true</code> if this creation helper is ready
140     */
141    public boolean isReady()
142    {
143        return _initialized;
144    }
145    
146    /**
147     * Method called at the start of the application based on
148     * {@link Returnable#relationsWith()} and {@link Searchable#relationsWith()}
149     * in order to build the data structure (in a map returnable -&gt; Collection of searchables)
150     * linking {@link Returnable}s and {@link Searchable}s
151     */
152    protected void buildTree()
153    {
154        _searchablesByReturnable = new HashMap<>();
155        for (String returnableId : _returnableEP.getExtensionsIds())
156        {
157            Returnable returnable = _returnableEP.getExtension(returnableId);
158            Collection<Searchable> searchables = new HashSet<>();
159            _searchablesByReturnable.put(returnable, searchables);
160            searchables.addAll(returnable.relationsWith());
161        }
162        
163        for (String searchableId : _searchableEP.getExtensionsIds())
164        {
165            Searchable searchable = _searchableEP.getExtension(searchableId);
166            for (Returnable returnable : searchable.relationsWith())
167            {
168                _searchablesByReturnable.get(returnable)/*cannot be null as we iterated over all resultType extensions*/.add(searchable);
169            }
170        }
171        
172        _logTree();
173    }
174    
175    private void _logTree()
176    {
177        if (getLogger().isInfoEnabled())
178        {
179            var sb = new StringBuilder("Searchables per returnable:");
180            for (Entry<Returnable, Collection<Searchable>> e : _searchablesByReturnable.entrySet())
181            {
182                sb.append("\n* ")
183                    .append(e.getKey().getId())
184                    .append("=")
185                    .append(e.getValue()/*unfortunately Searchable do not have getId, so keep a Collection<Searchable>*/);
186            }
187            getLogger().info(sb.toString());
188        }
189    }
190    
191    /**
192     * Creates and initializes enumerators ({@link SiteEnumerator}, etc.)
193     * @throws Exception if an error occurs
194     */
195    protected void createEnumerators() throws Exception
196    {
197        _enumeratorManager = new ThreadSafeComponentManager<>();
198        _enumeratorManager.setLogger(getLogger());
199        _enumeratorManager.contextualize(_context);
200        _enumeratorManager.service(_manager);
201        
202        final String siteRole = "site";
203        _enumeratorManager.addComponent(_pluginName, null, siteRole, SiteEnumerator.class, new DefaultConfiguration("enumerator"));
204        _enumeratorManager.initialize();
205        _siteEnumerator = (SiteEnumerator) _enumeratorManager.lookup(siteRole);
206        Objects.nonNull(_siteEnumerator);
207    }
208    
209    /**
210     * Gets the {@link Returnable}s with the given ids
211     * @param returnableIds the ids of the {@link Returnable}s
212     * @return the {@link Returnable}s with the given ids
213     */
214    public List<Returnable> getReturnables(List<String> returnableIds)
215    {
216        return returnableIds
217                .stream()
218                .map(_returnableEP::getExtension)
219                .filter(Objects::nonNull)
220                .collect(Collectors.toList());
221    }
222    
223    /**
224     * Gets the {@link Searchable}s linked with the given {@link Returnable}s
225     * @param returnables the {@link Returnable}s
226     * @return the {@link Searchable}s linked with the given {@link Returnable}
227     */
228    public Collection<Searchable> getSearchables(Collection<Returnable> returnables)
229    {
230        return _getSearchables(returnables.stream())
231                .collect(Collectors.toList());
232    }
233    
234    private Stream<Searchable> _getSearchables(Stream<Returnable> returnables)
235    {
236        return returnables
237                .map(_searchablesByReturnable::get)
238                .flatMap(Collection::stream)
239                .sorted(Comparator.comparingInt(Searchable::criteriaPosition))
240                .distinct();
241    }
242    
243    /**
244     * Gets the {@link SearchServiceCriterionDefinition}s available with the given {@link Searchable}s and additional parameter values
245     * @param searchables The {@link Searchable}s
246     * @param additionalParameterValues The additional parameter values
247     * @return the {@link SearchServiceCriterionDefinition}s available with the given {@link Searchable}s and additional parameter values
248     */
249    public Map<String, SearchServiceCriterionDefinition> getCriterionDefinitions(Collection<Searchable> searchables, AdditionalParameterValueMap additionalParameterValues)
250    {
251        Map<String, SearchServiceCriterionDefinition> criterionDefs = new LinkedHashMap<>();
252        
253        criterionDefs.putAll(SearchServiceCommonImpls.getCommonCriterionDefinitions(this));
254        
255        searchables.stream()
256                .map(searchable -> searchable.getCriteria(additionalParameterValues))
257                .flatMap(Collection::stream)
258                .distinct()
259                .forEach(criterionDef -> criterionDefs.put(criterionDef.getName(), criterionDef));
260        
261        return criterionDefs;
262    }
263    
264    /**
265     * Gets the {@link SearchServiceFacetDefinition}s available with the given {@link Returnable}s and additional parameter values
266     * @param returnables The {@link Returnable}s
267     * @param additionalParameterValues The additional parameter values
268     * @return the {@link SearchServiceFacetDefinition}s available with the given {@link Returnable}s and additional parameter values
269     */
270    public Map<String, SearchServiceFacetDefinition> getFacetDefinitions(Collection<Returnable> returnables, AdditionalParameterValueMap additionalParameterValues)
271    {
272        Map<String, SearchServiceFacetDefinition> facetDefs = new LinkedHashMap<>();
273        
274        facetDefs.putAll(SearchServiceCommonImpls.getCommonFacetDefinitions(this));
275        
276        returnables.stream()
277                .map(returnable -> returnable.getFacets(additionalParameterValues))
278                .flatMap(Collection::stream)
279                .distinct()
280                .forEach(facetDef -> facetDefs.put(facetDef.getName(), facetDef));
281        
282        return facetDefs;
283    }
284    
285    /**
286     * Gets the {@link SearchServiceSortDefinition}s available with the given {@link Returnable}s and additional parameter values
287     * @param returnables The {@link Returnable}s
288     * @param additionalParameterValues The additional parameter values
289     * @return the {@link SearchServiceSortDefinition}s available with the given {@link Returnable}s and additional parameter values
290     */
291    public Map<String, SearchServiceSortDefinition> getSortDefinitions(Collection<Returnable> returnables, AdditionalParameterValueMap additionalParameterValues)
292    {
293        Map<String, SearchServiceSortDefinition> sortDefs = new LinkedHashMap<>();
294        
295        sortDefs.putAll(SearchServiceCommonImpls.getCommonSortDefinitions(this));
296        
297        if (returnables.size() == 1)
298        // otherwise do not return them as it is not relevant to sort on a field not present on all returned documents
299        {
300            Returnable returnable = returnables.iterator().next();
301            returnable.getSorts(additionalParameterValues)
302                    .stream()
303                    .forEach(sortDef -> sortDefs.put(sortDef.getName(), sortDef));
304        }
305        
306        return sortDefs;
307    }
308    
309    /**
310     * Gets the {@link Returnable}s that must be selected by default
311     * @return the {@link Returnable}s that must be selected by default
312     */
313    public Collection<String> selectedReturnables()
314    {
315        return _returnableEP.getExtensionsIds()
316                .stream()
317                .map(_returnableEP::getExtension)
318                .filter(Returnable::selectedByDefault)
319                .map(Returnable::getId)
320                .collect(Collectors.toList());
321    }
322    
323    /**
324     * Gets the configurations of the additional parameters to display in general group
325     * @return the configurations of the additional parameters to display
326     */
327    public Collection<Configuration> getAdditionalParameterConfigurationsForGeneral()
328    {
329        return _getAdditionalParameterConfigurationsOfSearchables();
330    }
331    
332    private Collection<Configuration> _getAdditionalParameterConfigurationsOfSearchables()
333    {
334        Stream<Returnable> allReturnables = _returnableEP.getExtensionsIds()
335                .stream()
336                .map(_returnableEP::getExtension);
337
338        return _getSearchables(allReturnables)
339                // do not do _searchableEP.getExtensionIds() because we do not want the searchables not linked to any returnable
340                .map(LambdaUtils.wrap(searchable -> searchable.additionalServiceParameters()))
341                .flatMap(Collection::stream)
342                .collect(Collectors.toList());
343    }
344    
345    /**
346     * Gets the configurations of the additional parameters to display in display group
347     * @return the configurations of the additional parameters to display
348     */
349    public Collection<Configuration> getAdditionalParameterConfigurationsForDisplay()
350    {
351        return _getAdditionalParameterConfigurationsOfReturnables();
352    }
353    
354    private Collection<Configuration> _getAdditionalParameterConfigurationsOfReturnables()
355    {
356        return _returnableEP.getExtensionsIds()
357                .stream()
358                .map(_returnableEP::getExtension)
359                .map(LambdaUtils.wrap(returnable -> returnable.additionalServiceParameters()))
360                .flatMap(Collection::stream)
361                .collect(Collectors.toList());
362    }
363    
364    /**
365     * Sets the additional parameters to display
366     * @param additionalSearchServiceParameters the additional parameters to display
367     */
368    public void setAdditionalParameters(Collection<AdditionalSearchServiceParameter> additionalSearchServiceParameters)
369    {
370        Objects.requireNonNull(additionalSearchServiceParameters);
371        _additionalSearchServiceParameters = additionalSearchServiceParameters;
372    }
373    
374    /**
375     * Gets the additional parameters to display
376     * @return the additional parameters to display
377     */
378    public Collection<AdditionalSearchServiceParameter> getAdditionalParameters()
379    {
380        return Optional.ofNullable(_additionalSearchServiceParameters)
381                .orElseThrow(() -> new IllegalStateException("Too soon to call #getAdditionalParameters as they are not initialized yet. #setAdditionalParameters was not called."));
382    }
383    
384    /**
385     * Gets the values of the additional parameters
386     * @param additionalParameters the additional parameters
387     * @param serviceParameters the storage of the service parameters
388     * @return the values of the additional parameters
389     */
390    public AdditionalParameterValueMap getAdditionalParameterValues(Collection<AdditionalSearchServiceParameter> additionalParameters, ModelAwareDataHolder serviceParameters)
391    {
392        Map<String, Object> additionalParameterValues = new HashMap<>();
393        for (AdditionalSearchServiceParameter additionalParameter : additionalParameters)
394        {
395            ServiceParameter<Object> parameter = additionalParameter.getParameter();
396            String paramId = parameter.getName();
397            additionalParameterValues.put(paramId, _getAdditionalParameterValue(serviceParameters, parameter));
398        }
399        
400        Set<String> notDisplayableParameterIds = _notDisplayableParameterIds(additionalParameters);
401        
402        return new AdditionalParameterValueMap(additionalParameterValues, notDisplayableParameterIds);
403    }
404    
405    private <T> Object _getAdditionalParameterValue(ModelAwareDataHolder serviceParameters, ServiceParameter<T> parameter)
406    {
407        String parameterName = parameter.getName();
408        if (serviceParameters.hasValue(parameterName))
409        {
410            T value = serviceParameters.getValue(parameterName);
411            return serviceParameters.isMultiple(parameterName) ? Arrays.asList((Object[]) value) : value;
412        }
413        return null;
414    }
415    
416    /**
417     * Gets the values of the additional parameters from a client form
418     * @param additionalParameters the additional parameters
419     * @param clientSideValues the values of the client form
420     * @return the values of the additional parameters from the client form
421     */
422    public AdditionalParameterValueMap getAdditionalParameterValues(Collection<AdditionalSearchServiceParameter> additionalParameters, Map<String, Object> clientSideValues)
423    {
424        Set<String> paramIds = additionalParameters.stream()
425                .map(AdditionalSearchServiceParameter::getParameter)
426                .map(ServiceParameter::getName)
427                .collect(Collectors.toSet());
428        
429        Map<String, Object> additionalParameterValues = new HashMap<>();
430        clientSideValues.keySet()
431            .stream()
432            .filter(paramIds::contains)
433            .forEach(id -> additionalParameterValues.put(id, clientSideValues.get(id))); // bug with Collectors.toMap see https://bugs.openjdk.java.net/browse/JDK-8148463
434        
435        Set<String> notDisplayableParameterIds = _notDisplayableParameterIds(additionalParameters);
436        
437        return new AdditionalParameterValueMap(additionalParameterValues, notDisplayableParameterIds);
438    }
439    
440    private Set<String> _notDisplayableParameterIds(Collection<AdditionalSearchServiceParameter> additionalParameters)
441    {
442        return additionalParameters
443                .parallelStream()
444                .map(AdditionalSearchServiceParameter::getParameter)
445                .filter(p-> ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(p.getType().getId()))
446                .map(ServiceParameter::getName)
447                .collect(Collectors.toSet());
448    }
449}