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