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