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, 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 enumeratorEntry the enumerator entry 259 * @throws Exception if an exception occurs 260 */ 261 protected void saxEnumeratorEntry(ContentHandler contentHandler, Map.Entry<Object, I18nizableText> enumeratorEntry) throws Exception 262 { 263 AttributesImpl attrs = new AttributesImpl(); 264 attrs.addCDATAAttribute("value", String.valueOf(enumeratorEntry.getKey())); 265 XMLUtils.startElement(contentHandler, "item", attrs); 266 enumeratorEntry.getValue().toSAX(contentHandler, "label"); 267 XMLUtils.endElement(contentHandler, "item"); 268 } 269 270 /** 271 * SAX the form search criteria values 272 * @param contentHandler the content handler 273 * @param parameters the parameters 274 * @param nonStaticCriteria the non-{@link FOSearchCriterionMode#STATIC static} criteria 275 * @param userCriteria The user input criteria 276 * @throws SAXException if an error occurs while SAXing 277 */ 278 protected void saxFormValues(ContentHandler contentHandler, Parameters parameters, List<FOSearchCriterion> nonStaticCriteria, Map<String, Object> userCriteria) throws SAXException 279 { 280 for (FOSearchCriterion criterion : nonStaticCriteria) 281 { 282 String id = criterion.getId(); 283 Object userCriterion = userCriteria.get(id); 284 if (userCriterion != null) 285 { 286 AttributesImpl attrs = new AttributesImpl(); 287 String name = FormSearchUserInputs.CRITERION_PREFIX + id; 288 attrs.addCDATAAttribute("name", name); 289 saxFormValue(contentHandler, attrs, userCriterion); 290 } 291 } 292 } 293 294 /** 295 * SAX the values of a form search criterion 296 * @param contentHandler the content handler 297 * @param attrs The XML attributes 298 * @param userCriterion The user input criterion value (is a List if multiple) 299 * @throws SAXException if an error occurs while SAXing 300 */ 301 protected void saxFormValue(ContentHandler contentHandler, Attributes attrs, Object userCriterion) throws SAXException 302 { 303 if (userCriterion instanceof List<?>) 304 { 305 List<?> multipleUserCriterion = (List<?>) userCriterion; 306 for (Object singleUserCriterion : multipleUserCriterion) 307 { 308 saxFormSingleValue(contentHandler, attrs, singleUserCriterion); 309 } 310 } 311 else 312 { 313 saxFormSingleValue(contentHandler, attrs, userCriterion); 314 } 315 } 316 317 /** 318 * SAX a single value of a form search criterion 319 * @param contentHandler the content handler 320 * @param attrs The XML attributes 321 * @param singleUserCriterion The user input criterion single value 322 * @throws SAXException if an error occurs while SAXing 323 */ 324 protected void saxFormSingleValue(ContentHandler contentHandler, Attributes attrs, Object singleUserCriterion) throws SAXException 325 { 326 XMLUtils.createElement(contentHandler, "criterion", attrs, String.valueOf(singleUserCriterion)); 327 } 328 329 /** 330 * SAX the facets 331 * @param contentHandler the content handler 332 * @param parameters the parameters 333 * @param facets The facets 334 * @param userFacets The user input facets 335 * @param searchResults The search results 336 * @param currentLang The current lang 337 * @throws SAXException if an error occurs while SAXing 338 */ 339 protected void saxFacets(ContentHandler contentHandler, Parameters parameters, Collection<FacetDefinition> facets, Map<String, List<String>> userFacets, Optional<SearchResults<AmetysObject>> searchResults, String currentLang) throws SAXException 340 { 341 boolean formSubmitted = searchResults.isPresent(); 342 Map<String, Map<String, Integer>> facetResults = Collections.EMPTY_MAP; 343 Map<String, Integer> valuesForCurrentFacetDef = Collections.EMPTY_MAP; 344 if (formSubmitted) 345 { 346 facetResults = searchResults.get().getFacetResults(); 347 } 348 349 for (FacetDefinition facet : facets) 350 { 351 String id = facet.getId(); 352 List<String> selectedFacets = Optional.ofNullable(userFacets.get(id)).orElse(Collections.emptyList()); 353 String name = FormSearchUserInputs.FACET_PREFIX + id; 354 AttributesImpl attrs = new AttributesImpl(); 355 attrs.addCDATAAttribute("name", name); 356 if (formSubmitted) 357 { 358 if (facetResults.containsKey(id)) 359 { 360 valuesForCurrentFacetDef = facetResults.get(id); 361 } 362 attrs.addCDATAAttribute("total", String.valueOf(valuesForCurrentFacetDef.values() 363 .stream() 364 .mapToInt(Integer::intValue) 365 .sum())); 366 } 367 368 XMLUtils.startElement(contentHandler, "facet", attrs); 369 facet.getLabel().toSAX(contentHandler, "label"); 370 if (formSubmitted) 371 { 372 for (String value : valuesForCurrentFacetDef.keySet()) 373 { 374 Integer count = valuesForCurrentFacetDef.get(value); 375 AttributesImpl valueAttrs = new AttributesImpl(); 376 valueAttrs.addCDATAAttribute("value", value); 377 valueAttrs.addCDATAAttribute("count", count.toString()); 378 valueAttrs.addCDATAAttribute("selected", String.valueOf(selectedFacets.contains(value))); 379 380 XMLUtils.startElement(contentHandler, "item", valueAttrs); 381 Optional.ofNullable(facet.getFacetLabel(value, currentLang)) 382 .ifPresent(LambdaUtils.wrapConsumer(i18n -> i18n.toSAX(contentHandler))); 383 XMLUtils.endElement(contentHandler, "item"); 384 } 385 } 386 XMLUtils.endElement(contentHandler, "facet"); 387 } 388 } 389 390 /** 391 * SAX the sorts 392 * @param contentHandler the content handler 393 * @param parameters the parameters 394 * @param initialSorts The initial sorts 395 * @param sortsToPropose The sorts to propose 396 * @param userSorts The user input sorts 397 * @throws SAXException if an error occurs while SAXing 398 */ 399 protected void saxSorts(ContentHandler contentHandler, Parameters parameters, List<Pair<SortDefinition, Sort.Order>> initialSorts, Collection<SortDefinition> sortsToPropose, List<Pair<String, Sort.Order>> userSorts) throws SAXException 400 { 401 Map<String, Pair<Integer, Sort.Order>> initialSortMap = new HashMap<>(); 402 for (int i = 0; i < initialSorts.size(); i++) 403 { 404 Pair<SortDefinition, Sort.Order> initialSort = initialSorts.get(i); 405 initialSortMap.put(initialSort.getLeft().getId(), Pair.of(i, initialSort.getRight())); 406 } 407 408 Map<String, Sort.Order> userSortMap = userSorts.stream() 409 .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); 410 411 for (SortDefinition sort : sortsToPropose) 412 { 413 String id = sort.getId(); 414 AttributesImpl attrs = new AttributesImpl(); 415 String name = FormSearchUserInputs.SORT_PREFIX + id; 416 if (userSortMap.containsKey(id)) 417 { 418 // selected by the user 419 int number = ListUtils.indexOf(userSorts, pair -> pair.getLeft().equals(id)); 420 _addAttributesForSelectedSort(attrs, userSortMap.get(id), number); 421 } 422 else if (userSortMap.isEmpty() && initialSortMap.containsKey(id)) 423 { 424 // initial sort 425 Pair<Integer, Sort.Order> numberAndDirection = initialSortMap.get(id); 426 _addAttributesForSelectedSort(attrs, numberAndDirection.getRight(), numberAndDirection.getLeft()); 427 } 428 429 attrs.addCDATAAttribute("name", name); 430 XMLUtils.startElement(contentHandler, "sort", attrs); 431 sort.getLabel().toSAX(contentHandler, "label"); 432 for (Sort.Order order : sort.orders()) 433 { 434 AttributesImpl valueAttrs = new AttributesImpl(); 435 valueAttrs.addCDATAAttribute("value", order.name()); 436 XMLUtils.createElement(contentHandler, "item", valueAttrs); 437 } 438 XMLUtils.endElement(contentHandler, "sort"); 439 } 440 } 441 442 private void _addAttributesForSelectedSort(AttributesImpl attrs, Sort.Order direction, int number) 443 { 444 attrs.addCDATAAttribute("selected", Boolean.toString(true)); 445 attrs.addCDATAAttribute("direction", direction.name()); 446 attrs.addCDATAAttribute("number", Integer.toString(number)); 447 } 448}