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