001/* 002 * Copyright 2017 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 */ 016 017package org.ametys.web.frontoffice; 018 019import java.io.IOException; 020import java.time.LocalDate; 021import java.time.format.DateTimeFormatter; 022import java.time.format.DateTimeParseException; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.Enumeration; 028import java.util.HashMap; 029import java.util.HashSet; 030import java.util.List; 031import java.util.Locale; 032import java.util.Map; 033import java.util.Map.Entry; 034import java.util.Optional; 035import java.util.Set; 036import java.util.stream.Collectors; 037import java.util.stream.Stream; 038 039import org.apache.avalon.framework.service.ServiceException; 040import org.apache.avalon.framework.service.ServiceManager; 041import org.apache.cocoon.environment.ObjectModelHelper; 042import org.apache.cocoon.environment.Request; 043import org.apache.cocoon.xml.AttributesImpl; 044import org.apache.cocoon.xml.XMLUtils; 045import org.apache.commons.collections.MapUtils; 046import org.apache.commons.lang.StringEscapeUtils; 047import org.apache.commons.lang3.ArrayUtils; 048import org.apache.commons.lang3.StringUtils; 049import org.apache.solr.client.solrj.util.ClientUtils; 050import org.xml.sax.SAXException; 051 052import org.ametys.cms.content.indexing.solr.SolrFieldNames; 053import org.ametys.cms.contenttype.ContentAttributeDefinition; 054import org.ametys.cms.contenttype.ContentType; 055import org.ametys.cms.data.type.ModelItemTypeConstants; 056import org.ametys.cms.repository.Content; 057import org.ametys.cms.repository.ContentTypeExpression; 058import org.ametys.cms.repository.LanguageExpression; 059import org.ametys.cms.search.SearchField; 060import org.ametys.cms.search.SearchResult; 061import org.ametys.cms.search.SearchResults; 062import org.ametys.cms.search.SearchResultsIterable; 063import org.ametys.cms.search.SearchResultsIterator; 064import org.ametys.cms.search.Sort; 065import org.ametys.cms.search.Sort.Order; 066import org.ametys.cms.search.query.AndQuery; 067import org.ametys.cms.search.query.ContentTypeQuery; 068import org.ametys.cms.search.query.DateQuery; 069import org.ametys.cms.search.query.DocumentTypeQuery; 070import org.ametys.cms.search.query.FullTextQuery; 071import org.ametys.cms.search.query.JoinQuery; 072import org.ametys.cms.search.query.MatchAllQuery; 073import org.ametys.cms.search.query.NotQuery; 074import org.ametys.cms.search.query.OrQuery; 075import org.ametys.cms.search.query.Query; 076import org.ametys.cms.search.query.Query.Operator; 077import org.ametys.cms.search.query.QuerySyntaxException; 078import org.ametys.cms.search.query.StringQuery; 079import org.ametys.cms.search.query.TagQuery; 080import org.ametys.cms.search.solr.SearcherFactory.Searcher; 081import org.ametys.cms.search.solr.field.LastValidationSearchField; 082import org.ametys.cms.search.solr.field.StringSearchField; 083import org.ametys.cms.tag.CMSTag; 084import org.ametys.cms.tag.Tag; 085import org.ametys.cms.tag.TagProvider; 086import org.ametys.core.util.AvalonLoggerAdapter; 087import org.ametys.core.util.LambdaUtils; 088import org.ametys.plugins.explorer.resources.Resource; 089import org.ametys.plugins.repository.AmetysObject; 090import org.ametys.plugins.repository.AmetysObjectIterable; 091import org.ametys.plugins.repository.RepositoryConstants; 092import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 093import org.ametys.plugins.repository.query.QueryHelper; 094import org.ametys.plugins.repository.query.expression.AndExpression; 095import org.ametys.plugins.repository.query.expression.Expression; 096import org.ametys.plugins.repository.query.expression.OrExpression; 097import org.ametys.runtime.i18n.I18nizableText; 098import org.ametys.runtime.model.ElementDefinition; 099import org.ametys.runtime.model.ModelItem; 100import org.ametys.runtime.model.exception.UndefinedItemPathException; 101import org.ametys.web.frontoffice.FrontOfficeSearcherFactory.FrontOfficeSearcher; 102import org.ametys.web.frontoffice.FrontOfficeSearcherFactory.FrontOfficeSolrSearchResults; 103import org.ametys.web.frontoffice.FrontOfficeSearcherFactory.QueryFacet; 104import org.ametys.web.indexing.solr.SolrWebFieldNames; 105import org.ametys.web.repository.page.Page; 106import org.ametys.web.repository.page.ZoneItem; 107import org.ametys.web.repository.site.Site; 108import org.ametys.web.repository.site.SiteTypesExtensionPoint; 109import org.ametys.web.search.query.PageContentQuery; 110import org.ametys.web.search.query.PageQuery; 111import org.ametys.web.search.query.SiteQuery; 112import org.ametys.web.search.query.SitemapQuery; 113 114/** 115 * Generates the results of a search performed on front office 116 */ 117public class SearchGenerator extends AbstractSearchGenerator 118{ 119 private static final String __FACETS_CACHE = SearchGenerator.class.getName() + "$Cache-Facets"; 120 121 /** The query adapter extension point */ 122 protected QueryAdapterFOSearchExtensionPoint _queryAdapterFOSearchEP; 123 /** The site type manager */ 124 protected SiteTypesExtensionPoint _siteTypeEP; 125 126 /** 127 * Enumeration for content type search 128 */ 129 protected enum ContentTypeSearch 130 { 131 /** To search by content type with filter (facets)*/ 132 FILTER("filter"), 133 /** To search by content type with combo box selection */ 134 LIST("list"), 135 /** To search by content type with checkbox */ 136 CHECKBOX("checkbox"), 137 /** To search by content type with checkbox and filter (facets)*/ 138 CHECKBOX_FILTER("checkbox-filter"), 139 /** To not allow to search by content type */ 140 NONE("none"); 141 142 private String _name; 143 144 private ContentTypeSearch(String name) 145 { 146 this._name = name; 147 } 148 149 @Override 150 public String toString() 151 { 152 return this._name; 153 } 154 155 /** 156 * Get enumeration value 157 * @param value The value 158 * @return The enum 159 */ 160 public static ContentTypeSearch getEnum(String value) 161 { 162 for (ContentTypeSearch ct : ContentTypeSearch.values()) 163 { 164 if (ct.toString().equals(value)) 165 { 166 return ct; 167 } 168 } 169 170 throw new IllegalArgumentException("Invalid ContentTypeChoice value: " + value); 171 } 172 } 173 174 @Override 175 public void service(ServiceManager smanager) throws ServiceException 176 { 177 super.service(smanager); 178 _queryAdapterFOSearchEP = (QueryAdapterFOSearchExtensionPoint) smanager.lookup(QueryAdapterFOSearchExtensionPoint.ROLE); 179 _siteTypeEP = (SiteTypesExtensionPoint) smanager.lookup(SiteTypesExtensionPoint.ROLE); 180 } 181 182 /** 183 * Return the type of content type's search 184 * @param request The request 185 * @return The type of search 186 */ 187 protected ContentTypeSearch getContentTypeSearch(Request request) 188 { 189 return ContentTypeSearch.getEnum(parameters.getParameter("search-by-content-types-choice", "none")); 190 } 191 192 @Override 193 protected SearchResults<AmetysObject> search(Request request, Collection<String> siteNames, String language, int pageIndex, int start, int maxResults) throws Exception 194 { 195 ContentTypeSearch searchCTypeType = getContentTypeSearch(request); 196 try 197 { 198 if (searchCTypeType.equals(ContentTypeSearch.CHECKBOX_FILTER) || searchCTypeType.equals(ContentTypeSearch.FILTER)) 199 { 200 // Retrieve current workspace 201 String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 202 203 try 204 { 205 XMLUtils.startElement(contentHandler, "content-types"); 206 207 List<Sort> sorts = new ArrayList<>(); 208 Sort sort = getSortField(request); 209 sorts.addAll(getPrimarySortFields(request)); 210 sorts.add(sort); 211 212 // Get first sort field 213 saxSort(sort); 214 215 Query queryObject = getQuery(request, siteNames, language); 216 Collection<Query> filterQueries = getFilterQueries(request, siteNames, language); 217 218 Collection<SearchField> facets = getFacets(request).values().stream().map(f -> f.getSearchField()).collect(Collectors.toList()); 219 Map<String, List<String>> facetValues = getFacetValues(request, siteNames, language); 220 221 SearchResults<AmetysObject> results = null; 222 223 String currentCtype = getContentTypeFilterValue(request); 224 if (currentCtype == null) 225 { 226 // First, do search without 'content-types' facet value 227 results = _getResultsForContentType(queryObject, filterQueries, facets, sorts, facetValues, null); 228 229 Map<String, Map<String, Integer>> facetResults = results.getFacetResults(); 230 Map<String, Integer> cTypeFacets = facetResults.get(SolrWebFieldNames.PAGE_CONTENT_TYPES); 231 Map<String, Integer> facetQueryResults = _getFacetQueryResults(results); 232 233 long totalCount = results.getTotalCount(); 234 if (totalCount > 0) 235 { 236 Collection<String> cTypes = getContentTypes(request); 237 238 // Get the first content types (in order) with at least one result 239 for (String cType : cTypes) 240 { 241 long cTypeCount; 242 if (cType.equals("resource")) 243 { 244 cTypeCount = _getNbResource(facetQueryResults); 245 } 246 else if (cTypeFacets.containsKey(cType)) 247 { 248 cTypeCount = Long.valueOf(cTypeFacets.get(cType)); 249 } 250 else 251 { 252 continue; 253 } 254 255 if (cTypeCount > 0) 256 { 257 // If facet count is equals to total count, no need to do the new search 258 if (cTypeCount < totalCount) 259 { 260 results = _getResultsForContentType(queryObject, filterQueries, facets, sorts, facetValues, cType); 261 } 262 263 currentCtype = cType; 264 break; 265 } 266 } 267 } 268 } 269 else 270 { 271 Searcher searcher = _searcherFactory.create() 272 .withQuery(queryObject) 273 .withFilterQueries(filterQueries) 274 .withFacets(facets) 275 .withFacetValues(facetValues) 276 .withLimits(0, Integer.MAX_VALUE) 277 .withSort(sorts) 278 .setCheckRights(_checkRights()); 279 280 _additionalSearchProcessing(searcher); 281 282 results = searcher.searchWithFacets(); 283 } 284 285 _handleFacetResults(request, start, maxResults, results, currentCtype); 286 287 XMLUtils.endElement(contentHandler, "content-types"); 288 289 return results; 290 } 291 finally 292 { 293 // Restore context 294 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp); 295 } 296 } 297 else 298 { 299 return super.search(request, siteNames, language, pageIndex, start, maxResults); 300 } 301 } 302 catch (QuerySyntaxException e) 303 { 304 throw new IOException("Query syntax error while searching.", e); 305 } 306 } 307 308 private Map<String, Integer> _getFacetQueryResults(SearchResults<AmetysObject> results) 309 { 310 return Optional.of(results) 311 .filter(FrontOfficeSolrSearchResults.class::isInstance) 312 .map(FrontOfficeSolrSearchResults.class::cast) 313 .map(FrontOfficeSolrSearchResults::getFacetQueryResults) 314 .orElseGet(Collections::emptyMap); 315 } 316 317 private int _getNbResource(Map<String, Integer> facetQueryResults) 318 { 319 return Optional.ofNullable(facetQueryResults.get(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME)).orElse(0); 320 } 321 322 private SearchResults<AmetysObject> _getResultsForContentType(Query queryObject, Collection<Query> filterQueries, Collection<SearchField> facets, List<Sort> sorts, Map<String, List<String>> initialFacetValues, String forceContentType) throws Exception 323 { 324 Map<String, List<String>> facetValues = new HashMap<>(initialFacetValues); 325 326 if (forceContentType == null) 327 { 328 facetValues.remove(SolrWebFieldNames.PAGE_CONTENT_TYPES); 329 } 330 else 331 { 332 facetValues.put(SolrWebFieldNames.PAGE_CONTENT_TYPES, new ArrayList<>()); 333 facetValues.get(SolrWebFieldNames.PAGE_CONTENT_TYPES).add(forceContentType); 334 } 335 336 Searcher searcher = _searcherFactory.create() 337 .withQuery(queryObject) 338 .withFilterQueries(filterQueries) 339 .withFacets(facets) 340 .withFacetValues(facetValues) 341 .withLimits(0, Integer.MAX_VALUE) 342 .withSort(sorts) 343 .setCheckRights(_checkRights()); 344 345 _additionalSearchProcessing(searcher); 346 347 return searcher.searchWithFacets(); 348 } 349 350 @Override 351 protected void _additionalSearchProcessing(Searcher searcher) 352 { 353 Request request = ObjectModelHelper.getRequest(objectModel); 354 355 if (searcher instanceof FrontOfficeSearcher) 356 { 357 FrontOfficeSearcher foSearcher = (FrontOfficeSearcher) searcher; 358 Collection<QueryFacet> queryFacets = getQueryFacets(request); 359 if (!queryFacets.isEmpty()) 360 { 361 foSearcher.withQueryFacets(queryFacets) 362 .withQueryFacetValues(getQueryFacetValues(request)); 363 } 364 } 365 } 366 367 private void _handleFacetResults(Request request, int start, int maxResults, SearchResults<AmetysObject> results, String currentCtype) throws SAXException 368 { 369 int count = 0; 370 Map<String, Map<String, Integer>> facetResults = results.getFacetResults(); 371 Map<String, Integer> facetQueryResults = _getFacetQueryResults(results); 372 if (facetResults.containsKey(SolrWebFieldNames.PAGE_CONTENT_TYPES) || facetQueryResults.containsKey(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME)) 373 { 374 Map<String, Integer> cTypeFacets = Optional.ofNullable(facetResults.get(SolrWebFieldNames.PAGE_CONTENT_TYPES)).orElseGet(Collections::emptyMap); 375 376 for (String contentTypeId : getContentTypes(request)) 377 { 378 int nbResults; 379 if ("resource".equals(contentTypeId)) 380 { 381 nbResults = _getNbResource(facetQueryResults); 382 } 383 else if (cTypeFacets.containsKey(contentTypeId) && cTypeFacets.get(contentTypeId) > 0) 384 { 385 nbResults = cTypeFacets.get(contentTypeId); 386 } 387 else 388 { 389 continue; 390 } 391 392 boolean current = contentTypeId.equals(currentCtype) || currentCtype == null && count == 0; 393 count++; 394 395 AttributesImpl attr = new AttributesImpl(); 396 if (current) 397 { 398 attr.addCDATAAttribute("current", "true"); 399 } 400 401 XMLUtils.startElement(contentHandler, contentTypeId, attr); 402 403 if (contentTypeId.equals("resource")) 404 { 405 new I18nizableText("plugin.web", "PLUGINS_WEB_SERVICE_FRONT_SEARCH_ON_DOCUMENTS").toSAX(contentHandler, "label"); 406 } 407 else 408 { 409 ContentType contentType = _cTypeExtPt.getExtension(contentTypeId); 410 if (contentType != null) 411 { 412 contentType.getLabel().toSAX(contentHandler, "label"); 413 } 414 } 415 416 // SAX results 417 AttributesImpl atts = new AttributesImpl(); 418 atts.addCDATAAttribute("total", String.valueOf(nbResults)); 419 atts.addCDATAAttribute("maxScore", String.valueOf(results.getMaxScore())); 420 421 if (current) 422 { 423 XMLUtils.startElement(contentHandler, "hits", atts); 424 saxHits(results, start, maxResults); 425 XMLUtils.endElement(contentHandler, "hits"); 426 427 // SAX pagination 428 saxPagination(results.getTotalCount(), start, maxResults); 429 } 430 else 431 { 432 XMLUtils.createElement(contentHandler, "hits", atts); 433 } 434 435 XMLUtils.endElement(contentHandler, contentTypeId); 436 } 437 } 438 } 439 440 @Override 441 protected Query getQuery(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException 442 { 443 List<Query> wordingQueries = getWordingQueries(request, siteNames, language); 444 445 // Query to execute on joined contents 446 List<Query> contentQueries = new ArrayList<>(wordingQueries); // add wording queries 447 contentQueries.addAll(getContentQueries(request, siteNames, language)); // add specific queries to contents 448 Query contentQuery = new AndQuery(contentQueries); 449 450 List<Query> contentOrResourcesQueries = new ArrayList<>(); 451 contentOrResourcesQueries.add(contentQuery); 452 if (!wordingQueries.isEmpty()) 453 { 454 contentOrResourcesQueries.addAll(getContentResourcesOrAttachmentQueries(new AndQuery(wordingQueries))); // add queries on join content's resources 455 } 456 457 Query finalContentQuery = new PageContentQuery(new OrQuery(contentOrResourcesQueries)); 458 459 // Query to execute on pages 460 List<Query> pagesQueries = new ArrayList<>(wordingQueries); // add wording queries 461 pagesQueries.addAll(getPageQueries(request, siteNames, language)); // add specific queries to pages 462 Query pageQuery = pagesQueries.isEmpty() && contentQueries.isEmpty() ? new MatchAllQuery() : new AndQuery(pagesQueries); 463 464 List<Query> pageOrResourcesQueries = new ArrayList<>(); 465 pageOrResourcesQueries.add(pageQuery); 466 if (!wordingQueries.isEmpty()) 467 { 468 pageOrResourcesQueries.addAll(getPageResourcesOrAttachmentQueries(new AndQuery(wordingQueries))); // add queries on join page's resources 469 } 470 471 Query finalPageQuery = new OrQuery(pageOrResourcesQueries); 472 473 Query finalQuery = new OrQuery(finalPageQuery, finalContentQuery); 474 475 for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter()) 476 { 477 finalQuery = queryAdapter.modifyQuery(finalQuery, request, siteNames, language); 478 } 479 480 return finalQuery; 481 } 482 483 /** 484 * Get the queries on wording (keywords, no words, exact wording, no words) 485 * @param request the request 486 * @param siteNames the site names 487 * @param language the language 488 * @return the queries on wording 489 */ 490 protected List<Query> getWordingQueries(Request request, Collection<String> siteNames, String language) 491 { 492 List<Query> queries = new ArrayList<>(); 493 494 addTextFieldQuery(queries, language, request); 495 addAllWordsTextFieldQuery(queries, language, request); 496 addExactWordingTextFieldQuery(queries, language, request); 497 addNoWordsTextFieldQuery(queries, language, request); 498 499 return queries; 500 } 501 502 /** 503 * Get the queries to be apply on joined contents ONLY 504 * @param request the request 505 * @param siteNames the site names 506 * @param language the language 507 * @return the queries for contents only 508 */ 509 protected List<Query> getContentQueries(Request request, Collection<String> siteNames, String language) 510 { 511 List<Query> queries = new ArrayList<>(); 512 queries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT)); 513 addAttributeQuery(queries, language, request); 514 return queries; 515 } 516 517 /** 518 * Get the queries to be apply on pages ONLY 519 * @param request the request 520 * @param siteNames the site names 521 * @param language the language 522 * @return the queries for pages only 523 */ 524 protected List<Query> getPageQueries(Request request, Collection<String> siteNames, String language) 525 { 526 return new ArrayList<>(); 527 } 528 529 @Override 530 protected Collection<Query> getFilterQueries(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException 531 { 532 List<Query> queries = new ArrayList<>(); 533 534 Query siteQuery = new SiteQuery(siteNames); 535 Query sitemapQuery = new SitemapQuery(language); 536 for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter()) 537 { 538 siteQuery = queryAdapter.modifySiteQueryFilter(siteQuery, request, siteNames, language); 539 sitemapQuery = queryAdapter.modifySitemapQueryFilter(sitemapQuery, request, siteNames, language); 540 } 541 542 queries.add(siteQuery); 543 queries.add(sitemapQuery); 544 545 addContentTypeQuery(queries, request); 546 addTagsQuery(queries, request); 547 addPagesQuery(queries, request); 548 addDateQuery(queries, request); 549 550 return queries; 551 } 552 553 @Override 554 protected Map<String, List<String>> getFacetValues(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException 555 { 556 Map<String, List<String>> facetValues = new HashMap<>(); 557 558 String currentCType = getContentTypeFilterValue(request); 559 if (currentCType != null) 560 { 561 facetValues.put(SolrWebFieldNames.PAGE_CONTENT_TYPES, new ArrayList<>()); 562 if (!"resource".equals(currentCType)) 563 { 564 facetValues.get(SolrWebFieldNames.PAGE_CONTENT_TYPES).add(currentCType); 565 } 566 } 567 else if (getContentTypeSearch(request).equals(ContentTypeSearch.FILTER) || getContentTypeSearch(request).equals(ContentTypeSearch.CHECKBOX_FILTER)) 568 { 569 // Get the first content types as facet value 570 facetValues.put(SolrWebFieldNames.PAGE_CONTENT_TYPES, new ArrayList<>()); 571 String firstContentType = getContentTypes(request).iterator().next(); 572 if (!"resource".equals(firstContentType)) 573 { 574 facetValues.get(SolrWebFieldNames.PAGE_CONTENT_TYPES).add(firstContentType); 575 } 576 } 577 578 Map<String, FacetField> facets = getFacets(request); 579 for (String fieldName : facets.keySet()) 580 { 581 String[] parameterValues = request.getParameterValues("metadata-" + fieldName); 582 if (parameterValues != null && parameterValues.length > 0 && StringUtils.isNotEmpty(parameterValues[0])) 583 { 584 facetValues.put(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + fieldName, new ArrayList<>()); 585 List<String> preparedParameterValues = Stream.of(parameterValues) 586 .map(StringEscapeUtils::unescapeXml) 587 .map(ClientUtils::escapeQueryChars) 588 .collect(Collectors.toList()); 589 facetValues.get(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + fieldName).addAll(preparedParameterValues); 590 } 591 } 592 593 return facetValues; 594 } 595 596 @Override 597 protected Collection<String> getQueryFacetValues(Request request) 598 { 599 List<String> queryFacetValues = new ArrayList<>(); 600 601 String currentCType = getContentTypeFilterValue(request); 602 if (currentCType != null) 603 { 604 if ("resource".equals(currentCType)) 605 { 606 queryFacetValues.add(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME); 607 } 608 } 609 else if (getContentTypeSearch(request).equals(ContentTypeSearch.FILTER) || getContentTypeSearch(request).equals(ContentTypeSearch.CHECKBOX_FILTER)) 610 { 611 // Get the first content types as facet value 612 String firstContentType = getContentTypes(request).iterator().next(); 613 if ("resource".equals(firstContentType)) 614 { 615 queryFacetValues.add(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME); 616 } 617 } 618 619 return queryFacetValues; 620 } 621 622 @Override 623 protected Map<String, FacetField> getFacets(Request request) throws IllegalArgumentException 624 { 625 ZoneItem zoneItem = getZoneItem(request); 626 String facetCacheAttrName = __FACETS_CACHE + "$" + Optional.ofNullable(zoneItem).map(ZoneItem::getId).orElse("null"); 627 @SuppressWarnings("unchecked") 628 Map<String, FacetField> cache = (Map<String, FacetField>) request.getAttribute(facetCacheAttrName); 629 if (cache != null) 630 { 631 return cache; 632 } 633 634 Map<String, FacetField> facets = new HashMap<>(); 635 636 // Facet for content types 637 ContentTypeSearch contentTypeSearch = getContentTypeSearch(request); 638 if (contentTypeSearch.equals(ContentTypeSearch.FILTER) || contentTypeSearch.equals(ContentTypeSearch.CHECKBOX_FILTER)) 639 { 640 SearchField cTypeField = new ContentTypeSearchField(); 641 642 facets.put(PAGE_CONTENT_TYPES, new ContentTypeFacetField(cTypeField)); 643 } 644 645 if (useFacets()) 646 { 647 Collection<String> contentTypes = getContentTypes(request); 648 649 if (zoneItem != null && zoneItem.getServiceParameters().hasValue("search-by-metadata")) 650 { 651 String[] metadataPaths = zoneItem.getServiceParameters().getValue("search-by-metadata"); 652 for (String metadataPath : metadataPaths) 653 { 654 _addAttributeFacet(facets, contentTypes, metadataPath); 655 } 656 } 657 } 658 659 request.setAttribute(facetCacheAttrName, facets); 660 661 return facets; 662 } 663 664 /** 665 * Add attribute facet to facets map 666 * @param facets the facets map 667 * @param contentTypes the content types 668 * @param attributePath the attribute path 669 */ 670 protected void _addAttributeFacet(Map<String, FacetField> facets, Collection<String> contentTypes, String attributePath) 671 { 672 ModelItem modelItem = _getModelItemFromContentTypes(contentTypes, attributePath); 673 674 if (modelItem != null && modelItem instanceof ElementDefinition) 675 { 676 ElementDefinition elementDefinition = (ElementDefinition) modelItem; 677 if (elementDefinition.getEnumerator() != null || elementDefinition.getType().getId().equals(ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID)) 678 { 679 Optional<SearchField> searchField = _searchHelper.getSearchField(contentTypes, attributePath); 680 if (searchField.isPresent()) 681 { 682 String searchFieldName = searchField.get().getName(); 683 StringSearchField facetField = new StringSearchField(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + searchFieldName); 684 facets.put(searchFieldName, new AttributeFacetField(facetField, modelItem, new AvalonLoggerAdapter(getLogger()))); 685 } 686 } 687 } 688 } 689 690 @Override 691 protected Set<QueryFacet> getQueryFacets(Request request) 692 { 693 Set<QueryFacet> queryFacets = new HashSet<>(); 694 Collection<String> contentTypes = getContentTypes(request); 695 696 // For handling "resource" content type in facets for content types 697 ContentTypeSearch contentTypeSearch = getContentTypeSearch(request); 698 if ((contentTypeSearch.equals(ContentTypeSearch.FILTER) || contentTypeSearch.equals(ContentTypeSearch.CHECKBOX_FILTER)) 699 && contentTypes.contains("resource")) 700 { 701 queryFacets.add(new QueryFacet(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME, 702 PAGE_CONTENT_TYPES, 703 DOCUMENT_TYPE + ":" + TYPE_PAGE_RESOURCE)); 704 } 705 706 for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter()) 707 { 708 queryFacets = queryAdapter.modifyQueryFacets(queryFacets, request); 709 } 710 711 return queryFacets; 712 } 713 714 /** 715 * Get the filter queries for a single fixed content type. 716 * 717 * @param request The request. 718 * @param cType The fixed content type. 719 * @param siteNames the site names. 720 * @param language The language. 721 * @return The filter queries. 722 */ 723 protected Collection<Query> getFixedCTypeFilterQueries(Request request, String cType, Collection<String> siteNames, String language) 724 { 725 List<Query> queries = new ArrayList<>(); 726 727 queries.add(new SiteQuery(siteNames)); 728 queries.add(new SitemapQuery(language)); 729 730 if ("resource".equals(cType)) 731 { 732 queries.add(new DocumentTypeQuery(TYPE_PAGE_RESOURCE)); 733 } 734 else 735 { 736 // Fixed content type query. 737 queries.add(new PageContentQuery(new ContentTypeQuery(cType))); 738 } 739 addTagsQuery(queries, request); 740 addPagesQuery(queries, request); 741 addDateQuery(queries, request); 742 743 return queries; 744 } 745 746 @Override 747 protected Collection<String> getDocumentTypes(Request request) 748 { 749 List<String> documentTypes = new ArrayList<>(); 750 751 Collection<String> cTypes = getContentTypes(request); 752 if (cTypes.size() == 1 && "resource".equals(cTypes.iterator().next())) 753 { 754 documentTypes.add(TYPE_PAGE_RESOURCE); 755 } 756 // An empty collections means "all". 757 else if (cTypes.isEmpty() || cTypes.contains("resource")) 758 { 759 documentTypes.addAll(Arrays.asList(TYPE_PAGE, TYPE_PAGE_RESOURCE)); 760 } 761 else 762 { 763 documentTypes.add(TYPE_PAGE); 764 } 765 766 // Add other documents types (by priority order) 767 for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter()) 768 { 769 queryAdapter.addDocumentType(documentTypes); 770 } 771 772 return documentTypes; 773 } 774 775 @Override 776 protected Collection<String> getFields() 777 { 778 return Collections.emptyList(); 779 } 780 781 @Override 782 protected void saxHits(SearchResults<AmetysObject> results, int start, int maxResults) throws SAXException 783 { 784 SearchResultsIterable<SearchResult<AmetysObject>> resultsIt = results.getResults(); 785 long limit = Math.min(start + maxResults, resultsIt.getSize()); 786 float maxScore = results.getMaxScore(); 787 788 SearchResultsIterator<SearchResult<AmetysObject>> it = resultsIt.iterator(); 789 it.skip(start); 790 for (int i = start; i < limit; i++) 791 { 792 if (it.hasNext()) // this should return true except if there is a inconsistency between repository and Solr index 793 { 794 SearchResult<AmetysObject> searchResult = it.next(); 795 float score = searchResult.getScore(); 796 AmetysObject ametysObject = searchResult.getObject(); 797 if (ametysObject instanceof Page) 798 { 799 saxPageHit(score, maxScore, (Page) ametysObject); 800 } 801 else if (ametysObject instanceof Resource) 802 { 803 saxResourceHit(score, maxScore, (Resource) ametysObject); 804 } 805 } 806 807 } 808 } 809 810 @Override 811 protected Sort getSortField(Request request) 812 { 813 if (request.getParameter("sort-by-title-for-sorting") != null || request.getParameter("sort-by-title") != null) 814 { 815 return new Sort(TITLE_SORT, Order.ASC); 816 } 817 else if (request.getParameter("sort-by-lastValidation") != null) 818 { 819 return new Sort(LastValidationSearchField.NAME, Order.DESC); 820 } 821 else 822 { 823 // Generic sort field (with hardcorded descending order) 824 Enumeration paramNames = request.getParameterNames(); 825 while (paramNames.hasMoreElements()) 826 { 827 String param = (String) paramNames.nextElement(); 828 if (param.startsWith("sort-by-")) 829 { 830 String fieldName = StringUtils.removeStart(param, "sort-by-"); 831 return new Sort(fieldName, Order.ASC); 832 } 833 } 834 } 835 836 return new Sort("score", Order.DESC); 837 } 838 839 @Override 840 protected List<Sort> getPrimarySortFields(Request request) 841 { 842 return new ArrayList<>(); 843 } 844 845 @Override 846 protected void saxFormFields(Request request, String siteName, String lang) throws SAXException 847 { 848 XMLUtils.createElement(contentHandler, "textfield"); 849 850 boolean advancedSearch = parameters.getParameterAsBoolean("advanced-search", true); 851 if (advancedSearch) 852 { 853 XMLUtils.createElement(contentHandler, "all-words"); 854 XMLUtils.createElement(contentHandler, "exact-wording"); 855 XMLUtils.createElement(contentHandler, "no-words"); 856 } 857 858 ContentTypeSearch contentTypeSearch = getContentTypeSearch(request); 859 XMLUtils.createElement(contentHandler, "content-types-choice", contentTypeSearch.toString()); 860 861 _saxContentTypeCriteria(request); 862 _saxAttributeCriteria(request, lang); 863 _saxTagsCriteria(siteName); 864 _saxSitemapCriteria(); 865 866 boolean multisite = parameters.getParameterAsBoolean("search-multisite", false); 867 if (multisite) 868 { 869 XMLUtils.createElement(contentHandler, "multisite"); 870 871 XMLUtils.startElement(contentHandler, "sites"); 872 Collection<String> allSites = _siteManager.getSiteNames(); 873 for (String name : allSites) 874 { 875 Site site = _siteManager.getSite(name); 876 if (!_isPrivate(site)) 877 { 878 AttributesImpl attr = new AttributesImpl(); 879 attr.addCDATAAttribute("name", name); 880 if (name.equals(siteName)) 881 { 882 attr.addCDATAAttribute("current", "true"); 883 } 884 XMLUtils.createElement(contentHandler, "site", attr, StringUtils.defaultString(site.getTitle())); 885 } 886 } 887 XMLUtils.endElement(contentHandler, "sites"); 888 } 889 890 if (StringUtils.isNotBlank(parameters.getParameter("startDate", ""))) 891 { 892 XMLUtils.createElement(contentHandler, "dates", "true"); 893 } 894 } 895 896 private boolean _isPrivate(Site site) 897 { 898 String type = site.getType(); 899 return _siteTypeEP.getExtension(type).isPrivateType(); 900 } 901 902 private void _saxAttributeCriteria(Request request, String language) throws SAXException 903 { 904 ZoneItem zoneItem = getZoneItem(request); 905 if (zoneItem != null && zoneItem.getServiceParameters().hasValue("search-by-metadata")) 906 { 907 String[] attributePaths = zoneItem.getServiceParameters().getValue("search-by-metadata"); 908 if (attributePaths.length > 0) 909 { 910 Collection<String> cTypes = new ArrayList<>(getContentTypes(request)); 911 912 for (String attributePath : attributePaths) 913 { 914 ModelItem modelItem = _getModelItemFromContentTypes(cTypes, attributePath); 915 saxAttributeDefinition(modelItem, attributePath, language); 916 } 917 } 918 } 919 } 920 921 /** 922 * Generates SAX events for attribute definition 923 * 924 * @param modelItem The attribute definition. 925 * @param attributePath The attribute path 926 * @param language The current language 927 * @throws SAXException If an error occurred while generating SAX events 928 */ 929 protected void saxAttributeDefinition(ModelItem modelItem, String attributePath, String language) throws SAXException 930 { 931 AttributesImpl attrs = new AttributesImpl(); 932 attrs.addCDATAAttribute("name", attributePath); 933 934 XMLUtils.startElement(contentHandler, "metadata", attrs); 935 if (modelItem != null) 936 { 937 modelItem.getLabel().toSAX(contentHandler, "label"); 938 } 939 else 940 { 941 XMLUtils.startElement(contentHandler, "label"); 942 XMLUtils.data(contentHandler, attributePath); 943 XMLUtils.endElement(contentHandler, "label"); 944 } 945 946 saxEnumeratorValueForAttribute(modelItem, attributePath, language); 947 XMLUtils.endElement(contentHandler, "metadata"); 948 } 949 950 /** 951 * Sax enumeration value for enum or a content attribute 952 * @param modelItem The attribute definition. 953 * @param attributePath The attribute path 954 * @param language The current language 955 * @throws SAXException If an error occurred while saxing 956 */ 957 protected void saxEnumeratorValueForAttribute(ModelItem modelItem, String attributePath, String language) throws SAXException 958 { 959 if (modelItem != null && modelItem instanceof ElementDefinition) 960 { 961 ElementDefinition attributeDefinition = (ElementDefinition) modelItem; 962 if (attributeDefinition.getEnumerator() != null) 963 { 964 XMLUtils.startElement(contentHandler, "enumeration"); 965 try 966 { 967 Map<Object, I18nizableText> entries = attributeDefinition.getEnumerator().getTypedEntries(); 968 for (Object key : entries.keySet()) 969 { 970 AttributesImpl attrItem = new AttributesImpl(); 971 attrItem.addCDATAAttribute("value", (String) key); 972 XMLUtils.startElement(contentHandler, "item", attrItem); 973 entries.get(key).toSAX(contentHandler, "label"); 974 XMLUtils.endElement(contentHandler, "item"); 975 } 976 } 977 catch (Exception e) 978 { 979 getLogger().error("An error occurred getting enumerator items for attribute : " + attributePath, e); 980 } 981 XMLUtils.endElement(contentHandler, "enumeration"); 982 } 983 else if (attributeDefinition instanceof ContentAttributeDefinition) 984 { 985 ContentAttributeDefinition contentAttributeDefinition = (ContentAttributeDefinition) attributeDefinition; 986 XMLUtils.startElement(contentHandler, "enumeration"); 987 Map<String, String> values = getContentValues(contentAttributeDefinition.getContentTypeId(), language); 988 for (Entry<String, String> entry : values.entrySet()) 989 { 990 AttributesImpl attrItem = new AttributesImpl(); 991 attrItem.addCDATAAttribute("value", entry.getKey()); 992 XMLUtils.startElement(contentHandler, "item", attrItem); 993 XMLUtils.createElement(contentHandler, "label", entry.getValue()); 994 XMLUtils.endElement(contentHandler, "item"); 995 } 996 XMLUtils.endElement(contentHandler, "enumeration"); 997 } 998 } 999 } 1000 1001 /** 1002 * Get values for contents enumeration 1003 * @param cTypeId The id of content type 1004 * @param language The current language 1005 * @return The contents 1006 */ 1007 protected Map<String, String> getContentValues(String cTypeId, String language) 1008 { 1009 try 1010 { 1011 boolean multilingual = _cTypeExtPt.getExtension(cTypeId).isMultilingual(); 1012 Expression expr = new AndExpression( 1013 _getContentTypeExpression(cTypeId), 1014 multilingual ? null : new LanguageExpression(org.ametys.plugins.repository.query.expression.Expression.Operator.EQ, language)); 1015 AmetysObjectIterable<Content> contents = _resolver.query(QueryHelper.getXPathQuery(null, RepositoryConstants.NAMESPACE_PREFIX + ":content", expr)); 1016 1017 return contents.stream() 1018 .collect(Collectors.toMap(Content::getId, c -> c.getTitle(new Locale(language)))) 1019 .entrySet() 1020 .stream() 1021 .sorted(Map.Entry.comparingByValue()) // sort by title 1022 .collect(LambdaUtils.Collectors.toLinkedHashMap(Map.Entry::getKey, Map.Entry::getValue)); 1023 } 1024 catch (Exception e) 1025 { 1026 getLogger().error("Failed to get content enumeration for content type " + cTypeId, e); 1027 return MapUtils.EMPTY_MAP; 1028 } 1029 } 1030 1031 private Expression _getContentTypeExpression(String parentCTypeId) 1032 { 1033 Stream<String> subCTypesIds = _cTypeExtPt.getSubTypes(parentCTypeId).stream(); 1034 Expression[] exprs = Stream.concat(Stream.of(parentCTypeId), subCTypesIds) 1035 .map(cTypeId -> new ContentTypeExpression(org.ametys.plugins.repository.query.expression.Expression.Operator.EQ, cTypeId)) 1036 .toArray(Expression[]::new); 1037 return new OrExpression(exprs); 1038 } 1039 1040 @Override 1041 protected void saxFormValues(Request request, int start, int offset) throws SAXException 1042 { 1043 _saxTextField(request); 1044 _saxMetadataValues(request); 1045 _saxAllWords(request); 1046 _saxExactWording(request); 1047 _saxNoWords(request); 1048 _saxContentType(request); 1049 _saxTags(request); 1050 _saxPages(request); 1051 _saxMultisite(request); 1052 _saxDates(request); 1053 } 1054 1055 private void _saxDates(Request request) throws SAXException 1056 { 1057 String startDate = request.getParameter("startDate"); 1058 String endDate = request.getParameter("endDate"); 1059 1060 if (StringUtils.isNotBlank(startDate) && StringUtils.isNotBlank(endDate) && startDate.compareTo(endDate) > 0) 1061 { 1062 String tmp = startDate; 1063 startDate = endDate; 1064 endDate = tmp; 1065 } 1066 1067 if (StringUtils.isNotBlank(startDate)) 1068 { 1069 XMLUtils.createElement(contentHandler, "startDate", startDate); 1070 } 1071 if (StringUtils.isNotBlank(endDate)) 1072 { 1073 XMLUtils.createElement(contentHandler, "endDate", endDate); 1074 } 1075 } 1076 1077 private void _saxTextField(Request request) throws SAXException 1078 { 1079 String textfield = request.getParameter("textfield"); 1080 XMLUtils.createElement(contentHandler, "textfield", textfield != null ? textfield : ""); 1081 } 1082 1083 private void _saxMetadataValues(Request request) throws SAXException 1084 { 1085 ZoneItem zoneItem = getZoneItem(request); 1086 if (zoneItem != null && zoneItem.getServiceParameters().hasValue("search-by-metadata")) 1087 { 1088 String[] metadataPaths = zoneItem.getServiceParameters().getValue("search-by-metadata"); 1089 if (metadataPaths.length > 0) 1090 { 1091 XMLUtils.startElement(contentHandler, "metadata"); 1092 for (String metadataPath : metadataPaths) 1093 { 1094 String[] values = request.getParameterValues("metadata-" + metadataPath.replaceAll("/", ".")); 1095 if (values != null) 1096 { 1097 for (String value : values) 1098 { 1099 AttributesImpl attrs = new AttributesImpl(); 1100 attrs.addCDATAAttribute("name", metadataPath); 1101 XMLUtils.createElement(contentHandler, "metadata", attrs, value != null ? value : ""); 1102 } 1103 } 1104 } 1105 XMLUtils.endElement(contentHandler, "metadata"); 1106 } 1107 } 1108 } 1109 1110 private void _saxAllWords(Request request) throws SAXException 1111 { 1112 String textfield = request.getParameter("all-words"); 1113 XMLUtils.createElement(contentHandler, "all-words", textfield != null ? textfield : ""); 1114 } 1115 1116 private void _saxExactWording(Request request) throws SAXException 1117 { 1118 String textfield = request.getParameter("exact-wording"); 1119 XMLUtils.createElement(contentHandler, "exact-wording", textfield != null ? textfield : ""); 1120 } 1121 1122 private void _saxNoWords(Request request) throws SAXException 1123 { 1124 String textfield = request.getParameter("no-words"); 1125 XMLUtils.createElement(contentHandler, "no-words", textfield != null ? textfield : ""); 1126 } 1127 1128 private void _saxContentType(Request request) throws SAXException 1129 { 1130 String[] cTypes = request.getParameterValues("content-types"); 1131 if (cTypes != null && cTypes.length > 0 && !(cTypes.length == 1 && cTypes[0].equals(""))) 1132 { 1133 for (String cType : cTypes) 1134 { 1135 XMLUtils.createElement(contentHandler, "content-type", cType); 1136 } 1137 } 1138 } 1139 1140 private void _saxTags(Request request) throws SAXException 1141 { 1142 String size = request.getParameter("tags-size"); 1143 if (!StringUtils.isEmpty(size)) 1144 { 1145 int nbCat = Integer.parseInt(size); 1146 for (int i = 1; i < nbCat + 1; i++) 1147 { 1148 String[] tags = request.getParameterValues("tags-" + i); 1149 if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals(""))) 1150 { 1151 if (tags.length == 1) 1152 { 1153 tags = tags[0].split(","); 1154 } 1155 1156 for (String tag : tags) 1157 { 1158 XMLUtils.createElement(contentHandler, "tag", tag); 1159 } 1160 1161 } 1162 } 1163 } 1164 1165 String[] tags = request.getParameterValues("tags"); 1166 if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals(""))) 1167 { 1168 for (String tag : tags) 1169 { 1170 XMLUtils.createElement(contentHandler, "tag", tag); 1171 } 1172 } 1173 } 1174 1175 private void _saxPages(Request request) throws SAXException 1176 { 1177 String[] pages = request.getParameterValues("pages"); 1178 if (pages != null && pages.length > 0 && !(pages.length == 1 && pages[0].equals(""))) 1179 { 1180 for (String id : pages) 1181 { 1182 XMLUtils.createElement(contentHandler, "page", id); 1183 } 1184 } 1185 } 1186 1187 private void _saxMultisite(Request request) throws SAXException 1188 { 1189 boolean multisite = request.getParameter("multisite") != null; 1190 if (multisite) 1191 { 1192 XMLUtils.createElement(contentHandler, "multisite"); 1193 1194 String[] sites = request.getParameterValues("sites"); 1195 if (sites != null && sites.length > 0 && !(sites.length == 1 && sites[0].equals(""))) 1196 { 1197 for (String site : sites) 1198 { 1199 XMLUtils.createElement(contentHandler, "site", site); 1200 } 1201 } 1202 1203 } 1204 } 1205 1206 /** 1207 * Get the content type's 1208 * 1209 * @param request The request 1210 * @return the content type's 1211 */ 1212 protected Collection<String> getContentTypes(Request request) 1213 { 1214 String[] cTypes = request.getParameterValues("content-types"); 1215 1216 if (cTypes != null && cTypes.length > 0 && !(cTypes.length == 1 && cTypes[0].equals(""))) 1217 { 1218 if (cTypes.length == 1) 1219 { 1220 cTypes = cTypes[0].split(","); 1221 } 1222 1223 return Arrays.asList(cTypes); 1224 } 1225 return _getAvailableContentTypes(request); 1226 } 1227 1228 private Collection<String> _getAvailableContentTypes(Request request) 1229 { 1230 @SuppressWarnings("unchecked") 1231 Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); 1232 if (parentContext == null) 1233 { 1234 parentContext = Collections.emptyMap(); 1235 } 1236 1237 String[] serviceCTypes = (String[]) parentContext.get("search-by-content-types"); 1238 1239 ContentTypeSearch contentTypeSearch = getContentTypeSearch(request); 1240 1241 if (ArrayUtils.isNotEmpty(serviceCTypes) && StringUtils.isNotBlank(serviceCTypes[0])) 1242 { 1243 return Arrays.asList(serviceCTypes); 1244 } 1245 else if (!contentTypeSearch.equals(ContentTypeSearch.NONE)) 1246 { 1247 return _getAllContentTypes(); 1248 } 1249 1250 return Collections.emptyList(); 1251 } 1252 1253 private Set<String> _getAllContentTypes() 1254 { 1255 Set<String> allCTypes = new HashSet<>(_cTypeExtPt.getExtensionsIds()); 1256 allCTypes.add("resource"); 1257 return allCTypes; 1258 } 1259 1260 private void _saxContentTypeCriteria(Request request) throws SAXException 1261 { 1262 Collection<String> cTypes = _getAvailableContentTypes(request); 1263 1264 XMLUtils.startElement(contentHandler, "content-types"); 1265 for (String cTypeId : cTypes) 1266 { 1267 if ("resource".equals(cTypeId)) 1268 { 1269 AttributesImpl attr = new AttributesImpl(); 1270 attr.addCDATAAttribute("id", cTypeId); 1271 XMLUtils.startElement(contentHandler, "type", attr); 1272 new I18nizableText("plugin.web", "PLUGINS_WEB_SERVICE_FRONT_SEARCH_ON_DOCUMENTS").toSAX(contentHandler); 1273 XMLUtils.endElement(contentHandler, "type"); 1274 } 1275 else if (StringUtils.isNotEmpty(cTypeId)) 1276 { 1277 ContentType cType = _cTypeExtPt.getExtension(cTypeId); 1278 1279 if (cType != null) 1280 { 1281 AttributesImpl attr = new AttributesImpl(); 1282 attr.addCDATAAttribute("id", cTypeId); 1283 XMLUtils.startElement(contentHandler, "type", attr); 1284 cType.getLabel().toSAX(contentHandler); 1285 XMLUtils.endElement(contentHandler, "type"); 1286 } 1287 else 1288 { 1289 getLogger().warn("Cannot sax information about the unexising ContentType '" + cTypeId + "' for url " + request.getRequestURI()); 1290 } 1291 } 1292 1293 } 1294 XMLUtils.endElement(contentHandler, "content-types"); 1295 } 1296 1297 private void _saxTagsCriteria(String siteName) throws SAXException 1298 { 1299 @SuppressWarnings("unchecked") 1300 Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); 1301 if (parentContext == null) 1302 { 1303 parentContext = Collections.emptyMap(); 1304 } 1305 1306 Map<String, Object> contextParameters = new HashMap<>(); 1307 contextParameters.put("siteName", siteName); 1308 1309 String[] tagArray = (String[]) parentContext.get("search-by-tags"); 1310 if (ArrayUtils.isNotEmpty(tagArray)) 1311 { 1312 XMLUtils.startElement(contentHandler, "tags"); 1313 for (String tagName : tagArray) 1314 { 1315 if (StringUtils.isNotEmpty(tagName)) 1316 { 1317 boolean found = false; 1318 Set<String> extensionsIds = _tagExtPt.getExtensionsIds(); 1319 1320 for (String id : extensionsIds) 1321 { 1322 if (found) 1323 { 1324 continue; 1325 } 1326 1327 I18nizableText label = null; 1328 Map<String, CMSTag> tags = null; 1329 1330 TagProvider<CMSTag> tagProvider = _tagExtPt.getExtension(id); 1331 if (tagName.startsWith("provider_") && id.equals(tagName.substring("provider_".length()))) 1332 { 1333 found = true; 1334 1335 label = tagProvider.getLabel(); 1336 1337 tags = tagProvider.getTags(contextParameters); 1338 } 1339 else if (tagProvider.hasTag(tagName, contextParameters)) 1340 { 1341 found = true; 1342 1343 CMSTag tag = tagProvider.getTag(tagName, contextParameters); 1344 label = tag.getTitle(); 1345 tags = tag.getTags(); 1346 } 1347 1348 if (found) 1349 { 1350 AttributesImpl attr = new AttributesImpl(); 1351 attr.addCDATAAttribute("id", tagName); 1352 XMLUtils.startElement(contentHandler, "tag", attr); 1353 1354 if (label != null) 1355 { 1356 label.toSAX(contentHandler, "title"); 1357 } 1358 1359 if (tags != null) 1360 { 1361 for (CMSTag child : tags.values()) 1362 { 1363 if (child.getTarget().getName().equals("CONTENT")) 1364 { 1365 _saxTag(child, true); 1366 } 1367 } 1368 } 1369 1370 XMLUtils.endElement(contentHandler, "tag"); 1371 } 1372 } 1373 } 1374 } 1375 XMLUtils.endElement(contentHandler, "tags"); 1376 } 1377 } 1378 1379 private void _saxTag(Tag tag, boolean recursive) throws SAXException 1380 { 1381 AttributesImpl attr = new AttributesImpl(); 1382 attr.addCDATAAttribute("id", tag.getName()); 1383 XMLUtils.startElement(contentHandler, "tag", attr); 1384 tag.getTitle().toSAX(contentHandler, "title"); 1385 1386 if (recursive) 1387 { 1388 for (Tag child : tag.getTags().values()) 1389 { 1390 _saxTag(child, true); 1391 } 1392 } 1393 1394 XMLUtils.endElement(contentHandler, "tag"); 1395 } 1396 1397 private void _saxSitemapCriteria() throws SAXException 1398 { 1399 @SuppressWarnings("unchecked") 1400 Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); 1401 if (parentContext == null) 1402 { 1403 parentContext = Collections.emptyMap(); 1404 } 1405 1406 String[] pages = (String[]) parentContext.get("search-by-pages"); 1407 if (ArrayUtils.isNotEmpty(pages)) 1408 { 1409 XMLUtils.startElement(contentHandler, "pages"); 1410 1411 for (String pageID : pages) 1412 { 1413 if (StringUtils.isNotEmpty(pageID)) 1414 { 1415 Page page = _resolver.resolveById(pageID); 1416 AttributesImpl attr = new AttributesImpl(); 1417 attr.addCDATAAttribute("id", pageID); 1418 attr.addCDATAAttribute("path", page.getSitemap().getName() + "/" + page.getPathInSitemap()); 1419 attr.addCDATAAttribute("title", page.getTitle()); 1420 attr.addCDATAAttribute("long-title", page.getLongTitle()); 1421 XMLUtils.createElement(contentHandler, "page", attr); 1422 } 1423 } 1424 1425 XMLUtils.endElement(contentHandler, "pages"); 1426 } 1427 } 1428 1429 /** 1430 * Get the content type's filter value 1431 * 1432 * @param request The request 1433 * @return the content type's filter value 1434 */ 1435 protected String getContentTypeFilterValue(Request request) 1436 { 1437 Enumeration<String> paramNames = request.getParameterNames(); 1438 while (paramNames.hasMoreElements()) 1439 { 1440 String paramName = paramNames.nextElement(); 1441 if (paramName.startsWith("ctype-filter-")) 1442 { 1443 return paramName.substring("ctype-filter-".length()); 1444 } 1445 } 1446 1447 if (request.getParameter("current-ctype-filter") != null) 1448 { 1449 return request.getParameter("current-ctype-filter"); 1450 } 1451 1452 return null; 1453 } 1454 1455 /** 1456 * Add the text field query 1457 * @param queries the queries 1458 * @param language the language 1459 * @param request the request 1460 */ 1461 protected void addTextFieldQuery(Collection<Query> queries, String language, Request request) 1462 { 1463 String text = request.getParameter("textfield"); 1464 1465 if (StringUtils.isNotBlank(text)) 1466 { 1467 String trimText = text.trim(); 1468 String escapedText = _escapeQueryCharsButNotQuotes(trimText); 1469 1470 Query textFieldQuery = new OrQuery( 1471 new FullTextQuery(escapedText, language, Operator.SEARCH, true), 1472 new StringQuery(Content.ATTRIBUTE_TITLE, Operator.SEARCH, escapedText, language, true)); 1473 queries.add(textFieldQuery); 1474 } 1475 } 1476 1477 /** 1478 * Get the queries for content's attachments and resources 1479 * @param subQuery the query for attachments and resources 1480 * @return the join query for content's attachments and resources 1481 */ 1482 protected List<Query> getContentResourcesOrAttachmentQueries(Query subQuery) 1483 { 1484 if (subQuery instanceof MatchAllQuery) 1485 { 1486 return Collections.emptyList(); 1487 } 1488 1489 List<Query> queries = new ArrayList<>(); 1490 // join query on outgoing resources 1491 queries.add(new JoinQuery(subQuery, CONTENT_OUTGOING_REFEERENCES_RESOURCE_IDS)); 1492 // join query on content's attachments 1493 queries.add(() -> "{!join from=" + RESOURCE_ANCESTOR_IDS + " to=" + CONTENT_OUTGOING_REFEERENCES_RESOURCE_IDS + " v=\"" + _buildSubQuery(subQuery) + "\"}"); 1494 return queries; 1495 } 1496 1497 /** 1498 * Get the queries for page's attachments and resources 1499 * @param subQuery the query for attachments and resources 1500 * @return the join query for content's attachments and resources 1501 */ 1502 protected List<Query> getPageResourcesOrAttachmentQueries(Query subQuery) 1503 { 1504 List<Query> queries = new ArrayList<>(); 1505 // join query on outgoing resources 1506 queries.add(new JoinQuery(subQuery, PAGE_OUTGOING_REFEERENCES_RESOURCE_IDS)); 1507 // join query on page's attachments or explorer folder service 1508 queries.add(() -> "{!join from=" + RESOURCE_ANCESTOR_IDS + " to=" + PAGE_OUTGOING_REFEERENCES_RESOURCE_IDS + " v=\"" + _buildSubQuery(subQuery) + "\"}"); 1509 return queries; 1510 } 1511 1512 private String _buildSubQuery(Query subQuery) throws QuerySyntaxException 1513 { 1514 return ClientUtils.escapeQueryChars(subQuery.build()); 1515 } 1516 1517 /* 1518 * Do not escape double quotes for exact searching 1519 */ 1520 private String _escapeQueryCharsButNotQuotes(String text) 1521 { 1522 StringBuilder sb = new StringBuilder(); 1523 String[] parts = StringUtils.splitPreserveAllTokens(text, '"'); 1524 for (int i = 0; i < parts.length; i++) 1525 { 1526 if (i != 0) 1527 { 1528 sb.append("\""); 1529 } 1530 String part = parts[i]; 1531 sb.append(ClientUtils.escapeQueryChars(part)); 1532 } 1533 1534 return sb.toString(); 1535 } 1536 1537 /** 1538 * Add query for each attribute 1539 * 1540 * @param queries The list of query 1541 * @param language The lang 1542 * @param request The request 1543 * @throws IllegalArgumentException If an error occurred 1544 */ 1545 protected void addAttributeQuery(Collection<Query> queries, String language, Request request) throws IllegalArgumentException 1546 { 1547 ZoneItem zoneItem = getZoneItem(request); 1548 Set<String> facetFields = getFacets(request).keySet(); 1549 if (zoneItem != null && zoneItem.getServiceParameters().hasValue("search-by-metadata")) 1550 { 1551 String[] attributePaths = zoneItem.getServiceParameters().getValue("search-by-metadata"); 1552 for (String attributePath : attributePaths) 1553 { 1554 if (facetFields.contains(attributePath)) 1555 { 1556 // will be handled in org.ametys.web.frontoffice.SearchGenerator.getFacetValues(Request, Collection<String>, String) 1557 continue; 1558 } 1559 String value = request.getParameter("metadata-" + attributePath.replaceAll("/", ".")); 1560 1561 if (StringUtils.isNotBlank(value)) 1562 { 1563 Collection<String> contentTypes = getContentTypes(request); 1564 ModelItem modelItem = _getModelItemFromContentTypes(contentTypes, attributePath); 1565 queries.add(_getStringAttributeQuery(language, attributePath, value, modelItem)); 1566 } 1567 } 1568 } 1569 } 1570 1571 /** 1572 * Get query from string attribute 1573 * @param language the language 1574 * @param attributePath the attribute path 1575 * @param value the value 1576 * @param modelItem the model item 1577 * @return the query 1578 */ 1579 protected Query _getStringAttributeQuery(String language, String attributePath, String value, ModelItem modelItem) 1580 { 1581 Query query; 1582 if (modelItem != null && modelItem instanceof ElementDefinition 1583 && (((ElementDefinition) modelItem).getEnumerator() != null || modelItem.getType().getId().equals(ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID))) 1584 { 1585 // Exact search 1586 query = new StringQuery(attributePath, Operator.EQ, value, language); 1587 } 1588 else if (value.startsWith("\"") && value.endsWith("\"")) 1589 { 1590 String escapedText = ClientUtils.escapeQueryChars(value.substring(1, value.length() - 1)); // the StringQuery with EQ operator will add quotes characters anyway, so remove them 1591 query = new StringQuery(attributePath, Operator.EQ, escapedText, language, true); 1592 } 1593 else 1594 { 1595 String escapedText = _escapedTextForSearch(value); 1596 Operator op = escapedText.startsWith("*") || escapedText.endsWith("*") ? Operator.LIKE : Operator.SEARCH; 1597 boolean doNotUseLanguage = op == Operator.LIKE && modelItem != null && !(modelItem.getType().getId().equals(ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID)); 1598 query = new StringQuery(attributePath, op, escapedText, doNotUseLanguage ? null : language, true); 1599 } 1600 return query; 1601 } 1602 1603 private String _escapedTextForSearch(String value) 1604 { 1605 // Allow wildcards at the beginning and at the end 1606 // Ex.: 1607 // zert will stay zert to not match azerty 1608 // *zert will stay *zert to not match azerty and match azert 1609 // zert* will stay zert* to not match azerty and match zerty 1610 // *zert* will stay *zert* to match azerty 1611 // where zert is escaped 1612 1613 String valueToEscape = value; 1614 String begin = ""; 1615 String end = ""; 1616 boolean startsWithStar = value.startsWith("*"); 1617 boolean endsWithStar = value.endsWith("*"); 1618 if (startsWithStar) 1619 { 1620 valueToEscape = valueToEscape.substring(1); 1621 begin = "*"; 1622 } 1623 if (endsWithStar) 1624 { 1625 valueToEscape = valueToEscape.substring(0, valueToEscape.length() - 1); 1626 end = "*"; 1627 } 1628 1629 // escape special characters 1630 String escapedValue = ClientUtils.escapeQueryChars(valueToEscape).toLowerCase(); 1631 String escapedText = begin + escapedValue + end; 1632 return escapedText; 1633 } 1634 1635 /** 1636 * Add content query for "all words" 1637 * @param queries The queries 1638 * @param language The current language 1639 * @param request the request 1640 */ 1641 protected void addAllWordsTextFieldQuery(Collection<Query> queries, String language, Request request) 1642 { 1643 // TODO All words? OrQuery of AndQuery instead? 1644 String words = request.getParameter("all-words"); 1645 1646 if (StringUtils.isNotBlank(words)) 1647 { 1648 StringBuilder allWords = new StringBuilder(); 1649 for (String word : StringUtils.split(words)) 1650 { 1651 String escapedWord = ClientUtils.escapeQueryChars(word); 1652 if (allWords.length() > 0) 1653 { 1654 allWords.append(' '); 1655 } 1656 allWords.append('+').append(escapedWord); 1657 } 1658 1659 // queries.add(new FullTextQuery(allWords.toString(), language, 1660 // Operator.SEARCH)); 1661 1662 queries.add(new FullTextQuery(allWords.toString(), language, Operator.SEARCH, true)); 1663 } 1664 } 1665 1666 /** 1667 * Add content query for exact wording or phrase 1668 * @param queries The queries 1669 * @param language The current language 1670 * @param request the request 1671 */ 1672 protected void addExactWordingTextFieldQuery(Collection<Query> queries, String language, Request request) 1673 { 1674 String exact = request.getParameter("exact-wording"); 1675 1676 if (StringUtils.isNotBlank(exact)) 1677 { 1678 queries.add(new FullTextQuery(exact, language, Operator.EQ)); 1679 } 1680 } 1681 1682 /** 1683 * Add content query for "none of these words" 1684 * @param queries The queries 1685 * @param language The current language 1686 * @param request the request 1687 */ 1688 protected void addNoWordsTextFieldQuery(Collection<Query> queries, String language, Request request) 1689 { 1690 String noWords = request.getParameter("no-words"); 1691 1692 if (StringUtils.isNotBlank(noWords)) 1693 { 1694 StringBuilder allWords = new StringBuilder(); 1695 for (String word : StringUtils.split(noWords)) 1696 { 1697 String escapedWord = ClientUtils.escapeQueryChars(word); 1698 if (allWords.length() > 0) 1699 { 1700 allWords.append(' '); 1701 } 1702 allWords.append('+').append(escapedWord); 1703 } 1704 1705 // queries.add(new NotQuery(new FullTextQuery(allWords.toString(), 1706 // language, Operator.SEARCH))); 1707 1708 queries.add(new NotQuery(new FullTextQuery(allWords.toString(), language, Operator.SEARCH, true))); 1709 } 1710 } 1711 1712 /** 1713 * Add the content type query 1714 * @param queries The queries 1715 * @param request The request 1716 */ 1717 protected void addContentTypeQuery(Collection<Query> queries, Request request) 1718 { 1719 Collection<String> cTypes = new ArrayList<>(getContentTypes(request)); 1720 1721 // Resource is handled in the "document types" filter. 1722 cTypes.remove("resource"); 1723 1724 if (!cTypes.isEmpty()) 1725 { 1726 Query notAPageQuery = new NotQuery(new DocumentTypeQuery(TYPE_PAGE)); 1727 Query pageWithContentOfTypesQuery = new PageContentQuery(new ContentTypeQuery(cTypes)); 1728 // If document is not a page, it will match this fq (it will be filtered if necessary in the "document types" filter, see #getDocumentTypesQuery()) 1729 // If document is a page, then one of its contents must match one of the given content types 1730 queries.add(new OrQuery(notAPageQuery, pageWithContentOfTypesQuery)); 1731 } 1732 } 1733 1734 /** 1735 * Add the tags query 1736 * @param queries The queries 1737 * @param request The request 1738 */ 1739 protected void addTagsQuery(Collection<Query> queries, Request request) 1740 { 1741 String size = request.getParameter("tags-size"); 1742 if (!StringUtils.isEmpty(size)) 1743 { 1744 @SuppressWarnings("unchecked") 1745 Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); 1746 if (parentContext == null) 1747 { 1748 parentContext = Collections.emptyMap(); 1749 } 1750 1751 boolean isStrictSearch = parameters.getParameterAsBoolean("strict-search-on-tags", true); 1752 1753 int nbCat = Integer.parseInt(size); 1754 for (int i = 1; i < nbCat + 1; i++) 1755 { 1756 String[] tags = request.getParameterValues("tags-" + i); 1757 if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals(""))) 1758 { 1759 List<Query> tagQueries = new ArrayList<>(); 1760 1761 for (String tag : tags) 1762 { 1763 String[] values = StringUtils.split(tag, ','); 1764 1765 tagQueries.add(new TagQuery(Operator.EQ, !isStrictSearch, values)); 1766 } 1767 1768 queries.add(new PageContentQuery(new OrQuery(tagQueries))); 1769 } 1770 } 1771 } 1772 } 1773 1774 /** 1775 * Add the pages query 1776 * @param queries The queries 1777 * @param request The request 1778 */ 1779 protected void addPagesQuery(Collection<Query> queries, Request request) 1780 { 1781 List<Query> pageQueries = new ArrayList<>(); 1782 1783 String[] pages = request.getParameterValues("pages"); 1784 if (pages != null && pages.length > 0 && !(pages.length == 1 && pages[0].equals(""))) 1785 { 1786 for (String pageIds : pages) 1787 { 1788 for (String pageId : StringUtils.split(pageIds, ",")) 1789 { 1790 pageQueries.add(new PageQuery(pageId, true)); 1791 } 1792 } 1793 } 1794 1795 queries.add(new OrQuery(pageQueries)); 1796 } 1797 1798 /** 1799 * Add the query on start and end dates 1800 * @param queries The queries 1801 * @param request The request 1802 */ 1803 protected void addDateQuery(Collection<Query> queries, Request request) 1804 { 1805 String startDateId = parameters.getParameter("startDate", ""); 1806 String endDateId = parameters.getParameter("endDate", ""); 1807 1808 String startDateStr = request.getParameter("startDate"); 1809 String endDateStr = request.getParameter("endDate"); 1810 1811 // We check if startDateStr < endDateStr. If not, we swap. 1812 if (StringUtils.isNotBlank(endDateStr) && StringUtils.isNotBlank(startDateStr) && startDateStr.compareTo(endDateStr) > 0) 1813 { 1814 String dateAux = startDateStr; 1815 startDateStr = endDateStr; 1816 endDateStr = dateAux; 1817 } 1818 1819 String startPropId = startDateId.equals("last-modified") ? SolrFieldNames.LAST_MODIFIED : startDateId; 1820 String endPropId = endDateId.equals("last-modified") ? SolrFieldNames.LAST_MODIFIED : endDateId; 1821 1822 if (StringUtils.isNotBlank(endDateStr)) 1823 { 1824 LocalDate endDate = _toDate(endDateStr); 1825 // Join for returning page documents 1826 queries.add(new PageContentQuery(new DateQuery(startPropId, Operator.LE, endDate))); 1827 } 1828 1829 if (StringUtils.isNotBlank(startDateStr)) 1830 { 1831 LocalDate startDate = _toDate(startDateStr); 1832 1833 if (startDate != null) 1834 { 1835 // Two cases are possible : 1836 // An event could have an end date (query 1) 1837 // If not, the start date is also the end date (query 2) 1838 List<Query> endDateQueries = new ArrayList<>(); 1839 1840 if (StringUtils.isNotBlank(endPropId)) 1841 { 1842 endDateQueries.add(new DateQuery(endPropId, Operator.GE, startDate)); 1843 } 1844 endDateQueries.add(new DateQuery(startPropId, Operator.GE, startDate)); 1845 1846 // Join for returning page documents 1847 queries.add(new PageContentQuery(new OrQuery(endDateQueries))); 1848 } 1849 } 1850 } 1851 1852 private LocalDate _toDate(String dateStr) 1853 { 1854 try 1855 { 1856 return LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE); 1857 } 1858 catch (DateTimeParseException e) 1859 { 1860 getLogger().error("Invalid date format " + dateStr, e); 1861 } 1862 return null; 1863 } 1864 1865 private List<QueryAdapterFOSearch> _getSortedListQueryAdapter() 1866 { 1867 return _queryAdapterFOSearchEP.getExtensionsIds() 1868 .stream() 1869 .map(_queryAdapterFOSearchEP::getExtension) 1870 .collect(Collectors.toList()); 1871 } 1872 1873 private ModelItem _getModelItemFromContentTypes(Collection<String> contentTypeIds, String attributePath) 1874 { 1875 if (contentTypeIds.isEmpty()) 1876 { 1877 return null; 1878 } 1879 1880 try 1881 { 1882 String[] contentTypeIdsAsArray = contentTypeIds.toArray(new String[contentTypeIds.size()]); 1883 return _contentTypesHelper.getModelItem(attributePath, contentTypeIdsAsArray, new String[0]); 1884 } 1885 catch (UndefinedItemPathException e) 1886 { 1887 // No such attribute exists, retrieve null 1888 return null; 1889 } 1890 } 1891 1892 static class ContentTypeSearchField implements SearchField 1893 { 1894 @Override 1895 public String getSortField() 1896 { 1897 return PAGE_CONTENT_TYPES; 1898 } 1899 1900 @Override 1901 public String getName() 1902 { 1903 return PAGE_CONTENT_TYPES; 1904 } 1905 1906 @Override 1907 public String getFacetField() 1908 { 1909 return PAGE_CONTENT_TYPES + "_s_dv"; 1910 } 1911 1912 @Override 1913 public boolean isJoined() 1914 { 1915 return false; 1916 } 1917 1918 @Override 1919 public List<String> getJoinedPaths() 1920 { 1921 return Collections.emptyList(); 1922 } 1923 } 1924 1925}