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