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.Collection;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.stream.Collectors;
024
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.cocoon.environment.Request;
028
029import org.ametys.cms.repository.Content;
030import org.ametys.cms.search.advanced.AbstractTreeNode;
031import org.ametys.cms.search.content.ContentSearcherFactory;
032import org.ametys.cms.search.query.Query;
033import org.ametys.odf.program.Program;
034import org.ametys.odf.program.SubProgram;
035import org.ametys.odf.program.SubProgramFactory;
036import org.ametys.plugins.odfweb.service.search.ProgramReturnable.DisplaySubprogramMode;
037import org.ametys.web.frontoffice.search.instance.SearchServiceInstance;
038import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterion;
039import org.ametys.web.frontoffice.search.metamodel.AdditionalParameterValueMap;
040import org.ametys.web.frontoffice.search.metamodel.Returnable;
041import org.ametys.web.frontoffice.search.metamodel.Searchable;
042import org.ametys.web.frontoffice.search.metamodel.impl.WordingSearchCriterionDefinition;
043import org.ametys.web.frontoffice.search.requesttime.SearchComponent;
044import org.ametys.web.frontoffice.search.requesttime.SearchComponentArguments;
045import org.ametys.web.frontoffice.search.requesttime.impl.CriterionTreeSearchComponent;
046
047/**
048 * {@link SearchComponent} for getting subprograms matching search
049 * <br>
050 * <br>Note for developpers: This SearchComponent extends CriterionTreeSearchComponent just to have access to some of its protected method (#buildQuery() ...).
051 * <br>But it is an additional SearchComponent aiming at executing another independent Solr query, and it is not meant at all to override and replace
052 * CriterionTreeSearchComponent.
053 */
054public class MatchingSubprogramSearchComponent extends CriterionTreeSearchComponent
055{
056    private static final String __MATCHING_SUBPROGRAM_IDS_ATTR_NAME = MatchingSubprogramSearchComponent.class.getName() + "$matchingSubProgramIds";
057    
058    /** The content searcher factory */
059    protected ContentSearcherFactory _contentSearcherfactory;
060
061    @Override
062    public void service(ServiceManager manager) throws ServiceException
063    {
064        super.service(manager);
065        _contentSearcherfactory = (ContentSearcherFactory) manager.lookup(ContentSearcherFactory.ROLE);
066    }
067
068    @Override
069    public int priority()
070    {
071        return SEARCH_PRIORITY - 9000;
072    }
073
074    @Override
075    public boolean supports(SearchComponentArguments args)
076    {
077        SearchServiceInstance serviceInstance = args.serviceInstance();
078        return super.supports(args) 
079                && _containsProgramReturnable(serviceInstance)
080                && _needSubprogramDiscrimination(serviceInstance);
081    }
082    
083    private boolean _containsProgramReturnable(SearchServiceInstance serviceInstance)
084    {
085        return serviceInstance.getReturnables()
086                .stream()
087                .map(Returnable::getId)
088                .anyMatch(ProgramReturnable.ROLE::equals);
089    }
090    
091    private boolean _needSubprogramDiscrimination(SearchServiceInstance serviceInstance)
092    {
093        AdditionalParameterValueMap additionalParameterValues = serviceInstance.getAdditionalParameterValues();
094        DisplaySubprogramMode mode = ProgramReturnable.getDisplaySubprogramMode(additionalParameterValues);
095        return mode == DisplaySubprogramMode.ALL_WITH_HIGHLIGHT || mode == DisplaySubprogramMode.MATCHING_SEARCH_ONLY;
096    }
097    
098    @Override
099    public void execute(SearchComponentArguments args) throws Exception
100    {
101        SearchServiceInstance serviceInstance = args.serviceInstance();
102        List<String> matchingSubProgramIds = retrieveSubProgramsMatchingSearch(serviceInstance, args.userInputs().criteria(), args.currentLang());
103        _storeInReqMatchingSubProgramIds(args.request(), matchingSubProgramIds);
104    }
105    
106    /**
107     * 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.
108     * @param serviceInstance The service instance
109     * @param userCriteria The user criteria
110     * @param lang The current lang
111     * @return the {@link SubProgram}s that match the same query than the {@link Program}s
112     * @throws Exception If an exception occured during the Solr search
113     */
114    protected List<String> retrieveSubProgramsMatchingSearch(SearchServiceInstance serviceInstance, Map<String, Object> userCriteria, String lang) throws Exception
115    {
116        AbstractTreeNode<FOSearchCriterion> criterionTree = serviceInstance.getCriterionTree().get();
117        
118        Collection<Returnable> returnables = serviceInstance.getReturnables();
119        Collection<Searchable> searchables = serviceInstance.getSearchables();
120        AdditionalParameterValueMap additionalParameters = serviceInstance.getAdditionalParameterValues();
121        Map<String, Object> contextualParameters = new HashMap<>();
122        Query criterionTreeQuery = buildQuery(criterionTree, userCriteria, returnables, searchables, additionalParameters, lang, contextualParameters);
123        
124        return _contentSearcherfactory.create(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE)
125                .withLimits(0, Integer.MAX_VALUE)
126                .setCheckRights(true)
127                // TODO possible optimization: here we don't care about the order of returned subprograms, so avoid computation of score (sort=_docid_ asc)
128                .search(criterionTreeQuery)
129                .stream()
130                .map(Content::getId)
131                .collect(Collectors.toList());
132    }
133    
134    @Override
135    protected Query buildQuery(
136            AbstractTreeNode<FOSearchCriterion> criterionTree, 
137            Map<String, Object> userCriteria, 
138            Collection<Returnable> returnables, 
139            Collection<Searchable> searchables, 
140            AdditionalParameterValueMap additionalParameters,
141            String currentLang, 
142            Map<String, Object> contextualParameters)
143    {
144        // Sanitize additional params => we do not want ProgramSearchable to generate a Join on subprograms as we are already searching subprograms here !
145        AdditionalParameterValueMap modifiedAdditionalParameters = new NoSubprogramSearchAdditionalParameterValueMap(additionalParameters);
146        
147        return super.buildQuery(criterionTree, userCriteria, returnables, searchables, modifiedAdditionalParameters, currentLang, contextualParameters);
148    }
149    
150    @Override
151    protected Query singleCriterionToQuery(
152            FOSearchCriterion searchCriterion, 
153            Map<String, Object> userCriteria, 
154            Collection<Returnable> returnables, 
155            Collection<Searchable> searchables, 
156            AdditionalParameterValueMap additionalParameters, 
157            String currentLang,
158            Map<String, Object> contextualParameters)
159    {
160        if (_mustKeepCriterion(searchCriterion))
161        {
162            // Delegate to the function defined in CriterionTreeSearchComponent
163            return super.singleCriterionToQuery(searchCriterion, userCriteria, returnables, searchables, additionalParameters, currentLang, contextualParameters);
164        }
165        else
166        {
167            return __EMPTY_QUERY;
168        }
169    }
170    
171    private boolean _mustKeepCriterion(FOSearchCriterion criterion)
172    {
173        // We want to keep only wording criteria, as the Query is meant to return subprograms and not programs
174        return criterion.getCriterionDefinition() instanceof WordingSearchCriterionDefinition;
175    }
176    
177    private static void _storeInReqMatchingSubProgramIds(Request request, List<String> subPrograms)
178    {
179        request.setAttribute(__MATCHING_SUBPROGRAM_IDS_ATTR_NAME, subPrograms);
180    }
181    
182    @SuppressWarnings("unchecked")
183    static List<String> _getMatchingSubProgramIds(Request request)
184    {
185        List<String> ids = (List<String>) request.getAttribute(__MATCHING_SUBPROGRAM_IDS_ATTR_NAME);
186        return ids == null ? Collections.emptyList() : ids;
187    }
188    
189    static void _removeMatchingSubProgramIdsRequestAttribute(Request request)
190    {
191        request.removeAttribute(__MATCHING_SUBPROGRAM_IDS_ATTR_NAME);
192    }
193    
194    // AdditionalParameterValueMap, which will always answer false to ProgramSearchable.PARAMETER_SEARCH_ON_SUBPROGRAMS
195    private static class NoSubprogramSearchAdditionalParameterValueMap extends AdditionalParameterValueMap
196    {
197        private AdditionalParameterValueMap _ref;
198        NoSubprogramSearchAdditionalParameterValueMap(AdditionalParameterValueMap ref)
199        {
200            super(Collections.emptyMap(), Collections.emptySet());
201            _ref = ref;
202        }
203        
204        @SuppressWarnings("unchecked")
205        @Override
206        public <T> T getValue(String parameterId) throws ClassCastException
207        {
208            return ProgramSearchable.PARAMETER_SEARCH_ON_SUBPROGRAMS.equals(parameterId)
209                    ? (T) Boolean.FALSE
210                    : _ref.getValue(parameterId);
211        }
212    }
213}