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()); 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}