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