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