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