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