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