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.plugins.odfweb.service.search;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.function.Function;
025import java.util.stream.Collectors;
026
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030import org.apache.cocoon.environment.Request;
031
032import org.ametys.cms.repository.Content;
033import org.ametys.cms.search.SearchResult;
034import org.ametys.cms.search.SearchResultsIterable;
035import org.ametys.cms.search.SearchResultsIterator;
036import org.ametys.cms.search.advanced.AbstractTreeNode;
037import org.ametys.cms.search.advanced.AdvancedQueryBuilder;
038import org.ametys.cms.search.content.ContentSearcherFactory;
039import org.ametys.cms.search.query.Query;
040import org.ametys.odf.program.Program;
041import org.ametys.odf.program.SubProgram;
042import org.ametys.odf.program.SubProgramFactory;
043import org.ametys.plugins.odfweb.service.search.ProgramReturnable.DisplaySubprogramMode;
044import org.ametys.web.frontoffice.search.instance.SearchServiceInstance;
045import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterion;
046import org.ametys.web.frontoffice.search.metamodel.AdditionalParameterValueMap;
047import org.ametys.web.frontoffice.search.metamodel.FacetDefinition;
048import org.ametys.web.frontoffice.search.metamodel.Returnable;
049import org.ametys.web.frontoffice.search.metamodel.SearchCriterionDefinition;
050import org.ametys.web.frontoffice.search.metamodel.Searchable;
051import org.ametys.web.frontoffice.search.requesttime.SearchComponent;
052import org.ametys.web.frontoffice.search.requesttime.SearchComponentArguments;
053import org.ametys.web.frontoffice.search.requesttime.impl.SearchComponentHelper;
054
055/**
056 * {@link SearchComponent} for getting subprograms matching search
057 */
058public class MatchingSubprogramSearchComponent implements SearchComponent, Serviceable
059{
060    private static final String __MATCHING_SUBPROGRAM_IDS_ATTR_NAME = MatchingSubprogramSearchComponent.class.getName() + "$matchingSubProgramIds";
061    
062    /** The content searcher factory */
063    protected ContentSearcherFactory _contentSearcherfactory;
064
065    /** The builder of advanced queries */
066    protected AdvancedQueryBuilder _advancedQueryBuilder;
067    
068    /** The search component helper */
069    protected SearchComponentHelper _searchComponentHelper;
070    
071    /** The ODF search helper */
072    protected ODFSearchHelper _odfSearchHelper;
073    
074    @Override
075    public void service(ServiceManager manager) throws ServiceException
076    {
077        _contentSearcherfactory = (ContentSearcherFactory) manager.lookup(ContentSearcherFactory.ROLE);
078        _advancedQueryBuilder = (AdvancedQueryBuilder) manager.lookup(AdvancedQueryBuilder.ROLE);
079        _searchComponentHelper = (SearchComponentHelper) manager.lookup(SearchComponentHelper.ROLE);
080        _odfSearchHelper = (ODFSearchHelper) manager.lookup(ODFSearchHelper.ROLE);
081    }
082
083    @Override
084    public int priority()
085    {
086        return SEARCH_PRIORITY - 9000;
087    }
088
089    @Override
090    public boolean supports(SearchComponentArguments args)
091    {
092        SearchServiceInstance serviceInstance = args.serviceInstance();
093        return  args.launchSearch() 
094                && args.serviceInstance().getCriterionTree().isPresent()
095                && _containsProgramReturnable(serviceInstance)
096                && _needSubprogramDiscrimination(serviceInstance);
097    }
098    
099    private boolean _containsProgramReturnable(SearchServiceInstance serviceInstance)
100    {
101        return serviceInstance.getReturnables()
102                .stream()
103                .map(Returnable::getId)
104                .anyMatch(ProgramReturnable.ROLE::equals);
105    }
106    
107    private boolean _needSubprogramDiscrimination(SearchServiceInstance serviceInstance)
108    {
109        AdditionalParameterValueMap additionalParameterValues = serviceInstance.getAdditionalParameterValues();
110        DisplaySubprogramMode mode = ProgramReturnable.getDisplaySubprogramMode(additionalParameterValues);
111        return mode == DisplaySubprogramMode.ALL_WITH_HIGHLIGHT || mode == DisplaySubprogramMode.MATCHING_SEARCH_ONLY;
112    }
113    
114    @Override
115    public void execute(SearchComponentArguments args) throws Exception
116    {
117        SearchServiceInstance serviceInstance = args.serviceInstance();
118
119        List<String> matchingSubProgramIds = retrieveSubProgramsMatchingSearch(
120                                                    serviceInstance, 
121                                                    args.userInputs().criteria(),
122                                                    args.serviceInstance().getFacets(),
123                                                    args.userInputs().facets(),
124                                                    args.currentLang());
125        _storeInReqMatchingSubProgramIds(args.request(), matchingSubProgramIds);
126    }
127    
128    /**
129     * Retrieves, thanks to a (independent but close to the main one) Solr search, the {@link SubProgram}s that match the same query than the {@link Program}s, for further special display.
130     * @param serviceInstance The service instance
131     * @param userCriteria The user criteria
132     * @param serviceFacets the service facets
133     * @param userFacets the user facets
134     * @param lang The current lang
135     * @return the {@link SubProgram}s that match the same query than the {@link Program}s
136     * @throws Exception If an exception occured during the Solr search
137     */
138    protected List<String> retrieveSubProgramsMatchingSearch(SearchServiceInstance serviceInstance, Map<String, Object> userCriteria, Collection<FacetDefinition> serviceFacets, Map<String, List<String>> userFacets, String lang) throws Exception
139    {
140        AbstractTreeNode<FOSearchCriterion> criterionTree = serviceInstance.getCriterionTree().get();
141        
142        Collection<Returnable> returnables = serviceInstance.getReturnables();
143        Collection<Searchable> searchables = serviceInstance.getSearchables();
144        AdditionalParameterValueMap additionalParameters = serviceInstance.getAdditionalParameterValues();
145        Map<String, Object> contextualParameters = new HashMap<>();
146        Query criterionTreeQuery = buildQuery(criterionTree, userCriteria, returnables, searchables, additionalParameters, lang, contextualParameters);
147        
148        List<String> facets = serviceFacets.stream()
149            .filter(this::_mustKeepFacet)
150            .map(FacetDefinition::getId)
151            .map(id -> _odfSearchHelper.getPathFromSearchFieldId(id))
152            .collect(Collectors.toList());
153
154        Map<String, List<String>> computedUserFacets = userFacets.entrySet()
155            .stream()
156            .collect(Collectors.toMap(e -> _odfSearchHelper.getPathFromSearchFieldId(e.getKey()), Map.Entry::getValue));
157        
158        SearchResultsIterable<SearchResult<Content>> results = _contentSearcherfactory.create(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE)
159                .withLimits(0, Integer.MAX_VALUE)
160                .setCheckRights(true)
161                .withFacets(facets)
162                // TODO possible optimization: here we don't care about the order of returned subprograms, so avoid computation of score (sort=_docid_ asc)
163                .searchWithFacets(criterionTreeQuery, computedUserFacets)
164                .getResults();
165        
166        List<String> contentIds = new ArrayList<>();
167        SearchResultsIterator<SearchResult<Content>> resultsIterator = results.iterator();
168        while (resultsIterator.hasNext())
169        {
170            SearchResult<Content> content = resultsIterator.next();
171            String id = content.getObject().getId();
172            contentIds.add(id);
173        }
174        
175        return contentIds;
176    }
177    
178    /**
179     * Builds the query of the criterion tree
180     * @param criterionTree The criterion tree of the service instance
181     * @param userCriteria The user input criteria
182     * @param returnables The returnables of the service instance
183     * @param searchables The searchables of the service instance
184     * @param additionalParameters The values of additional parameters of the service instance
185     * @param currentLang The current lang
186     * @param contextualParameters the search contextual parameters. 
187     * @return The query of the criterion tree
188     */
189    protected Query buildQuery(
190            AbstractTreeNode<FOSearchCriterion> criterionTree, 
191            Map<String, Object> userCriteria, 
192            Collection<Returnable> returnables, 
193            Collection<Searchable> searchables, 
194            AdditionalParameterValueMap additionalParameters, 
195            String currentLang, 
196            Map<String, Object> contextualParameters)
197    {
198        // Sanitize additional params => we do not want ProgramSearchable to generate a Join on subprograms as we are already searching subprograms here !
199        AdditionalParameterValueMap modifiedAdditionalParameters = new NoSubprogramSearchAdditionalParameterValueMap(additionalParameters);
200        
201        Function<FOSearchCriterion, Query> queryMapper = crit -> singleCriterionToQuery(crit, userCriteria, returnables, searchables, modifiedAdditionalParameters, currentLang, contextualParameters);
202        return _advancedQueryBuilder.build(criterionTree, queryMapper);
203    }
204    
205    /**
206     * Builds the query of the single criterion
207     * @param searchCriterion The criterion
208     * @param userCriteria The user input criteria
209     * @param returnables The returnables of the service instance
210     * @param searchables The searchables of the service instance
211     * @param additionalParameters The values of additional parameters of the service instance
212     * @param currentLang The current lang
213     * @param contextualParameters the search contextual parameters. 
214     * @return The query of the single criterion
215     */
216    protected Query singleCriterionToQuery(
217            FOSearchCriterion searchCriterion, 
218            Map<String, Object> userCriteria, 
219            Collection<Returnable> returnables, 
220            Collection<Searchable> searchables, 
221            AdditionalParameterValueMap additionalParameters, 
222            String currentLang,
223            Map<String, Object> contextualParameters)
224    {
225        if (_mustKeepCriterion(searchCriterion))
226        {
227            // Delegate to the function defined in CriterionTreeSearchComponent
228            return _searchComponentHelper.singleCriterionToQuery(searchCriterion, userCriteria, returnables, searchables, additionalParameters, currentLang, contextualParameters);
229        }
230        else
231        {
232            return SearchComponentHelper.EMPTY_QUERY;
233        }
234    }
235    
236    private boolean _mustKeepCriterion(FOSearchCriterion criterion)
237    {
238        SearchCriterionDefinition criterionDefinition = criterion.getCriterionDefinition();
239        
240        // We want to keep only wording criteria or common criteria between programs and subprograms: the Query is meant to return subprograms and not programs
241        return _odfSearchHelper.isCriterionOnBothProgramAndSubProgram(criterionDefinition);
242    }
243    
244    private boolean _mustKeepFacet(FacetDefinition facet)
245    {
246        // We want to keep common facet between programs and subprograms: the Query is meant to return subprograms and not programs
247        return _odfSearchHelper.isFacetOnBothProgramAndSubProgram(facet);
248    }
249    
250    private static void _storeInReqMatchingSubProgramIds(Request request, List<String> subPrograms)
251    {
252        request.setAttribute(__MATCHING_SUBPROGRAM_IDS_ATTR_NAME, subPrograms);
253    }
254    
255    @SuppressWarnings("unchecked")
256    static List<String> _getMatchingSubProgramIds(Request request)
257    {
258        List<String> ids = (List<String>) request.getAttribute(__MATCHING_SUBPROGRAM_IDS_ATTR_NAME);
259        return ids == null ? Collections.emptyList() : ids;
260    }
261    
262    // AdditionalParameterValueMap, which will always answer false to ProgramSearchable.PARAMETER_SEARCH_ON_SUBPROGRAMS
263    private static class NoSubprogramSearchAdditionalParameterValueMap extends AdditionalParameterValueMap
264    {
265        private AdditionalParameterValueMap _ref;
266        NoSubprogramSearchAdditionalParameterValueMap(AdditionalParameterValueMap ref)
267        {
268            super(Collections.emptyMap(), Collections.emptySet());
269            _ref = ref;
270        }
271        
272        @SuppressWarnings("unchecked")
273        @Override
274        public <T> T getValue(String parameterId) throws ClassCastException
275        {
276            return ProgramSearchable.PARAMETER_SEARCH_ON_SUBPROGRAMS.equals(parameterId)
277                    ? (T) Boolean.FALSE
278                    : _ref.getValue(parameterId);
279        }
280    }
281}