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 // FIXME Remove this hack when CMS-11599 will be fixed 474 // The good code probably should be the old one => Query finalQuery = new OrQuery(finalPageQuery, finalContentQuery); 475 try 476 { 477 Query finalQuery = null; 478 if (StringUtils.isBlank(finalContentQuery.build())) 479 { 480 finalQuery = finalPageQuery; 481 } 482 else if (StringUtils.isBlank(finalPageQuery.build())) 483 { 484 finalQuery = finalContentQuery; 485 } 486 else 487 { 488 finalQuery = new OrQuery(finalPageQuery, finalContentQuery); 489 } 490 491 for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter()) 492 { 493 finalQuery = queryAdapter.modifyQuery(finalQuery, request, siteNames, language); 494 } 495 496 return finalQuery; 497 } 498 catch (QuerySyntaxException e) 499 { 500 throw new IllegalArgumentException("An error occurred building query", e); 501 } 502 503 } 504 505 /** 506 * Get the queries on wording (keywords, no words, exact wording, no words) 507 * @param request the request 508 * @param siteNames the site names 509 * @param language the language 510 * @return the queries on wording 511 */ 512 protected List<Query> getWordingQueries(Request request, Collection<String> siteNames, String language) 513 { 514 List<Query> queries = new ArrayList<>(); 515 516 addTextFieldQuery(queries, language, request); 517 addAllWordsTextFieldQuery(queries, language, request); 518 addExactWordingTextFieldQuery(queries, language, request); 519 addNoWordsTextFieldQuery(queries, language, request); 520 521 return queries; 522 } 523 524 /** 525 * Get the queries to be apply on joined contents ONLY 526 * @param request the request 527 * @param siteNames the site names 528 * @param language the language 529 * @return the queries for contents only 530 */ 531 protected List<Query> getContentQueries(Request request, Collection<String> siteNames, String language) 532 { 533 List<Query> queries = new ArrayList<>(); 534 queries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT)); 535 addAttributeQuery(queries, language, request); 536 return queries; 537 } 538 539 /** 540 * Get the queries to be apply on pages ONLY 541 * @param request the request 542 * @param siteNames the site names 543 * @param language the language 544 * @return the queries for pages only 545 */ 546 protected List<Query> getPageQueries(Request request, Collection<String> siteNames, String language) 547 { 548 return new ArrayList<>(); 549 } 550 551 @Override 552 protected Collection<Query> getFilterQueries(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException 553 { 554 List<Query> queries = new ArrayList<>(); 555 556 Query siteQuery = new SiteQuery(siteNames); 557 Query sitemapQuery = new SitemapQuery(language); 558 for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter()) 559 { 560 siteQuery = queryAdapter.modifySiteQueryFilter(siteQuery, request, siteNames, language); 561 sitemapQuery = queryAdapter.modifySitemapQueryFilter(sitemapQuery, request, siteNames, language); 562 } 563 564 queries.add(siteQuery); 565 queries.add(sitemapQuery); 566 567 addContentTypeQuery(queries, request); 568 addTagsQuery(queries, request); 569 addPagesQuery(queries, request); 570 addDateQuery(queries, request); 571 572 return queries; 573 } 574 575 @Override 576 protected Map<String, List<String>> getFacetValues(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException 577 { 578 Map<String, List<String>> facetValues = new HashMap<>(); 579 580 String currentCType = getContentTypeFilterValue(request); 581 if (currentCType != null) 582 { 583 facetValues.put(SolrWebFieldNames.PAGE_CONTENT_TYPES, new ArrayList<>()); 584 if (!"resource".equals(currentCType)) 585 { 586 facetValues.get(SolrWebFieldNames.PAGE_CONTENT_TYPES).add(currentCType); 587 } 588 } 589 else if (getContentTypeSearch(request).equals(ContentTypeSearch.FILTER) || getContentTypeSearch(request).equals(ContentTypeSearch.CHECKBOX_FILTER)) 590 { 591 // Get the first content types as facet value 592 facetValues.put(SolrWebFieldNames.PAGE_CONTENT_TYPES, new ArrayList<>()); 593 String firstContentType = getContentTypes(request).iterator().next(); 594 if (!"resource".equals(firstContentType)) 595 { 596 facetValues.get(SolrWebFieldNames.PAGE_CONTENT_TYPES).add(firstContentType); 597 } 598 } 599 600 Map<String, FacetField> facets = getFacets(request); 601 for (String fieldName : facets.keySet()) 602 { 603 String[] parameterValues = request.getParameterValues("metadata-" + fieldName); 604 if (parameterValues != null && parameterValues.length > 0 && StringUtils.isNotEmpty(parameterValues[0])) 605 { 606 facetValues.put(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + fieldName, new ArrayList<>()); 607 List<String> preparedParameterValues = Stream.of(parameterValues) 608 .map(StringEscapeUtils::unescapeXml) 609 .map(ClientUtils::escapeQueryChars) 610 .collect(Collectors.toList()); 611 facetValues.get(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + fieldName).addAll(preparedParameterValues); 612 } 613 } 614 615 return facetValues; 616 } 617 618 @Override 619 protected Collection<String> getQueryFacetValues(Request request) 620 { 621 List<String> queryFacetValues = new ArrayList<>(); 622 623 String currentCType = getContentTypeFilterValue(request); 624 if (currentCType != null) 625 { 626 if ("resource".equals(currentCType)) 627 { 628 queryFacetValues.add(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME); 629 } 630 } 631 else if (getContentTypeSearch(request).equals(ContentTypeSearch.FILTER) || getContentTypeSearch(request).equals(ContentTypeSearch.CHECKBOX_FILTER)) 632 { 633 // Get the first content types as facet value 634 String firstContentType = getContentTypes(request).iterator().next(); 635 if ("resource".equals(firstContentType)) 636 { 637 queryFacetValues.add(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME); 638 } 639 } 640 641 return queryFacetValues; 642 } 643 644 @Override 645 protected Map<String, FacetField> getFacets(Request request) throws IllegalArgumentException 646 { 647 ZoneItem zoneItem = getZoneItem(request); 648 String facetCacheAttrName = __FACETS_CACHE + "$" + Optional.ofNullable(zoneItem).map(ZoneItem::getId).orElse("null"); 649 @SuppressWarnings("unchecked") 650 Map<String, FacetField> cache = (Map<String, FacetField>) request.getAttribute(facetCacheAttrName); 651 if (cache != null) 652 { 653 return cache; 654 } 655 656 Map<String, FacetField> facets = new HashMap<>(); 657 658 // Facet for content types 659 ContentTypeSearch contentTypeSearch = getContentTypeSearch(request); 660 if (contentTypeSearch.equals(ContentTypeSearch.FILTER) || contentTypeSearch.equals(ContentTypeSearch.CHECKBOX_FILTER)) 661 { 662 SearchField cTypeField = new ContentTypeSearchField(); 663 664 facets.put(PAGE_CONTENT_TYPES, new ContentTypeFacetField(cTypeField)); 665 } 666 667 if (useFacets()) 668 { 669 Collection<String> contentTypes = getContentTypes(request); 670 671 if (zoneItem != null && zoneItem.getServiceParameters().hasValue("search-by-metadata")) 672 { 673 String[] metadataPaths = zoneItem.getServiceParameters().getValue("search-by-metadata"); 674 for (String metadataPath : metadataPaths) 675 { 676 _addAttributeFacet(facets, contentTypes, metadataPath); 677 } 678 } 679 } 680 681 request.setAttribute(facetCacheAttrName, facets); 682 683 return facets; 684 } 685 686 /** 687 * Add attribute facet to facets map 688 * @param facets the facets map 689 * @param contentTypes the content types 690 * @param attributePath the attribute path 691 */ 692 protected void _addAttributeFacet(Map<String, FacetField> facets, Collection<String> contentTypes, String attributePath) 693 { 694 ModelItem modelItem = _getModelItemFromContentTypes(contentTypes, attributePath); 695 696 if (modelItem != null && modelItem instanceof ElementDefinition) 697 { 698 ElementDefinition elementDefinition = (ElementDefinition) modelItem; 699 if (elementDefinition.getEnumerator() != null || elementDefinition.getType().getId().equals(ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID)) 700 { 701 Optional<SearchField> searchField = _searchHelper.getSearchField(contentTypes, attributePath); 702 if (searchField.isPresent()) 703 { 704 String searchFieldName = searchField.get().getName(); 705 StringSearchField facetField = new StringSearchField(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + searchFieldName); 706 facets.put(searchFieldName, new AttributeFacetField(facetField, modelItem, new AvalonLoggerAdapter(getLogger()))); 707 } 708 } 709 } 710 } 711 712 @Override 713 protected Set<QueryFacet> getQueryFacets(Request request) 714 { 715 Set<QueryFacet> queryFacets = new HashSet<>(); 716 Collection<String> contentTypes = getContentTypes(request); 717 718 // For handling "resource" content type in facets for content types 719 ContentTypeSearch contentTypeSearch = getContentTypeSearch(request); 720 if ((contentTypeSearch.equals(ContentTypeSearch.FILTER) || contentTypeSearch.equals(ContentTypeSearch.CHECKBOX_FILTER)) 721 && contentTypes.contains("resource")) 722 { 723 queryFacets.add(new QueryFacet(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME, 724 PAGE_CONTENT_TYPES, 725 DOCUMENT_TYPE + ":" + TYPE_PAGE_RESOURCE)); 726 } 727 728 for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter()) 729 { 730 queryFacets = queryAdapter.modifyQueryFacets(queryFacets, request); 731 } 732 733 return queryFacets; 734 } 735 736 /** 737 * Get the filter queries for a single fixed content type. 738 * 739 * @param request The request. 740 * @param cType The fixed content type. 741 * @param siteNames the site names. 742 * @param language The language. 743 * @return The filter queries. 744 */ 745 protected Collection<Query> getFixedCTypeFilterQueries(Request request, String cType, Collection<String> siteNames, String language) 746 { 747 List<Query> queries = new ArrayList<>(); 748 749 queries.add(new SiteQuery(siteNames)); 750 queries.add(new SitemapQuery(language)); 751 752 if ("resource".equals(cType)) 753 { 754 queries.add(new DocumentTypeQuery(TYPE_PAGE_RESOURCE)); 755 } 756 else 757 { 758 // Fixed content type query. 759 queries.add(new PageContentQuery(new ContentTypeQuery(cType))); 760 } 761 addTagsQuery(queries, request); 762 addPagesQuery(queries, request); 763 addDateQuery(queries, request); 764 765 return queries; 766 } 767 768 @Override 769 protected Collection<String> getDocumentTypes(Request request) 770 { 771 List<String> documentTypes = new ArrayList<>(); 772 773 Collection<String> cTypes = getContentTypes(request); 774 if (cTypes.size() == 1 && "resource".equals(cTypes.iterator().next())) 775 { 776 documentTypes.add(TYPE_PAGE_RESOURCE); 777 } 778 // An empty collections means "all". 779 else if (cTypes.isEmpty() || cTypes.contains("resource")) 780 { 781 documentTypes.addAll(Arrays.asList(TYPE_PAGE, TYPE_PAGE_RESOURCE)); 782 } 783 else 784 { 785 documentTypes.add(TYPE_PAGE); 786 } 787 788 // Add other documents types (by priority order) 789 for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter()) 790 { 791 queryAdapter.addDocumentType(documentTypes); 792 } 793 794 return documentTypes; 795 } 796 797 @Override 798 protected Collection<String> getFields() 799 { 800 return Collections.emptyList(); 801 } 802 803 @Override 804 protected void saxHits(SearchResults<AmetysObject> results, int start, int maxResults) throws SAXException 805 { 806 SearchResultsIterable<SearchResult<AmetysObject>> resultsIt = results.getResults(); 807 long limit = Math.min(start + maxResults, resultsIt.getSize()); 808 float maxScore = results.getMaxScore(); 809 810 SearchResultsIterator<SearchResult<AmetysObject>> it = resultsIt.iterator(); 811 it.skip(start); 812 for (int i = start; i < limit; i++) 813 { 814 if (it.hasNext()) // this should return true except if there is a inconsistency between repository and Solr index 815 { 816 SearchResult<AmetysObject> searchResult = it.next(); 817 float score = searchResult.getScore(); 818 AmetysObject ametysObject = searchResult.getObject(); 819 if (ametysObject instanceof Page) 820 { 821 saxPageHit(score, maxScore, (Page) ametysObject); 822 } 823 else if (ametysObject instanceof Resource) 824 { 825 saxResourceHit(score, maxScore, (Resource) ametysObject); 826 } 827 } 828 829 } 830 } 831 832 @Override 833 protected Sort getSortField(Request request) 834 { 835 if (request.getParameter("sort-by-title-for-sorting") != null || request.getParameter("sort-by-title") != null) 836 { 837 return new Sort(TITLE_SORT, Order.ASC); 838 } 839 else if (request.getParameter("sort-by-lastValidation") != null) 840 { 841 return new Sort(LastValidationSearchField.NAME, Order.DESC); 842 } 843 else 844 { 845 // Generic sort field (with hardcorded descending order) 846 Enumeration paramNames = request.getParameterNames(); 847 while (paramNames.hasMoreElements()) 848 { 849 String param = (String) paramNames.nextElement(); 850 if (param.startsWith("sort-by-")) 851 { 852 String fieldName = StringUtils.removeStart(param, "sort-by-"); 853 return new Sort(fieldName, Order.ASC); 854 } 855 } 856 } 857 858 return new Sort("score", Order.DESC); 859 } 860 861 @Override 862 protected List<Sort> getPrimarySortFields(Request request) 863 { 864 return new ArrayList<>(); 865 } 866 867 @Override 868 protected void saxFormFields(Request request, String siteName, String lang) throws SAXException 869 { 870 XMLUtils.createElement(contentHandler, "textfield"); 871 872 boolean advancedSearch = parameters.getParameterAsBoolean("advanced-search", true); 873 if (advancedSearch) 874 { 875 XMLUtils.createElement(contentHandler, "all-words"); 876 XMLUtils.createElement(contentHandler, "exact-wording"); 877 XMLUtils.createElement(contentHandler, "no-words"); 878 } 879 880 ContentTypeSearch contentTypeSearch = getContentTypeSearch(request); 881 XMLUtils.createElement(contentHandler, "content-types-choice", contentTypeSearch.toString()); 882 883 _saxContentTypeCriteria(request); 884 _saxAttributeCriteria(request, lang); 885 _saxTagsCriteria(siteName); 886 _saxSitemapCriteria(); 887 888 boolean multisite = parameters.getParameterAsBoolean("search-multisite", false); 889 if (multisite) 890 { 891 XMLUtils.createElement(contentHandler, "multisite"); 892 893 XMLUtils.startElement(contentHandler, "sites"); 894 Collection<String> allSites = _siteManager.getSiteNames(); 895 for (String name : allSites) 896 { 897 Site site = _siteManager.getSite(name); 898 if (!_isPrivate(site)) 899 { 900 AttributesImpl attr = new AttributesImpl(); 901 attr.addCDATAAttribute("name", name); 902 if (name.equals(siteName)) 903 { 904 attr.addCDATAAttribute("current", "true"); 905 } 906 XMLUtils.createElement(contentHandler, "site", attr, StringUtils.defaultString(site.getTitle())); 907 } 908 } 909 XMLUtils.endElement(contentHandler, "sites"); 910 } 911 912 if (StringUtils.isNotBlank(parameters.getParameter("startDate", ""))) 913 { 914 XMLUtils.createElement(contentHandler, "dates", "true"); 915 } 916 } 917 918 private boolean _isPrivate(Site site) 919 { 920 String type = site.getType(); 921 return _siteTypeEP.getExtension(type).isPrivateType(); 922 } 923 924 private void _saxAttributeCriteria(Request request, String language) throws SAXException 925 { 926 ZoneItem zoneItem = getZoneItem(request); 927 if (zoneItem != null && zoneItem.getServiceParameters().hasValue("search-by-metadata")) 928 { 929 String[] attributePaths = zoneItem.getServiceParameters().getValue("search-by-metadata"); 930 if (attributePaths.length > 0) 931 { 932 Collection<String> cTypes = new ArrayList<>(getContentTypes(request)); 933 934 for (String attributePath : attributePaths) 935 { 936 ModelItem modelItem = _getModelItemFromContentTypes(cTypes, attributePath); 937 saxAttributeDefinition(modelItem, attributePath, language); 938 } 939 } 940 } 941 } 942 943 /** 944 * Generates SAX events for attribute definition 945 * 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 generating SAX events 950 */ 951 protected void saxAttributeDefinition(ModelItem modelItem, String attributePath, String language) throws SAXException 952 { 953 AttributesImpl attrs = new AttributesImpl(); 954 attrs.addCDATAAttribute("name", attributePath); 955 956 XMLUtils.startElement(contentHandler, "metadata", attrs); 957 if (modelItem != null) 958 { 959 modelItem.getLabel().toSAX(contentHandler, "label"); 960 } 961 else 962 { 963 XMLUtils.startElement(contentHandler, "label"); 964 XMLUtils.data(contentHandler, attributePath); 965 XMLUtils.endElement(contentHandler, "label"); 966 } 967 968 saxEnumeratorValueForAttribute(modelItem, attributePath, language); 969 XMLUtils.endElement(contentHandler, "metadata"); 970 } 971 972 /** 973 * Sax enumeration value for enum or a content attribute 974 * @param modelItem The attribute definition. 975 * @param attributePath The attribute path 976 * @param language The current language 977 * @throws SAXException If an error occurred while saxing 978 */ 979 protected void saxEnumeratorValueForAttribute(ModelItem modelItem, String attributePath, String language) throws SAXException 980 { 981 if (modelItem != null && modelItem instanceof ElementDefinition) 982 { 983 ElementDefinition attributeDefinition = (ElementDefinition) modelItem; 984 if (attributeDefinition.getEnumerator() != null) 985 { 986 XMLUtils.startElement(contentHandler, "enumeration"); 987 try 988 { 989 Map<Object, I18nizableText> entries = attributeDefinition.getEnumerator().getTypedEntries(); 990 for (Object key : entries.keySet()) 991 { 992 AttributesImpl attrItem = new AttributesImpl(); 993 attrItem.addCDATAAttribute("value", (String) key); 994 XMLUtils.startElement(contentHandler, "item", attrItem); 995 entries.get(key).toSAX(contentHandler, "label"); 996 XMLUtils.endElement(contentHandler, "item"); 997 } 998 } 999 catch (Exception e) 1000 { 1001 getLogger().error("An error occurred getting enumerator items for attribute : " + attributePath, e); 1002 } 1003 XMLUtils.endElement(contentHandler, "enumeration"); 1004 } 1005 else if (attributeDefinition instanceof ContentAttributeDefinition) 1006 { 1007 ContentAttributeDefinition contentAttributeDefinition = (ContentAttributeDefinition) attributeDefinition; 1008 XMLUtils.startElement(contentHandler, "enumeration"); 1009 Map<String, String> values = getContentValues(contentAttributeDefinition.getContentTypeId(), language); 1010 for (Entry<String, String> entry : values.entrySet()) 1011 { 1012 AttributesImpl attrItem = new AttributesImpl(); 1013 attrItem.addCDATAAttribute("value", entry.getKey()); 1014 XMLUtils.startElement(contentHandler, "item", attrItem); 1015 XMLUtils.createElement(contentHandler, "label", entry.getValue()); 1016 XMLUtils.endElement(contentHandler, "item"); 1017 } 1018 XMLUtils.endElement(contentHandler, "enumeration"); 1019 } 1020 } 1021 } 1022 1023 /** 1024 * Get values for contents enumeration 1025 * @param cTypeId The id of content type 1026 * @param language The current language 1027 * @return The contents 1028 */ 1029 protected Map<String, String> getContentValues(String cTypeId, String language) 1030 { 1031 try 1032 { 1033 boolean multilingual = _cTypeExtPt.getExtension(cTypeId).isMultilingual(); 1034 Expression expr = new AndExpression( 1035 _getContentTypeExpression(cTypeId), 1036 multilingual ? null : new LanguageExpression(org.ametys.plugins.repository.query.expression.Expression.Operator.EQ, language)); 1037 AmetysObjectIterable<Content> contents = _resolver.query(QueryHelper.getXPathQuery(null, RepositoryConstants.NAMESPACE_PREFIX + ":content", expr)); 1038 1039 return contents.stream() 1040 .collect(Collectors.toMap(Content::getId, c -> c.getTitle(new Locale(language)))) 1041 .entrySet() 1042 .stream() 1043 .sorted(Map.Entry.comparingByValue()) // sort by title 1044 .collect(LambdaUtils.Collectors.toLinkedHashMap(Map.Entry::getKey, Map.Entry::getValue)); 1045 } 1046 catch (Exception e) 1047 { 1048 getLogger().error("Failed to get content enumeration for content type " + cTypeId, e); 1049 return MapUtils.EMPTY_MAP; 1050 } 1051 } 1052 1053 private Expression _getContentTypeExpression(String parentCTypeId) 1054 { 1055 Stream<String> subCTypesIds = _cTypeExtPt.getSubTypes(parentCTypeId).stream(); 1056 Expression[] exprs = Stream.concat(Stream.of(parentCTypeId), subCTypesIds) 1057 .map(cTypeId -> new ContentTypeExpression(org.ametys.plugins.repository.query.expression.Expression.Operator.EQ, cTypeId)) 1058 .toArray(Expression[]::new); 1059 return new OrExpression(exprs); 1060 } 1061 1062 @Override 1063 protected void saxFormValues(Request request, int start, int offset) throws SAXException 1064 { 1065 _saxTextField(request); 1066 _saxMetadataValues(request); 1067 _saxAllWords(request); 1068 _saxExactWording(request); 1069 _saxNoWords(request); 1070 _saxContentType(request); 1071 _saxTags(request); 1072 _saxPages(request); 1073 _saxMultisite(request); 1074 _saxDates(request); 1075 } 1076 1077 private void _saxDates(Request request) throws SAXException 1078 { 1079 String startDate = request.getParameter("startDate"); 1080 String endDate = request.getParameter("endDate"); 1081 1082 if (StringUtils.isNotBlank(startDate) && StringUtils.isNotBlank(endDate) && startDate.compareTo(endDate) > 0) 1083 { 1084 String tmp = startDate; 1085 startDate = endDate; 1086 endDate = tmp; 1087 } 1088 1089 if (StringUtils.isNotBlank(startDate)) 1090 { 1091 XMLUtils.createElement(contentHandler, "startDate", startDate); 1092 } 1093 if (StringUtils.isNotBlank(endDate)) 1094 { 1095 XMLUtils.createElement(contentHandler, "endDate", endDate); 1096 } 1097 } 1098 1099 private void _saxTextField(Request request) throws SAXException 1100 { 1101 String textfield = request.getParameter("textfield"); 1102 XMLUtils.createElement(contentHandler, "textfield", textfield != null ? textfield : ""); 1103 } 1104 1105 private void _saxMetadataValues(Request request) throws SAXException 1106 { 1107 ZoneItem zoneItem = getZoneItem(request); 1108 if (zoneItem != null && zoneItem.getServiceParameters().hasValue("search-by-metadata")) 1109 { 1110 String[] metadataPaths = zoneItem.getServiceParameters().getValue("search-by-metadata"); 1111 if (metadataPaths.length > 0) 1112 { 1113 XMLUtils.startElement(contentHandler, "metadata"); 1114 for (String metadataPath : metadataPaths) 1115 { 1116 String[] values = request.getParameterValues("metadata-" + metadataPath.replaceAll("/", ".")); 1117 if (values != null) 1118 { 1119 for (String value : values) 1120 { 1121 AttributesImpl attrs = new AttributesImpl(); 1122 attrs.addCDATAAttribute("name", metadataPath); 1123 XMLUtils.createElement(contentHandler, "metadata", attrs, value != null ? value : ""); 1124 } 1125 } 1126 } 1127 XMLUtils.endElement(contentHandler, "metadata"); 1128 } 1129 } 1130 } 1131 1132 private void _saxAllWords(Request request) throws SAXException 1133 { 1134 String textfield = request.getParameter("all-words"); 1135 XMLUtils.createElement(contentHandler, "all-words", textfield != null ? textfield : ""); 1136 } 1137 1138 private void _saxExactWording(Request request) throws SAXException 1139 { 1140 String textfield = request.getParameter("exact-wording"); 1141 XMLUtils.createElement(contentHandler, "exact-wording", textfield != null ? textfield : ""); 1142 } 1143 1144 private void _saxNoWords(Request request) throws SAXException 1145 { 1146 String textfield = request.getParameter("no-words"); 1147 XMLUtils.createElement(contentHandler, "no-words", textfield != null ? textfield : ""); 1148 } 1149 1150 private void _saxContentType(Request request) throws SAXException 1151 { 1152 String[] cTypes = request.getParameterValues("content-types"); 1153 if (cTypes != null && cTypes.length > 0 && !(cTypes.length == 1 && cTypes[0].equals(""))) 1154 { 1155 for (String cType : cTypes) 1156 { 1157 XMLUtils.createElement(contentHandler, "content-type", cType); 1158 } 1159 } 1160 } 1161 1162 private void _saxTags(Request request) throws SAXException 1163 { 1164 String size = request.getParameter("tags-size"); 1165 if (!StringUtils.isEmpty(size)) 1166 { 1167 int nbCat = Integer.parseInt(size); 1168 for (int i = 1; i < nbCat + 1; i++) 1169 { 1170 String[] tags = request.getParameterValues("tags-" + i); 1171 if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals(""))) 1172 { 1173 if (tags.length == 1) 1174 { 1175 tags = tags[0].split(","); 1176 } 1177 1178 for (String tag : tags) 1179 { 1180 XMLUtils.createElement(contentHandler, "tag", tag); 1181 } 1182 1183 } 1184 } 1185 } 1186 1187 String[] tags = request.getParameterValues("tags"); 1188 if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals(""))) 1189 { 1190 for (String tag : tags) 1191 { 1192 XMLUtils.createElement(contentHandler, "tag", tag); 1193 } 1194 } 1195 } 1196 1197 private void _saxPages(Request request) throws SAXException 1198 { 1199 String[] pages = request.getParameterValues("pages"); 1200 if (pages != null && pages.length > 0 && !(pages.length == 1 && pages[0].equals(""))) 1201 { 1202 for (String id : pages) 1203 { 1204 XMLUtils.createElement(contentHandler, "page", id); 1205 } 1206 } 1207 } 1208 1209 private void _saxMultisite(Request request) throws SAXException 1210 { 1211 boolean multisite = request.getParameter("multisite") != null; 1212 if (multisite) 1213 { 1214 XMLUtils.createElement(contentHandler, "multisite"); 1215 1216 String[] sites = request.getParameterValues("sites"); 1217 if (sites != null && sites.length > 0 && !(sites.length == 1 && sites[0].equals(""))) 1218 { 1219 for (String site : sites) 1220 { 1221 XMLUtils.createElement(contentHandler, "site", site); 1222 } 1223 } 1224 1225 } 1226 } 1227 1228 /** 1229 * Get the content type's 1230 * 1231 * @param request The request 1232 * @return the content type's 1233 */ 1234 protected Collection<String> getContentTypes(Request request) 1235 { 1236 String[] cTypes = request.getParameterValues("content-types"); 1237 1238 if (cTypes != null && cTypes.length > 0 && !(cTypes.length == 1 && cTypes[0].equals(""))) 1239 { 1240 if (cTypes.length == 1) 1241 { 1242 cTypes = cTypes[0].split(","); 1243 } 1244 1245 return Arrays.asList(cTypes); 1246 } 1247 return _getAvailableContentTypes(request); 1248 } 1249 1250 private Collection<String> _getAvailableContentTypes(Request request) 1251 { 1252 @SuppressWarnings("unchecked") 1253 Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); 1254 if (parentContext == null) 1255 { 1256 parentContext = Collections.emptyMap(); 1257 } 1258 1259 String[] serviceCTypes = (String[]) parentContext.get("search-by-content-types"); 1260 1261 ContentTypeSearch contentTypeSearch = getContentTypeSearch(request); 1262 1263 if (ArrayUtils.isNotEmpty(serviceCTypes) && StringUtils.isNotBlank(serviceCTypes[0])) 1264 { 1265 return Arrays.asList(serviceCTypes); 1266 } 1267 else if (!contentTypeSearch.equals(ContentTypeSearch.NONE)) 1268 { 1269 return _getAllContentTypes(); 1270 } 1271 1272 return Collections.emptyList(); 1273 } 1274 1275 private Set<String> _getAllContentTypes() 1276 { 1277 Set<String> allCTypes = new HashSet<>(_cTypeExtPt.getExtensionsIds()); 1278 allCTypes.add("resource"); 1279 return allCTypes; 1280 } 1281 1282 private void _saxContentTypeCriteria(Request request) throws SAXException 1283 { 1284 Collection<String> cTypes = _getAvailableContentTypes(request); 1285 1286 XMLUtils.startElement(contentHandler, "content-types"); 1287 for (String cTypeId : cTypes) 1288 { 1289 if ("resource".equals(cTypeId)) 1290 { 1291 AttributesImpl attr = new AttributesImpl(); 1292 attr.addCDATAAttribute("id", cTypeId); 1293 XMLUtils.startElement(contentHandler, "type", attr); 1294 new I18nizableText("plugin.web", "PLUGINS_WEB_SERVICE_FRONT_SEARCH_ON_DOCUMENTS").toSAX(contentHandler); 1295 XMLUtils.endElement(contentHandler, "type"); 1296 } 1297 else if (StringUtils.isNotEmpty(cTypeId)) 1298 { 1299 ContentType cType = _cTypeExtPt.getExtension(cTypeId); 1300 1301 if (cType != null) 1302 { 1303 AttributesImpl attr = new AttributesImpl(); 1304 attr.addCDATAAttribute("id", cTypeId); 1305 XMLUtils.startElement(contentHandler, "type", attr); 1306 cType.getLabel().toSAX(contentHandler); 1307 XMLUtils.endElement(contentHandler, "type"); 1308 } 1309 else 1310 { 1311 getLogger().warn("Cannot sax information about the unexising ContentType '" + cTypeId + "' for url " + request.getRequestURI()); 1312 } 1313 } 1314 1315 } 1316 XMLUtils.endElement(contentHandler, "content-types"); 1317 } 1318 1319 private void _saxTagsCriteria(String siteName) throws SAXException 1320 { 1321 @SuppressWarnings("unchecked") 1322 Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); 1323 if (parentContext == null) 1324 { 1325 parentContext = Collections.emptyMap(); 1326 } 1327 1328 Map<String, Object> contextParameters = new HashMap<>(); 1329 contextParameters.put("siteName", siteName); 1330 1331 String[] tagArray = (String[]) parentContext.get("search-by-tags"); 1332 if (ArrayUtils.isNotEmpty(tagArray)) 1333 { 1334 XMLUtils.startElement(contentHandler, "tags"); 1335 for (String tagName : tagArray) 1336 { 1337 if (StringUtils.isNotEmpty(tagName)) 1338 { 1339 boolean found = false; 1340 Set<String> extensionsIds = _tagExtPt.getExtensionsIds(); 1341 1342 for (String id : extensionsIds) 1343 { 1344 if (found) 1345 { 1346 continue; 1347 } 1348 1349 I18nizableText label = null; 1350 Map<String, CMSTag> tags = null; 1351 1352 TagProvider<CMSTag> tagProvider = _tagExtPt.getExtension(id); 1353 if (tagName.startsWith("provider_") && id.equals(tagName.substring("provider_".length()))) 1354 { 1355 found = true; 1356 1357 label = tagProvider.getLabel(); 1358 1359 tags = tagProvider.getTags(contextParameters); 1360 } 1361 else if (tagProvider.hasTag(tagName, contextParameters)) 1362 { 1363 found = true; 1364 1365 CMSTag tag = tagProvider.getTag(tagName, contextParameters); 1366 label = tag.getTitle(); 1367 tags = tag.getTags(); 1368 } 1369 1370 if (found) 1371 { 1372 AttributesImpl attr = new AttributesImpl(); 1373 attr.addCDATAAttribute("id", tagName); 1374 XMLUtils.startElement(contentHandler, "tag", attr); 1375 1376 if (label != null) 1377 { 1378 label.toSAX(contentHandler, "title"); 1379 } 1380 1381 if (tags != null) 1382 { 1383 for (CMSTag child : tags.values()) 1384 { 1385 if (child.getTarget().getName().equals("CONTENT")) 1386 { 1387 _saxTag(child, true); 1388 } 1389 } 1390 } 1391 1392 XMLUtils.endElement(contentHandler, "tag"); 1393 } 1394 } 1395 } 1396 } 1397 XMLUtils.endElement(contentHandler, "tags"); 1398 } 1399 } 1400 1401 private void _saxTag(Tag tag, boolean recursive) throws SAXException 1402 { 1403 AttributesImpl attr = new AttributesImpl(); 1404 attr.addCDATAAttribute("id", tag.getName()); 1405 XMLUtils.startElement(contentHandler, "tag", attr); 1406 tag.getTitle().toSAX(contentHandler, "title"); 1407 1408 if (recursive) 1409 { 1410 for (Tag child : tag.getTags().values()) 1411 { 1412 _saxTag(child, true); 1413 } 1414 } 1415 1416 XMLUtils.endElement(contentHandler, "tag"); 1417 } 1418 1419 private void _saxSitemapCriteria() throws SAXException 1420 { 1421 @SuppressWarnings("unchecked") 1422 Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); 1423 if (parentContext == null) 1424 { 1425 parentContext = Collections.emptyMap(); 1426 } 1427 1428 String[] pages = (String[]) parentContext.get("search-by-pages"); 1429 if (ArrayUtils.isNotEmpty(pages)) 1430 { 1431 XMLUtils.startElement(contentHandler, "pages"); 1432 1433 for (String pageID : pages) 1434 { 1435 if (StringUtils.isNotEmpty(pageID)) 1436 { 1437 Page page = _resolver.resolveById(pageID); 1438 AttributesImpl attr = new AttributesImpl(); 1439 attr.addCDATAAttribute("id", pageID); 1440 attr.addCDATAAttribute("path", page.getSitemap().getName() + "/" + page.getPathInSitemap()); 1441 attr.addCDATAAttribute("title", page.getTitle()); 1442 attr.addCDATAAttribute("long-title", page.getLongTitle()); 1443 XMLUtils.createElement(contentHandler, "page", attr); 1444 } 1445 } 1446 1447 XMLUtils.endElement(contentHandler, "pages"); 1448 } 1449 } 1450 1451 /** 1452 * Get the content type's filter value 1453 * 1454 * @param request The request 1455 * @return the content type's filter value 1456 */ 1457 protected String getContentTypeFilterValue(Request request) 1458 { 1459 Enumeration<String> paramNames = request.getParameterNames(); 1460 while (paramNames.hasMoreElements()) 1461 { 1462 String paramName = paramNames.nextElement(); 1463 if (paramName.startsWith("ctype-filter-")) 1464 { 1465 return paramName.substring("ctype-filter-".length()); 1466 } 1467 } 1468 1469 if (request.getParameter("current-ctype-filter") != null) 1470 { 1471 return request.getParameter("current-ctype-filter"); 1472 } 1473 1474 return null; 1475 } 1476 1477 /** 1478 * Add the text field query 1479 * @param queries the queries 1480 * @param language the language 1481 * @param request the request 1482 */ 1483 protected void addTextFieldQuery(Collection<Query> queries, String language, Request request) 1484 { 1485 String text = request.getParameter("textfield"); 1486 1487 if (StringUtils.isNotBlank(text)) 1488 { 1489 String trimText = text.trim(); 1490 String escapedText = _escapeQueryCharsButNotQuotes(trimText); 1491 1492 Query textFieldQuery = new OrQuery( 1493 new FullTextQuery(escapedText, language, Operator.SEARCH, true), 1494 new StringQuery(Content.ATTRIBUTE_TITLE, Operator.SEARCH, escapedText, language, true)); 1495 queries.add(textFieldQuery); 1496 } 1497 } 1498 1499 /** 1500 * Get the queries for content's attachments and resources 1501 * @param subQuery the query for attachments and resources 1502 * @return the join query for content's attachments and resources 1503 */ 1504 protected List<Query> getContentResourcesOrAttachmentQueries(Query subQuery) 1505 { 1506 if (subQuery instanceof MatchAllQuery) 1507 { 1508 return Collections.emptyList(); 1509 } 1510 1511 List<Query> queries = new ArrayList<>(); 1512 // join query on outgoing resources 1513 queries.add(new JoinQuery(subQuery, CONTENT_OUTGOING_REFEERENCES_RESOURCE_IDS)); 1514 // join query on content's attachments 1515 queries.add(() -> "{!join from=" + RESOURCE_ANCESTOR_IDS + " to=" + CONTENT_OUTGOING_REFEERENCES_RESOURCE_IDS + " v=\"" + _buildSubQuery(subQuery) + "\"}"); 1516 return queries; 1517 } 1518 1519 /** 1520 * Get the queries for page's attachments and resources 1521 * @param subQuery the query for attachments and resources 1522 * @return the join query for content's attachments and resources 1523 */ 1524 protected List<Query> getPageResourcesOrAttachmentQueries(Query subQuery) 1525 { 1526 List<Query> queries = new ArrayList<>(); 1527 // join query on outgoing resources 1528 queries.add(new JoinQuery(subQuery, PAGE_OUTGOING_REFEERENCES_RESOURCE_IDS)); 1529 // join query on page's attachments or explorer folder service 1530 queries.add(() -> "{!join from=" + RESOURCE_ANCESTOR_IDS + " to=" + PAGE_OUTGOING_REFEERENCES_RESOURCE_IDS + " v=\"" + _buildSubQuery(subQuery) + "\"}"); 1531 return queries; 1532 } 1533 1534 private String _buildSubQuery(Query subQuery) throws QuerySyntaxException 1535 { 1536 return ClientUtils.escapeQueryChars(subQuery.build()); 1537 } 1538 1539 /* 1540 * Do not escape double quotes for exact searching 1541 */ 1542 private String _escapeQueryCharsButNotQuotes(String text) 1543 { 1544 StringBuilder sb = new StringBuilder(); 1545 String[] parts = StringUtils.splitPreserveAllTokens(text, '"'); 1546 for (int i = 0; i < parts.length; i++) 1547 { 1548 if (i != 0) 1549 { 1550 sb.append("\""); 1551 } 1552 String part = parts[i]; 1553 sb.append(ClientUtils.escapeQueryChars(part)); 1554 } 1555 1556 return sb.toString(); 1557 } 1558 1559 /** 1560 * Add query for each attribute 1561 * 1562 * @param queries The list of query 1563 * @param language The lang 1564 * @param request The request 1565 * @throws IllegalArgumentException If an error occurred 1566 */ 1567 protected void addAttributeQuery(Collection<Query> queries, String language, Request request) throws IllegalArgumentException 1568 { 1569 ZoneItem zoneItem = getZoneItem(request); 1570 Set<String> facetFields = getFacets(request).keySet(); 1571 if (zoneItem != null && zoneItem.getServiceParameters().hasValue("search-by-metadata")) 1572 { 1573 String[] attributePaths = zoneItem.getServiceParameters().getValue("search-by-metadata"); 1574 for (String attributePath : attributePaths) 1575 { 1576 if (facetFields.contains(attributePath)) 1577 { 1578 // will be handled in org.ametys.web.frontoffice.SearchGenerator.getFacetValues(Request, Collection<String>, String) 1579 continue; 1580 } 1581 String value = request.getParameter("metadata-" + attributePath.replaceAll("/", ".")); 1582 1583 if (StringUtils.isNotBlank(value)) 1584 { 1585 Collection<String> contentTypes = getContentTypes(request); 1586 ModelItem modelItem = _getModelItemFromContentTypes(contentTypes, attributePath); 1587 queries.add(_getStringAttributeQuery(language, attributePath, value, modelItem)); 1588 } 1589 } 1590 } 1591 } 1592 1593 /** 1594 * Get query from string attribute 1595 * @param language the language 1596 * @param attributePath the attribute path 1597 * @param value the value 1598 * @param modelItem the model item 1599 * @return the query 1600 */ 1601 protected Query _getStringAttributeQuery(String language, String attributePath, String value, ModelItem modelItem) 1602 { 1603 Query query; 1604 if (modelItem != null && modelItem instanceof ElementDefinition 1605 && (((ElementDefinition) modelItem).getEnumerator() != null || modelItem.getType().getId().equals(ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID))) 1606 { 1607 // Exact search 1608 query = new StringQuery(attributePath, Operator.EQ, value, language); 1609 } 1610 else if (value.startsWith("\"") && value.endsWith("\"")) 1611 { 1612 String escapedText = ClientUtils.escapeQueryChars(value.substring(1, value.length() - 1)); // the StringQuery with EQ operator will add quotes characters anyway, so remove them 1613 query = new StringQuery(attributePath, Operator.EQ, escapedText, language, true); 1614 } 1615 else 1616 { 1617 String escapedText = _escapedTextForSearch(value); 1618 Operator op = escapedText.startsWith("*") || escapedText.endsWith("*") ? Operator.LIKE : Operator.SEARCH; 1619 boolean doNotUseLanguage = op == Operator.LIKE && modelItem != null && !(modelItem.getType().getId().equals(ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID)); 1620 query = new StringQuery(attributePath, op, escapedText, doNotUseLanguage ? null : language, true); 1621 } 1622 return query; 1623 } 1624 1625 private String _escapedTextForSearch(String value) 1626 { 1627 // Allow wildcards at the beginning and at the end 1628 // Ex.: 1629 // zert will stay zert to not match azerty 1630 // *zert will stay *zert to not match azerty and match azert 1631 // zert* will stay zert* to not match azerty and match zerty 1632 // *zert* will stay *zert* to match azerty 1633 // where zert is escaped 1634 1635 String valueToEscape = value; 1636 String begin = ""; 1637 String end = ""; 1638 boolean startsWithStar = value.startsWith("*"); 1639 boolean endsWithStar = value.endsWith("*"); 1640 if (startsWithStar) 1641 { 1642 valueToEscape = valueToEscape.substring(1); 1643 begin = "*"; 1644 } 1645 if (endsWithStar) 1646 { 1647 valueToEscape = valueToEscape.substring(0, valueToEscape.length() - 1); 1648 end = "*"; 1649 } 1650 1651 // escape special characters 1652 String escapedValue = ClientUtils.escapeQueryChars(valueToEscape).toLowerCase(); 1653 String escapedText = begin + escapedValue + end; 1654 return escapedText; 1655 } 1656 1657 /** 1658 * Add content query for "all words" 1659 * @param queries The queries 1660 * @param language The current language 1661 * @param request the request 1662 */ 1663 protected void addAllWordsTextFieldQuery(Collection<Query> queries, String language, Request request) 1664 { 1665 // TODO All words? OrQuery of AndQuery instead? 1666 String words = request.getParameter("all-words"); 1667 1668 if (StringUtils.isNotBlank(words)) 1669 { 1670 StringBuilder allWords = new StringBuilder(); 1671 for (String word : StringUtils.split(words)) 1672 { 1673 String escapedWord = ClientUtils.escapeQueryChars(word); 1674 if (allWords.length() > 0) 1675 { 1676 allWords.append(' '); 1677 } 1678 allWords.append('+').append(escapedWord); 1679 } 1680 1681 // queries.add(new FullTextQuery(allWords.toString(), language, 1682 // Operator.SEARCH)); 1683 1684 queries.add(new FullTextQuery(allWords.toString(), language, Operator.SEARCH, true)); 1685 } 1686 } 1687 1688 /** 1689 * Add content query for exact wording or phrase 1690 * @param queries The queries 1691 * @param language The current language 1692 * @param request the request 1693 */ 1694 protected void addExactWordingTextFieldQuery(Collection<Query> queries, String language, Request request) 1695 { 1696 String exact = request.getParameter("exact-wording"); 1697 1698 if (StringUtils.isNotBlank(exact)) 1699 { 1700 queries.add(new FullTextQuery(exact, language, Operator.EQ)); 1701 } 1702 } 1703 1704 /** 1705 * Add content query for "none of these words" 1706 * @param queries The queries 1707 * @param language The current language 1708 * @param request the request 1709 */ 1710 protected void addNoWordsTextFieldQuery(Collection<Query> queries, String language, Request request) 1711 { 1712 String noWords = request.getParameter("no-words"); 1713 1714 if (StringUtils.isNotBlank(noWords)) 1715 { 1716 StringBuilder allWords = new StringBuilder(); 1717 for (String word : StringUtils.split(noWords)) 1718 { 1719 String escapedWord = ClientUtils.escapeQueryChars(word); 1720 if (allWords.length() > 0) 1721 { 1722 allWords.append(' '); 1723 } 1724 allWords.append('+').append(escapedWord); 1725 } 1726 1727 // queries.add(new NotQuery(new FullTextQuery(allWords.toString(), 1728 // language, Operator.SEARCH))); 1729 1730 queries.add(new NotQuery(new FullTextQuery(allWords.toString(), language, Operator.SEARCH, true))); 1731 } 1732 } 1733 1734 /** 1735 * Add the content type query 1736 * @param queries The queries 1737 * @param request The request 1738 */ 1739 protected void addContentTypeQuery(Collection<Query> queries, Request request) 1740 { 1741 Collection<String> cTypes = new ArrayList<>(getContentTypes(request)); 1742 1743 // Resource is handled in the "document types" filter. 1744 cTypes.remove("resource"); 1745 1746 if (!cTypes.isEmpty()) 1747 { 1748 Query notAPageQuery = new NotQuery(new DocumentTypeQuery(TYPE_PAGE)); 1749 Query pageWithContentOfTypesQuery = new PageContentQuery(new ContentTypeQuery(cTypes)); 1750 // If document is not a page, it will match this fq (it will be filtered if necessary in the "document types" filter, see #getDocumentTypesQuery()) 1751 // If document is a page, then one of its contents must match one of the given content types 1752 queries.add(new OrQuery(notAPageQuery, pageWithContentOfTypesQuery)); 1753 } 1754 } 1755 1756 /** 1757 * Add the tags query 1758 * @param queries The queries 1759 * @param request The request 1760 */ 1761 protected void addTagsQuery(Collection<Query> queries, Request request) 1762 { 1763 String size = request.getParameter("tags-size"); 1764 if (!StringUtils.isEmpty(size)) 1765 { 1766 @SuppressWarnings("unchecked") 1767 Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); 1768 if (parentContext == null) 1769 { 1770 parentContext = Collections.emptyMap(); 1771 } 1772 1773 boolean isStrictSearch = parameters.getParameterAsBoolean("strict-search-on-tags", true); 1774 1775 int nbCat = Integer.parseInt(size); 1776 for (int i = 1; i < nbCat + 1; i++) 1777 { 1778 String[] tags = request.getParameterValues("tags-" + i); 1779 if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals(""))) 1780 { 1781 List<Query> tagQueries = new ArrayList<>(); 1782 1783 for (String tag : tags) 1784 { 1785 String[] values = StringUtils.split(tag, ','); 1786 1787 tagQueries.add(new TagQuery(Operator.EQ, !isStrictSearch, values)); 1788 } 1789 1790 queries.add(new PageContentQuery(new OrQuery(tagQueries))); 1791 } 1792 } 1793 } 1794 } 1795 1796 /** 1797 * Add the pages query 1798 * @param queries The queries 1799 * @param request The request 1800 */ 1801 protected void addPagesQuery(Collection<Query> queries, Request request) 1802 { 1803 List<Query> pageQueries = new ArrayList<>(); 1804 1805 String[] pages = request.getParameterValues("pages"); 1806 if (pages != null && pages.length > 0 && !(pages.length == 1 && pages[0].equals(""))) 1807 { 1808 for (String pageIds : pages) 1809 { 1810 for (String pageId : StringUtils.split(pageIds, ",")) 1811 { 1812 pageQueries.add(new PageQuery(pageId, true)); 1813 } 1814 } 1815 } 1816 1817 queries.add(new OrQuery(pageQueries)); 1818 } 1819 1820 /** 1821 * Add the query on start and end dates 1822 * @param queries The queries 1823 * @param request The request 1824 */ 1825 protected void addDateQuery(Collection<Query> queries, Request request) 1826 { 1827 String startDateId = parameters.getParameter("startDate", ""); 1828 String endDateId = parameters.getParameter("endDate", ""); 1829 1830 String startDateStr = request.getParameter("startDate"); 1831 String endDateStr = request.getParameter("endDate"); 1832 1833 // We check if startDateStr < endDateStr. If not, we swap. 1834 if (StringUtils.isNotBlank(endDateStr) && StringUtils.isNotBlank(startDateStr) && startDateStr.compareTo(endDateStr) > 0) 1835 { 1836 String dateAux = startDateStr; 1837 startDateStr = endDateStr; 1838 endDateStr = dateAux; 1839 } 1840 1841 String startPropId = startDateId.equals("last-modified") ? SolrFieldNames.LAST_MODIFIED : startDateId; 1842 String endPropId = endDateId.equals("last-modified") ? SolrFieldNames.LAST_MODIFIED : endDateId; 1843 1844 if (StringUtils.isNotBlank(endDateStr)) 1845 { 1846 LocalDate endDate = _toDate(endDateStr); 1847 // Join for returning page documents 1848 queries.add(new PageContentQuery(new DateQuery(startPropId, Operator.LE, endDate))); 1849 } 1850 1851 if (StringUtils.isNotBlank(startDateStr)) 1852 { 1853 LocalDate startDate = _toDate(startDateStr); 1854 1855 if (startDate != null) 1856 { 1857 // Two cases are possible : 1858 // An event could have an end date (query 1) 1859 // If not, the start date is also the end date (query 2) 1860 List<Query> endDateQueries = new ArrayList<>(); 1861 1862 if (StringUtils.isNotBlank(endPropId)) 1863 { 1864 endDateQueries.add(new DateQuery(endPropId, Operator.GE, startDate)); 1865 } 1866 endDateQueries.add(new DateQuery(startPropId, Operator.GE, startDate)); 1867 1868 // Join for returning page documents 1869 queries.add(new PageContentQuery(new OrQuery(endDateQueries))); 1870 } 1871 } 1872 } 1873 1874 private LocalDate _toDate(String dateStr) 1875 { 1876 try 1877 { 1878 return LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE); 1879 } 1880 catch (DateTimeParseException e) 1881 { 1882 getLogger().error("Invalid date format " + dateStr, e); 1883 } 1884 return null; 1885 } 1886 1887 private List<QueryAdapterFOSearch> _getSortedListQueryAdapter() 1888 { 1889 return _queryAdapterFOSearchEP.getExtensionsIds() 1890 .stream() 1891 .map(_queryAdapterFOSearchEP::getExtension) 1892 .collect(Collectors.toList()); 1893 } 1894 1895 private ModelItem _getModelItemFromContentTypes(Collection<String> contentTypeIds, String attributePath) 1896 { 1897 if (contentTypeIds.isEmpty()) 1898 { 1899 return null; 1900 } 1901 1902 try 1903 { 1904 String[] contentTypeIdsAsArray = contentTypeIds.toArray(new String[contentTypeIds.size()]); 1905 return _contentTypesHelper.getModelItem(attributePath, contentTypeIdsAsArray, new String[0]); 1906 } 1907 catch (UndefinedItemPathException e) 1908 { 1909 // No such attribute exists, retrieve null 1910 return null; 1911 } 1912 } 1913 1914 static class ContentTypeSearchField implements SearchField 1915 { 1916 @Override 1917 public String getSortField() 1918 { 1919 return PAGE_CONTENT_TYPES; 1920 } 1921 1922 @Override 1923 public String getName() 1924 { 1925 return PAGE_CONTENT_TYPES; 1926 } 1927 1928 @Override 1929 public String getFacetField() 1930 { 1931 return PAGE_CONTENT_TYPES + "_s_dv"; 1932 } 1933 1934 @Override 1935 public boolean isJoined() 1936 { 1937 return false; 1938 } 1939 1940 @Override 1941 public List<String> getJoinedPaths() 1942 { 1943 return Collections.emptyList(); 1944 } 1945 } 1946 1947}