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