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}