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