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