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