001/* 002 * Copyright 2018 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.web.frontoffice.search.requesttime.impl; 017 018import java.util.Collection; 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Optional; 024import java.util.stream.Collectors; 025 026import org.apache.avalon.framework.parameters.Parameters; 027import org.apache.cocoon.xml.AttributesImpl; 028import org.apache.cocoon.xml.XMLUtils; 029import org.apache.commons.collections4.ListUtils; 030import org.apache.commons.lang3.tuple.Pair; 031import org.xml.sax.Attributes; 032import org.xml.sax.ContentHandler; 033import org.xml.sax.SAXException; 034 035import org.ametys.cms.search.SearchResults; 036import org.ametys.cms.search.Sort; 037import org.ametys.cms.search.advanced.AbstractTreeNode; 038import org.ametys.cms.search.advanced.TreeLeaf; 039import org.ametys.core.util.LambdaUtils; 040import org.ametys.plugins.repository.AmetysObject; 041import org.ametys.runtime.i18n.I18nizableText; 042import org.ametys.runtime.parameter.Enumerator; 043import org.ametys.web.frontoffice.search.instance.SearchServiceInstance; 044import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterion; 045import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterionMode; 046import org.ametys.web.frontoffice.search.instance.model.ResultDisplayType; 047import org.ametys.web.frontoffice.search.metamodel.FacetDefinition; 048import org.ametys.web.frontoffice.search.metamodel.FrontEnumerableSearchCriterionDefinition; 049import org.ametys.web.frontoffice.search.metamodel.SearchCriterionDefinition; 050import org.ametys.web.frontoffice.search.metamodel.SortDefinition; 051import org.ametys.web.frontoffice.search.requesttime.SearchComponent; 052import org.ametys.web.frontoffice.search.requesttime.SearchComponentArguments; 053import org.ametys.web.frontoffice.search.requesttime.input.SearchUserInputs; 054import org.ametys.web.frontoffice.search.requesttime.input.impl.FormSearchUserInputs; 055import org.ametys.web.repository.site.Site; 056 057import com.google.common.collect.ImmutableMap; 058 059/** 060 * {@link SearchComponent} for saxing form 061 */ 062public class SaxFormSearchComponent implements SearchComponent 063{ 064 private static final String __IS_MINIMAL_FORM_SAX_PARAMETER_NAME = "minimalFormSax"; 065 066 @Override 067 public int priority() 068 { 069 return SEARCH_PRIORITY + 2000; 070 } 071 072 @Override 073 public boolean supports(SearchComponentArguments args) 074 { 075 if (args.generatorParameters().getParameterAsBoolean(DISABLE_DEFAULT_SAX_PARAMETER_NAME, false)) 076 { 077 return false; 078 } 079 080 ResultDisplayType resultDisplayType = args.serviceInstance().getResultDisplay().getType(); 081 return resultDisplayType == ResultDisplayType.ABOVE_CRITERIA // results are displayed above criteria => we always need criteria 082 || resultDisplayType == ResultDisplayType.IN_PLACE_OF_CRITERIA // if there is a result count of 0, the XSL may choose to display criteria anyway 083 || !args.launchSearch(); // form was not submitted yet => need criteria 084 } 085 086 @Override 087 public void execute(SearchComponentArguments args) throws Exception 088 { 089 ContentHandler contentHandler = args.contentHandler(); 090 SearchServiceInstance serviceInstance = args.serviceInstance(); 091 List<FOSearchCriterion> nonStaticCriteria = serviceInstance.getCriterionTree() 092 .map(AbstractTreeNode::getFlatLeaves) 093 .orElseGet(Collections::emptyList) 094 .stream() 095 .map(TreeLeaf::getValue) 096 .filter(c -> c.getMode() != FOSearchCriterionMode.STATIC) 097 .collect(Collectors.toList()); 098 Parameters parameters = args.generatorParameters(); 099 100 XMLUtils.startElement(contentHandler, "form"); 101 102 XMLUtils.startElement(contentHandler, "fields"); 103 saxFormFields(contentHandler, parameters, nonStaticCriteria, args.currentLang(), args.currentSite()); 104 XMLUtils.endElement(contentHandler, "fields"); 105 106 SearchUserInputs userInputs = args.userInputs(); 107 108 Map<String, Object> userCriteria = userInputs.criteria(); 109 XMLUtils.startElement(contentHandler, "values"); 110 saxFormValues(contentHandler, parameters, nonStaticCriteria, userCriteria); 111 XMLUtils.endElement(contentHandler, "values"); 112 113 XMLUtils.startElement(contentHandler, "facets"); 114 saxFacets(contentHandler, parameters, serviceInstance.getFacets(), userInputs.facets(), args.results(), args.currentLang()); 115 XMLUtils.endElement(contentHandler, "facets"); 116 117 XMLUtils.startElement(contentHandler, "sorts"); 118 saxSorts(contentHandler, parameters, serviceInstance.getInitialSorts(), serviceInstance.getProposedSorts(), userInputs.sorts()); 119 XMLUtils.endElement(contentHandler, "sorts"); 120 121 XMLUtils.endElement(contentHandler, "form"); 122 } 123 124 /** 125 * Returns <code>true</code> if the Generator is parameterized with a minimal form sax 126 * @param parameters the parameters 127 * @return <code>true</code> if the Generator is parameterized with a minimal form sax 128 */ 129 protected boolean isMinimalFormSax(Parameters parameters) 130 { 131 return parameters.getParameterAsBoolean(__IS_MINIMAL_FORM_SAX_PARAMETER_NAME, false); 132 } 133 134 /** 135 * SAX the form search criteria 136 * @param contentHandler the content handler 137 * @param parameters the parameters 138 * @param nonStaticCriteria the non-{@link FOSearchCriterionMode#STATIC static} criteria 139 * @param language The current language 140 * @param currentSite The current site 141 * @throws Exception if an exception occurs 142 */ 143 protected void saxFormFields(ContentHandler contentHandler, Parameters parameters, List<FOSearchCriterion> nonStaticCriteria, String language, Site currentSite) throws Exception 144 { 145 for (FOSearchCriterion criterion : nonStaticCriteria) 146 { 147 AttributesImpl attrs = new AttributesImpl(); 148 attrs.addCDATAAttribute("name", FormSearchUserInputs.CRITERION_PREFIX + criterion.getId()); 149 SearchCriterionDefinition criterionDef = criterion.getCriterionDefinition(); 150 attrs.addCDATAAttribute("definition", criterionDef.getId()); 151 attrs.addCDATAAttribute("operator", criterion.getOperator()); 152 XMLUtils.startElement(contentHandler, "criterion", attrs); 153 154 criterionDef.getLabel().toSAX(contentHandler, "label"); 155 if (!isMinimalFormSax(parameters)) 156 { 157 saxEnumeratorEntries(contentHandler, criterion, language, currentSite); 158 } 159 160 XMLUtils.createElement(contentHandler, "type", criterionDef.getType().name()); 161 XMLUtils.endElement(contentHandler, "criterion"); 162 } 163 } 164 165 /** 166 * SAX {@link Enumerator} entries 167 * @param contentHandler the content handler 168 * @param criterion the (non-{@link FOSearchCriterionMode#STATIC static}) criterion 169 * @param language The current language 170 * @param currentSite The current site 171 * @throws Exception if an exception occurs 172 */ 173 protected void saxEnumeratorEntries(ContentHandler contentHandler, FOSearchCriterion criterion, String language, Site currentSite) throws Exception 174 { 175 Map<Object, I18nizableText> enumeratorEntries = null; 176 SearchCriterionDefinition criterionDef = criterion.getCriterionDefinition(); 177 if (criterion.getMode() == FOSearchCriterionMode.RESTRICTED_USER_INPUT) 178 { 179 enumeratorEntries = criterion.getRestrictedValues().get().values(); 180 } 181 else if (criterionDef instanceof FrontEnumerableSearchCriterionDefinition) 182 { 183 enumeratorEntries = ((FrontEnumerableSearchCriterionDefinition) criterionDef).getEntries(language); 184 } 185 else if (criterionDef.isEnumerated()) 186 { 187 enumeratorEntries = criterionDef 188 .getEnumeratedValues(_contextualParameters(currentSite)) 189 .get() 190 .getAllValues(); 191 } 192 193 if (enumeratorEntries == null) 194 { 195 return; 196 } 197 198 XMLUtils.startElement(contentHandler, "enumeration"); 199 enumeratorEntries 200 .entrySet() 201 .stream() 202 .sequential() 203 .forEach(LambdaUtils.wrapConsumer(e -> saxEnumeratorEntry(contentHandler, e))); 204 XMLUtils.endElement(contentHandler, "enumeration"); 205 } 206 207 private Map<String, Object> _contextualParameters(Site currentSite) 208 { 209 return ImmutableMap.of("siteName", currentSite.getName()); 210 } 211 212 /** 213 * SAX {@link Enumerator} entry 214 * @param contentHandler the content handler 215 * @param enumeratorEntry the enumerator entry 216 * @throws Exception if an exception occurs 217 */ 218 protected void saxEnumeratorEntry(ContentHandler contentHandler, Map.Entry<Object, I18nizableText> enumeratorEntry) throws Exception 219 { 220 AttributesImpl attrs = new AttributesImpl(); 221 attrs.addCDATAAttribute("value", String.valueOf(enumeratorEntry.getKey())); 222 XMLUtils.startElement(contentHandler, "item", attrs); 223 enumeratorEntry.getValue().toSAX(contentHandler, "label"); 224 XMLUtils.endElement(contentHandler, "item"); 225 } 226 227 /** 228 * SAX the form search criteria values 229 * @param contentHandler the content handler 230 * @param parameters the parameters 231 * @param nonStaticCriteria the non-{@link FOSearchCriterionMode#STATIC static} criteria 232 * @param userCriteria The user input criteria 233 * @throws SAXException if an error occurs while SAXing 234 */ 235 protected void saxFormValues(ContentHandler contentHandler, Parameters parameters, List<FOSearchCriterion> nonStaticCriteria, Map<String, Object> userCriteria) throws SAXException 236 { 237 for (FOSearchCriterion criterion : nonStaticCriteria) 238 { 239 String id = criterion.getId(); 240 Object userCriterion = userCriteria.get(id); 241 if (userCriterion != null) 242 { 243 AttributesImpl attrs = new AttributesImpl(); 244 String name = FormSearchUserInputs.CRITERION_PREFIX + id; 245 attrs.addCDATAAttribute("name", name); 246 saxFormValue(contentHandler, attrs, userCriterion); 247 } 248 } 249 } 250 251 /** 252 * SAX the values of a form search criterion 253 * @param contentHandler the content handler 254 * @param attrs The XML attributes 255 * @param userCriterion The user input criterion value (is a List if multiple) 256 * @throws SAXException if an error occurs while SAXing 257 */ 258 protected void saxFormValue(ContentHandler contentHandler, Attributes attrs, Object userCriterion) throws SAXException 259 { 260 if (userCriterion instanceof List<?>) 261 { 262 List<?> multipleUserCriterion = (List<?>) userCriterion; 263 for (Object singleUserCriterion : multipleUserCriterion) 264 { 265 saxFormSingleValue(contentHandler, attrs, singleUserCriterion); 266 } 267 } 268 else 269 { 270 saxFormSingleValue(contentHandler, attrs, userCriterion); 271 } 272 } 273 274 /** 275 * SAX a single value of a form search criterion 276 * @param contentHandler the content handler 277 * @param attrs The XML attributes 278 * @param singleUserCriterion The user input criterion single value 279 * @throws SAXException if an error occurs while SAXing 280 */ 281 protected void saxFormSingleValue(ContentHandler contentHandler, Attributes attrs, Object singleUserCriterion) throws SAXException 282 { 283 XMLUtils.createElement(contentHandler, "criterion", attrs, String.valueOf(singleUserCriterion)); 284 } 285 286 /** 287 * SAX the facets 288 * @param contentHandler the content handler 289 * @param parameters the parameters 290 * @param facets The facets 291 * @param userFacets The user input facets 292 * @param searchResults The search results 293 * @param currentLang The current lang 294 * @throws SAXException if an error occurs while SAXing 295 */ 296 protected void saxFacets(ContentHandler contentHandler, Parameters parameters, Collection<FacetDefinition> facets, Map<String, List<String>> userFacets, Optional<SearchResults<AmetysObject>> searchResults, String currentLang) throws SAXException 297 { 298 boolean formSubmitted = searchResults.isPresent(); 299 Map<String, Map<String, Integer>> facetResults = Collections.EMPTY_MAP; 300 Map<String, Integer> valuesForCurrentFacetDef = Collections.EMPTY_MAP; 301 if (formSubmitted) 302 { 303 facetResults = searchResults.get().getFacetResults(); 304 } 305 306 for (FacetDefinition facet : facets) 307 { 308 String id = facet.getId(); 309 List<String> selectedFacets = Optional.ofNullable(userFacets.get(id)).orElse(Collections.emptyList()); 310 String name = FormSearchUserInputs.FACET_PREFIX + id; 311 AttributesImpl attrs = new AttributesImpl(); 312 attrs.addCDATAAttribute("name", name); 313 if (formSubmitted) 314 { 315 if (facetResults.containsKey(id)) 316 { 317 valuesForCurrentFacetDef = facetResults.get(id); 318 } 319 attrs.addCDATAAttribute("total", String.valueOf(valuesForCurrentFacetDef.values() 320 .stream() 321 .mapToInt(Integer::intValue) 322 .sum())); 323 } 324 325 XMLUtils.startElement(contentHandler, "facet", attrs); 326 facet.getLabel().toSAX(contentHandler, "label"); 327 if (formSubmitted) 328 { 329 for (String value : valuesForCurrentFacetDef.keySet()) 330 { 331 Integer count = valuesForCurrentFacetDef.get(value); 332 AttributesImpl valueAttrs = new AttributesImpl(); 333 valueAttrs.addCDATAAttribute("value", value); 334 valueAttrs.addCDATAAttribute("count", count.toString()); 335 valueAttrs.addCDATAAttribute("selected", String.valueOf(selectedFacets.contains(value))); 336 337 XMLUtils.startElement(contentHandler, "item", valueAttrs); 338 Optional.ofNullable(facet.getFacetLabel(value, currentLang)) 339 .ifPresent(LambdaUtils.wrapConsumer(i18n -> i18n.toSAX(contentHandler))); 340 XMLUtils.endElement(contentHandler, "item"); 341 } 342 } 343 XMLUtils.endElement(contentHandler, "facet"); 344 } 345 } 346 347 /** 348 * SAX the sorts 349 * @param contentHandler the content handler 350 * @param parameters the parameters 351 * @param initialSorts The initial sorts 352 * @param sortsToPropose The sorts to propose 353 * @param userSorts The user input sorts 354 * @throws SAXException if an error occurs while SAXing 355 */ 356 protected void saxSorts(ContentHandler contentHandler, Parameters parameters, List<Pair<SortDefinition, Sort.Order>> initialSorts, Collection<SortDefinition> sortsToPropose, List<Pair<String, Sort.Order>> userSorts) throws SAXException 357 { 358 Map<String, Pair<Integer, Sort.Order>> initialSortMap = new HashMap<>(); 359 for (int i = 0; i < initialSorts.size(); i++) 360 { 361 Pair<SortDefinition, Sort.Order> initialSort = initialSorts.get(i); 362 initialSortMap.put(initialSort.getLeft().getId(), Pair.of(i, initialSort.getRight())); 363 } 364 365 Map<String, Sort.Order> userSortMap = userSorts.stream() 366 .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); 367 368 for (SortDefinition sort : sortsToPropose) 369 { 370 String id = sort.getId(); 371 AttributesImpl attrs = new AttributesImpl(); 372 String name = FormSearchUserInputs.SORT_PREFIX + id; 373 if (userSortMap.containsKey(id)) 374 { 375 // selected by the user 376 int number = ListUtils.indexOf(userSorts, pair -> pair.getLeft().equals(id)); 377 _addAttributesForSelectedSort(attrs, userSortMap.get(id), number); 378 } 379 else if (userSortMap.isEmpty() && initialSortMap.containsKey(id)) 380 { 381 // initial sort 382 Pair<Integer, Sort.Order> numberAndDirection = initialSortMap.get(id); 383 _addAttributesForSelectedSort(attrs, numberAndDirection.getRight(), numberAndDirection.getLeft()); 384 } 385 386 attrs.addCDATAAttribute("name", name); 387 XMLUtils.startElement(contentHandler, "sort", attrs); 388 sort.getLabel().toSAX(contentHandler, "label"); 389 for (Sort.Order order : sort.orders()) 390 { 391 AttributesImpl valueAttrs = new AttributesImpl(); 392 valueAttrs.addCDATAAttribute("value", order.name()); 393 XMLUtils.createElement(contentHandler, "item", valueAttrs); 394 } 395 XMLUtils.endElement(contentHandler, "sort"); 396 } 397 } 398 399 private void _addAttributesForSelectedSort(AttributesImpl attrs, Sort.Order direction, int number) 400 { 401 attrs.addCDATAAttribute("selected", Boolean.toString(true)); 402 attrs.addCDATAAttribute("direction", direction.name()); 403 attrs.addCDATAAttribute("number", Integer.toString(number)); 404 } 405}