001/* 002 * Copyright 2017 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.ametys.web.frontoffice; 018 019import java.io.IOException; 020import java.time.LocalDate; 021import java.time.format.DateTimeFormatter; 022import java.time.format.DateTimeParseException; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.Enumeration; 028import java.util.HashMap; 029import java.util.HashSet; 030import java.util.List; 031import java.util.Locale; 032import java.util.Map; 033import java.util.Map.Entry; 034import java.util.Optional; 035import java.util.Set; 036import java.util.stream.Collectors; 037import java.util.stream.Stream; 038 039import org.apache.avalon.framework.service.ServiceException; 040import org.apache.avalon.framework.service.ServiceManager; 041import org.apache.cocoon.environment.ObjectModelHelper; 042import org.apache.cocoon.environment.Request; 043import org.apache.cocoon.xml.AttributesImpl; 044import org.apache.cocoon.xml.XMLUtils; 045import org.apache.commons.collections.MapUtils; 046import org.apache.commons.lang.StringEscapeUtils; 047import org.apache.commons.lang3.ArrayUtils; 048import org.apache.commons.lang3.StringUtils; 049import org.apache.solr.client.solrj.util.ClientUtils; 050import org.xml.sax.SAXException; 051 052import org.ametys.cms.content.indexing.solr.SolrFieldNames; 053import org.ametys.cms.contenttype.ContentAttributeDefinition; 054import org.ametys.cms.contenttype.ContentType; 055import org.ametys.cms.data.type.ModelItemTypeConstants; 056import org.ametys.cms.repository.Content; 057import org.ametys.cms.repository.ContentTypeExpression; 058import org.ametys.cms.repository.LanguageExpression; 059import org.ametys.cms.search.SearchField; 060import org.ametys.cms.search.SearchResult; 061import org.ametys.cms.search.SearchResults; 062import org.ametys.cms.search.SearchResultsIterable; 063import org.ametys.cms.search.SearchResultsIterator; 064import org.ametys.cms.search.Sort; 065import org.ametys.cms.search.Sort.Order; 066import org.ametys.cms.search.query.AndQuery; 067import org.ametys.cms.search.query.ContentTypeQuery; 068import org.ametys.cms.search.query.DateQuery; 069import org.ametys.cms.search.query.DocumentTypeQuery; 070import org.ametys.cms.search.query.FullTextQuery; 071import org.ametys.cms.search.query.JoinQuery; 072import org.ametys.cms.search.query.MatchAllQuery; 073import org.ametys.cms.search.query.NotQuery; 074import org.ametys.cms.search.query.OrQuery; 075import org.ametys.cms.search.query.Query; 076import org.ametys.cms.search.query.Query.Operator; 077import org.ametys.cms.search.query.QuerySyntaxException; 078import org.ametys.cms.search.query.StringQuery; 079import org.ametys.cms.search.query.TagQuery; 080import org.ametys.cms.search.solr.SearcherFactory.Searcher; 081import org.ametys.cms.search.solr.field.StringSearchField; 082import org.ametys.cms.tag.CMSTag; 083import org.ametys.cms.tag.Tag; 084import org.ametys.cms.tag.TagProvider; 085import org.ametys.core.util.AvalonLoggerAdapter; 086import org.ametys.core.util.LambdaUtils; 087import org.ametys.plugins.explorer.resources.Resource; 088import org.ametys.plugins.repository.AmetysObject; 089import org.ametys.plugins.repository.AmetysObjectIterable; 090import org.ametys.plugins.repository.RepositoryConstants; 091import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 092import org.ametys.plugins.repository.query.QueryHelper; 093import org.ametys.plugins.repository.query.expression.AndExpression; 094import org.ametys.plugins.repository.query.expression.Expression; 095import org.ametys.plugins.repository.query.expression.OrExpression; 096import org.ametys.runtime.i18n.I18nizableText; 097import org.ametys.runtime.model.ElementDefinition; 098import org.ametys.runtime.model.ModelItem; 099import org.ametys.runtime.model.exception.UndefinedItemPathException; 100import org.ametys.web.frontoffice.FrontOfficeSearcherFactory.FrontOfficeSearcher; 101import org.ametys.web.frontoffice.FrontOfficeSearcherFactory.FrontOfficeSolrSearchResults; 102import org.ametys.web.frontoffice.FrontOfficeSearcherFactory.QueryFacet; 103import org.ametys.web.indexing.solr.SolrWebFieldNames; 104import org.ametys.web.repository.page.Page; 105import org.ametys.web.repository.page.ZoneItem; 106import org.ametys.web.repository.site.Site; 107import org.ametys.web.repository.site.SiteTypesExtensionPoint; 108import org.ametys.web.search.query.PageContentQuery; 109import org.ametys.web.search.query.PageQuery; 110import org.ametys.web.search.query.SiteQuery; 111import org.ametys.web.search.query.SitemapQuery; 112 113/** 114 * Generates the results of a search performed on front office 115 */ 116public class SearchGenerator extends AbstractSearchGenerator 117{ 118 private static final String __FACETS_CACHE = SearchGenerator.class.getName() + "$Cache-Facets"; 119 120 /** The query adapter extension point */ 121 protected QueryAdapterFOSearchExtensionPoint _queryAdapterFOSearchEP; 122 /** The site type manager */ 123 protected SiteTypesExtensionPoint _siteTypeEP; 124 125 /** 126 * Enumeration for content type search 127 */ 128 protected enum ContentTypeSearch 129 { 130 /** To search by content type with filter (facets)*/ 131 FILTER("filter"), 132 /** To search by content type with combo box selection */ 133 LIST("list"), 134 /** To search by content type with checkbox */ 135 CHECKBOX("checkbox"), 136 /** To search by content type with checkbox and filter (facets)*/ 137 CHECKBOX_FILTER("checkbox-filter"), 138 /** To not allow to search by content type */ 139 NONE("none"); 140 141 private String _name; 142 143 private ContentTypeSearch(String name) 144 { 145 this._name = name; 146 } 147 148 @Override 149 public String toString() 150 { 151 return this._name; 152 } 153 154 /** 155 * Get enumeration value 156 * @param value The value 157 * @return The enum 158 */ 159 public static ContentTypeSearch getEnum(String value) 160 { 161 for (ContentTypeSearch ct : ContentTypeSearch.values()) 162 { 163 if (ct.toString().equals(value)) 164 { 165 return ct; 166 } 167 } 168 169 throw new IllegalArgumentException("Invalid ContentTypeChoice value: " + value); 170 } 171 } 172 173 @Override 174 public void service(ServiceManager smanager) throws ServiceException 175 { 176 super.service(smanager); 177 _queryAdapterFOSearchEP = (QueryAdapterFOSearchExtensionPoint) smanager.lookup(QueryAdapterFOSearchExtensionPoint.ROLE); 178 _siteTypeEP = (SiteTypesExtensionPoint) smanager.lookup(SiteTypesExtensionPoint.ROLE); 179 } 180 181 /** 182 * Return the type of content type's search 183 * @param request The request 184 * @return The type of search 185 */ 186 protected ContentTypeSearch getContentTypeSearch(Request request) 187 { 188 return ContentTypeSearch.getEnum(parameters.getParameter("search-by-content-types-choice", "none")); 189 } 190 191 @Override 192 protected SearchResults<AmetysObject> search(Request request, Collection<String> siteNames, String language, int pageIndex, int start, int maxResults) throws Exception 193 { 194 ContentTypeSearch searchCTypeType = getContentTypeSearch(request); 195 try 196 { 197 if (searchCTypeType.equals(ContentTypeSearch.CHECKBOX_FILTER) || searchCTypeType.equals(ContentTypeSearch.FILTER)) 198 { 199 // Retrieve current workspace 200 String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 201 202 try 203 { 204 XMLUtils.startElement(contentHandler, "content-types"); 205 206 List<Sort> sorts = new ArrayList<>(); 207 Sort sort = getSortField(request); 208 sorts.addAll(getPrimarySortFields(request)); 209 sorts.add(sort); 210 211 // Get first sort field 212 saxSort(sort); 213 214 Query queryObject = getQuery(request, siteNames, language); 215 Collection<Query> filterQueries = getFilterQueries(request, siteNames, language); 216 217 Collection<SearchField> facets = getFacets(request).values().stream().map(f -> f.getSearchField()).collect(Collectors.toList()); 218 Map<String, List<String>> facetValues = getFacetValues(request, siteNames, language); 219 220 SearchResults<AmetysObject> results = null; 221 222 String currentCtype = getContentTypeFilterValue(request); 223 if (currentCtype == null) 224 { 225 // First, do search without 'content-types' facet value 226 results = _getResultsForContentType(queryObject, filterQueries, facets, sorts, facetValues, null); 227 228 Map<String, Map<String, Integer>> facetResults = results.getFacetResults(); 229 Map<String, Integer> cTypeFacets = facetResults.get(SolrWebFieldNames.PAGE_CONTENT_TYPES); 230 Map<String, Integer> facetQueryResults = _getFacetQueryResults(results); 231 232 long totalCount = results.getTotalCount(); 233 if (totalCount > 0) 234 { 235 Collection<String> cTypes = getContentTypes(request); 236 237 // Get the first content types (in order) with at least one result 238 for (String cType : cTypes) 239 { 240 long cTypeCount; 241 if (cType.equals("resource")) 242 { 243 cTypeCount = _getNbResource(facetQueryResults); 244 } 245 else if (cTypeFacets.containsKey(cType)) 246 { 247 cTypeCount = Long.valueOf(cTypeFacets.get(cType)); 248 } 249 else 250 { 251 continue; 252 } 253 254 if (cTypeCount > 0) 255 { 256 // If facet count is equals to total count, no need to do the new search 257 if (cTypeCount < totalCount) 258 { 259 results = _getResultsForContentType(queryObject, filterQueries, facets, sorts, facetValues, cType); 260 } 261 262 currentCtype = cType; 263 break; 264 } 265 } 266 } 267 } 268 else 269 { 270 Searcher searcher = _searcherFactory.create() 271 .withQuery(queryObject) 272 .withFilterQueries(filterQueries) 273 .withFacets(facets) 274 .withFacetValues(facetValues) 275 .withLimits(0, Integer.MAX_VALUE) 276 .withSort(sorts) 277 .setCheckRights(_checkRights()); 278 279 _additionalSearchProcessing(searcher); 280 281 results = searcher.searchWithFacets(); 282 } 283 284 _handleFacetResults(request, start, maxResults, results, currentCtype); 285 286 XMLUtils.endElement(contentHandler, "content-types"); 287 288 return results; 289 } 290 finally 291 { 292 // Restore context 293 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp); 294 } 295 } 296 else 297 { 298 return super.search(request, siteNames, language, pageIndex, start, maxResults); 299 } 300 } 301 catch (QuerySyntaxException e) 302 { 303 throw new IOException("Query syntax error while searching.", e); 304 } 305 } 306 307 private Map<String, Integer> _getFacetQueryResults(SearchResults<AmetysObject> results) 308 { 309 return Optional.of(results) 310 .filter(FrontOfficeSolrSearchResults.class::isInstance) 311 .map(FrontOfficeSolrSearchResults.class::cast) 312 .map(FrontOfficeSolrSearchResults::getFacetQueryResults) 313 .orElseGet(Collections::emptyMap); 314 } 315 316 private int _getNbResource(Map<String, Integer> facetQueryResults) 317 { 318 return Optional.ofNullable(facetQueryResults.get(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME)).orElse(0); 319 } 320 321 private SearchResults<AmetysObject> _getResultsForContentType(Query queryObject, Collection<Query> filterQueries, Collection<SearchField> facets, List<Sort> sorts, Map<String, List<String>> initialFacetValues, String forceContentType) throws Exception 322 { 323 Map<String, List<String>> facetValues = new HashMap<>(initialFacetValues); 324 325 if (forceContentType == null) 326 { 327 facetValues.remove(SolrWebFieldNames.PAGE_CONTENT_TYPES); 328 } 329 else 330 { 331 facetValues.put(SolrWebFieldNames.PAGE_CONTENT_TYPES, new ArrayList<>()); 332 facetValues.get(SolrWebFieldNames.PAGE_CONTENT_TYPES).add(forceContentType); 333 } 334 335 Searcher searcher = _searcherFactory.create() 336 .withQuery(queryObject) 337 .withFilterQueries(filterQueries) 338 .withFacets(facets) 339 .withFacetValues(facetValues) 340 .withLimits(0, Integer.MAX_VALUE) 341 .withSort(sorts) 342 .setCheckRights(_checkRights()); 343 344 _additionalSearchProcessing(searcher); 345 346 return searcher.searchWithFacets(); 347 } 348 349 @Override 350 protected void _additionalSearchProcessing(Searcher searcher) 351 { 352 Request request = ObjectModelHelper.getRequest(objectModel); 353 354 if (searcher instanceof FrontOfficeSearcher) 355 { 356 FrontOfficeSearcher foSearcher = (FrontOfficeSearcher) searcher; 357 Collection<QueryFacet> queryFacets = getQueryFacets(request); 358 if (!queryFacets.isEmpty()) 359 { 360 foSearcher.withQueryFacets(queryFacets) 361 .withQueryFacetValues(getQueryFacetValues(request)); 362 } 363 } 364 } 365 366 private void _handleFacetResults(Request request, int start, int maxResults, SearchResults<AmetysObject> results, String currentCtype) throws SAXException 367 { 368 int count = 0; 369 Map<String, Map<String, Integer>> facetResults = results.getFacetResults(); 370 Map<String, Integer> facetQueryResults = _getFacetQueryResults(results); 371 if (facetResults.containsKey(SolrWebFieldNames.PAGE_CONTENT_TYPES) || facetQueryResults.containsKey(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME)) 372 { 373 Map<String, Integer> cTypeFacets = Optional.ofNullable(facetResults.get(SolrWebFieldNames.PAGE_CONTENT_TYPES)).orElseGet(Collections::emptyMap); 374 375 for (String contentTypeId : getContentTypes(request)) 376 { 377 int nbResults; 378 if ("resource".equals(contentTypeId)) 379 { 380 nbResults = _getNbResource(facetQueryResults); 381 } 382 else if (cTypeFacets.containsKey(contentTypeId) && cTypeFacets.get(contentTypeId) > 0) 383 { 384 nbResults = cTypeFacets.get(contentTypeId); 385 } 386 else 387 { 388 continue; 389 } 390 391 boolean current = contentTypeId.equals(currentCtype) || currentCtype == null && count == 0; 392 count++; 393 394 AttributesImpl attr = new AttributesImpl(); 395 if (current) 396 { 397 attr.addCDATAAttribute("current", "true"); 398 } 399 400 XMLUtils.startElement(contentHandler, contentTypeId, attr); 401 402 if (contentTypeId.equals("resource")) 403 { 404 new I18nizableText("plugin.web", "PLUGINS_WEB_SERVICE_FRONT_SEARCH_ON_DOCUMENTS").toSAX(contentHandler, "label"); 405 } 406 else 407 { 408 ContentType contentType = _cTypeExtPt.getExtension(contentTypeId); 409 if (contentType != null) 410 { 411 contentType.getLabel().toSAX(contentHandler, "label"); 412 } 413 } 414 415 // SAX results 416 AttributesImpl atts = new AttributesImpl(); 417 atts.addCDATAAttribute("total", String.valueOf(nbResults)); 418 atts.addCDATAAttribute("maxScore", String.valueOf(results.getMaxScore())); 419 420 if (current) 421 { 422 XMLUtils.startElement(contentHandler, "hits", atts); 423 saxHits(results, start, maxResults); 424 XMLUtils.endElement(contentHandler, "hits"); 425 426 // SAX pagination 427 saxPagination(results.getTotalCount(), start, maxResults); 428 } 429 else 430 { 431 XMLUtils.createElement(contentHandler, "hits", atts); 432 } 433 434 XMLUtils.endElement(contentHandler, contentTypeId); 435 } 436 } 437 } 438 439 @Override 440 protected Query getQuery(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException 441 { 442 List<Query> wordingQueries = getWordingQueries(request, siteNames, language); 443 444 // Query to execute on joined contents 445 List<Query> contentQueries = new ArrayList<>(wordingQueries); // add wording queries 446 contentQueries.addAll(getContentQueries(request, siteNames, language)); // add specific queries to contents 447 Query contentQuery = new AndQuery(contentQueries); 448 449 List<Query> contentOrResourcesQueries = new ArrayList<>(); 450 contentOrResourcesQueries.add(contentQuery); 451 if (!wordingQueries.isEmpty()) 452 { 453 contentOrResourcesQueries.addAll(getContentResourcesOrAttachmentQueries(new AndQuery(wordingQueries))); // add queries on join content's resources 454 } 455 456 Query finalContentQuery = new PageContentQuery(new OrQuery(contentOrResourcesQueries)); 457 458 // Query to execute on pages 459 List<Query> pagesQueries = new ArrayList<>(wordingQueries); // add wording queries 460 pagesQueries.addAll(getPageQueries(request, siteNames, language)); // add specific queries to pages 461 Query pageQuery = pagesQueries.isEmpty() && contentQueries.isEmpty() ? new MatchAllQuery() : new AndQuery(pagesQueries); 462 463 List<Query> pageOrResourcesQueries = new ArrayList<>(); 464 pageOrResourcesQueries.add(pageQuery); 465 if (!wordingQueries.isEmpty()) 466 { 467 pageOrResourcesQueries.addAll(getPageResourcesOrAttachmentQueries(new AndQuery(wordingQueries))); // add queries on join page's resources 468 } 469 470 Query finalPageQuery = new OrQuery(pageOrResourcesQueries); 471 472 Query finalQuery = new OrQuery(finalPageQuery, finalContentQuery); 473 474 for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter()) 475 { 476 finalQuery = queryAdapter.modifyQuery(finalQuery, request, siteNames, language); 477 } 478 479 return finalQuery; 480 } 481 482 /** 483 * Get the queries on wording (keywords, no words, exact wording, no words) 484 * @param request the request 485 * @param siteNames the site names 486 * @param language the language 487 * @return the queries on wording 488 */ 489 protected List<Query> getWordingQueries(Request request, Collection<String> siteNames, String language) 490 { 491 List<Query> queries = new ArrayList<>(); 492 493 addTextFieldQuery(queries, language, request); 494 addAllWordsTextFieldQuery(queries, language, request); 495 addExactWordingTextFieldQuery(queries, language, request); 496 addNoWordsTextFieldQuery(queries, language, request); 497 498 return queries; 499 } 500 501 /** 502 * Get the queries to be apply on joined contents ONLY 503 * @param request the request 504 * @param siteNames the site names 505 * @param language the language 506 * @return the queries for contents only 507 */ 508 protected List<Query> getContentQueries(Request request, Collection<String> siteNames, String language) 509 { 510 List<Query> queries = new ArrayList<>(); 511 queries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT)); 512 addAttributeQuery(queries, language, request); 513 return queries; 514 } 515 516 /** 517 * Get the queries to be apply on pages ONLY 518 * @param request the request 519 * @param siteNames the site names 520 * @param language the language 521 * @return the queries for pages only 522 */ 523 protected List<Query> getPageQueries(Request request, Collection<String> siteNames, String language) 524 { 525 return new ArrayList<>(); 526 } 527 528 @Override 529 protected Collection<Query> getFilterQueries(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException 530 { 531 List<Query> queries = new ArrayList<>(); 532 533 Query siteQuery = new SiteQuery(siteNames); 534 Query sitemapQuery = new SitemapQuery(language); 535 for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter()) 536 { 537 siteQuery = queryAdapter.modifySiteQueryFilter(siteQuery, request, siteNames, language); 538 sitemapQuery = queryAdapter.modifySitemapQueryFilter(sitemapQuery, request, siteNames, language); 539 } 540 541 queries.add(siteQuery); 542 queries.add(sitemapQuery); 543 544 addContentTypeQuery(queries, request); 545 addTagsQuery(queries, request); 546 addPagesQuery(queries, request); 547 addDateQuery(queries, request); 548 549 return queries; 550 } 551 552 @Override 553 protected Map<String, List<String>> getFacetValues(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException 554 { 555 Map<String, List<String>> facetValues = new HashMap<>(); 556 557 String currentCType = getContentTypeFilterValue(request); 558 if (currentCType != null) 559 { 560 facetValues.put(SolrWebFieldNames.PAGE_CONTENT_TYPES, new ArrayList<>()); 561 if (!"resource".equals(currentCType)) 562 { 563 facetValues.get(SolrWebFieldNames.PAGE_CONTENT_TYPES).add(currentCType); 564 } 565 } 566 else if (getContentTypeSearch(request).equals(ContentTypeSearch.FILTER) || getContentTypeSearch(request).equals(ContentTypeSearch.CHECKBOX_FILTER)) 567 { 568 // Get the first content types as facet value 569 facetValues.put(SolrWebFieldNames.PAGE_CONTENT_TYPES, new ArrayList<>()); 570 String firstContentType = getContentTypes(request).iterator().next(); 571 if (!"resource".equals(firstContentType)) 572 { 573 facetValues.get(SolrWebFieldNames.PAGE_CONTENT_TYPES).add(firstContentType); 574 } 575 } 576 577 Map<String, FacetField> facets = getFacets(request); 578 for (String fieldName : facets.keySet()) 579 { 580 String[] parameterValues = request.getParameterValues("metadata-" + fieldName); 581 if (parameterValues != null && parameterValues.length > 0 && StringUtils.isNotEmpty(parameterValues[0])) 582 { 583 facetValues.put(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + fieldName, new ArrayList<>()); 584 List<String> preparedParameterValues = Stream.of(parameterValues) 585 .map(StringEscapeUtils::unescapeXml) 586 .map(ClientUtils::escapeQueryChars) 587 .collect(Collectors.toList()); 588 facetValues.get(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + fieldName).addAll(preparedParameterValues); 589 } 590 } 591 592 return facetValues; 593 } 594 595 @Override 596 protected Collection<String> getQueryFacetValues(Request request) 597 { 598 List<String> queryFacetValues = new ArrayList<>(); 599 600 String currentCType = getContentTypeFilterValue(request); 601 if (currentCType != null) 602 { 603 if ("resource".equals(currentCType)) 604 { 605 queryFacetValues.add(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME); 606 } 607 } 608 else if (getContentTypeSearch(request).equals(ContentTypeSearch.FILTER) || getContentTypeSearch(request).equals(ContentTypeSearch.CHECKBOX_FILTER)) 609 { 610 // Get the first content types as facet value 611 String firstContentType = getContentTypes(request).iterator().next(); 612 if ("resource".equals(firstContentType)) 613 { 614 queryFacetValues.add(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME); 615 } 616 } 617 618 return queryFacetValues; 619 } 620 621 @Override 622 protected Map<String, FacetField> getFacets(Request request) throws IllegalArgumentException 623 { 624 ZoneItem zoneItem = getZoneItem(request); 625 String facetCacheAttrName = __FACETS_CACHE + "$" + Optional.ofNullable(zoneItem).map(ZoneItem::getId).orElse("null"); 626 @SuppressWarnings("unchecked") 627 Map<String, FacetField> cache = (Map<String, FacetField>) request.getAttribute(facetCacheAttrName); 628 if (cache != null) 629 { 630 return cache; 631 } 632 633 Map<String, FacetField> facets = new HashMap<>(); 634 635 // Facet for content types 636 ContentTypeSearch contentTypeSearch = getContentTypeSearch(request); 637 if (contentTypeSearch.equals(ContentTypeSearch.FILTER) || contentTypeSearch.equals(ContentTypeSearch.CHECKBOX_FILTER)) 638 { 639 SearchField cTypeField = new ContentTypeSearchField(); 640 641 facets.put(PAGE_CONTENT_TYPES, new ContentTypeFacetField(cTypeField)); 642 } 643 644 if (useFacets()) 645 { 646 Collection<String> contentTypes = getContentTypes(request); 647 648 if (zoneItem != null && zoneItem.getServiceParameters().hasValue("search-by-metadata")) 649 { 650 String[] metadataPaths = zoneItem.getServiceParameters().getValue("search-by-metadata"); 651 for (String metadataPath : metadataPaths) 652 { 653 _addAttributeFacet(facets, contentTypes, metadataPath); 654 } 655 } 656 } 657 658 request.setAttribute(facetCacheAttrName, facets); 659 660 return facets; 661 } 662 663 /** 664 * Add attribute facet to facets map 665 * @param facets the facets map 666 * @param contentTypes the content types 667 * @param attributePath the attribute path 668 */ 669 protected void _addAttributeFacet(Map<String, FacetField> facets, Collection<String> contentTypes, String attributePath) 670 { 671 ModelItem modelItem = _getModelItemFromContentTypes(contentTypes, attributePath); 672 673 if (modelItem != null && modelItem instanceof ElementDefinition) 674 { 675 ElementDefinition elementDefinition = (ElementDefinition) modelItem; 676 if (elementDefinition.getEnumerator() != null || elementDefinition.getType().getId().equals(ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID)) 677 { 678 Optional<SearchField> searchField = _searchHelper.getSearchField(contentTypes, attributePath); 679 if (searchField.isPresent()) 680 { 681 String searchFieldName = searchField.get().getName(); 682 StringSearchField facetField = new StringSearchField(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + searchFieldName); 683 facets.put(searchFieldName, new AttributeFacetField(facetField, modelItem, new AvalonLoggerAdapter(getLogger()))); 684 } 685 } 686 } 687 } 688 689 @Override 690 protected Set<QueryFacet> getQueryFacets(Request request) 691 { 692 Set<QueryFacet> queryFacets = new HashSet<>(); 693 Collection<String> contentTypes = getContentTypes(request); 694 695 // For handling "resource" content type in facets for content types 696 ContentTypeSearch contentTypeSearch = getContentTypeSearch(request); 697 if ((contentTypeSearch.equals(ContentTypeSearch.FILTER) || contentTypeSearch.equals(ContentTypeSearch.CHECKBOX_FILTER)) 698 && contentTypes.contains("resource")) 699 { 700 queryFacets.add(new QueryFacet(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME, 701 PAGE_CONTENT_TYPES, 702 DOCUMENT_TYPE + ":" + TYPE_PAGE_RESOURCE)); 703 } 704 705 for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter()) 706 { 707 queryFacets = queryAdapter.modifyQueryFacets(queryFacets, request); 708 } 709 710 return queryFacets; 711 } 712 713 /** 714 * Get the filter queries for a single fixed content type. 715 * 716 * @param request The request. 717 * @param cType The fixed content type. 718 * @param siteNames the site names. 719 * @param language The language. 720 * @return The filter queries. 721 */ 722 protected Collection<Query> getFixedCTypeFilterQueries(Request request, String cType, Collection<String> siteNames, String language) 723 { 724 List<Query> queries = new ArrayList<>(); 725 726 queries.add(new SiteQuery(siteNames)); 727 queries.add(new SitemapQuery(language)); 728 729 if ("resource".equals(cType)) 730 { 731 queries.add(new DocumentTypeQuery(TYPE_PAGE_RESOURCE)); 732 } 733 else 734 { 735 // Fixed content type query. 736 queries.add(new PageContentQuery(new ContentTypeQuery(cType))); 737 } 738 addTagsQuery(queries, request); 739 addPagesQuery(queries, request); 740 addDateQuery(queries, request); 741 742 return queries; 743 } 744 745 @Override 746 protected Collection<String> getDocumentTypes(Request request) 747 { 748 List<String> documentTypes = new ArrayList<>(); 749 750 Collection<String> cTypes = getContentTypes(request); 751 if (cTypes.size() == 1 && "resource".equals(cTypes.iterator().next())) 752 { 753 documentTypes.add(TYPE_PAGE_RESOURCE); 754 } 755 // An empty collections means "all". 756 else if (cTypes.isEmpty() || cTypes.contains("resource")) 757 { 758 documentTypes.addAll(Arrays.asList(TYPE_PAGE, TYPE_PAGE_RESOURCE)); 759 } 760 else 761 { 762 documentTypes.add(TYPE_PAGE); 763 } 764 765 // Add other documents types (by priority order) 766 for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter()) 767 { 768 queryAdapter.addDocumentType(documentTypes); 769 } 770 771 return documentTypes; 772 } 773 774 @Override 775 protected Collection<String> getFields() 776 { 777 return Collections.emptyList(); 778 } 779 780 @Override 781 protected void saxHits(SearchResults<AmetysObject> results, int start, int maxResults) throws SAXException 782 { 783 SearchResultsIterable<SearchResult<AmetysObject>> resultsIt = results.getResults(); 784 long limit = Math.min(start + maxResults, resultsIt.getSize()); 785 float maxScore = results.getMaxScore(); 786 787 SearchResultsIterator<SearchResult<AmetysObject>> it = resultsIt.iterator(); 788 it.skip(start); 789 for (int i = start; i < limit; i++) 790 { 791 if (it.hasNext()) // this should return true except if there is a inconsistency between repository and Solr index 792 { 793 SearchResult<AmetysObject> searchResult = it.next(); 794 float score = searchResult.getScore(); 795 AmetysObject ametysObject = searchResult.getObject(); 796 if (ametysObject instanceof Page) 797 { 798 saxPageHit(score, maxScore, (Page) ametysObject); 799 } 800 else if (ametysObject instanceof Resource) 801 { 802 saxResourceHit(score, maxScore, (Resource) ametysObject); 803 } 804 } 805 806 } 807 } 808 809 @Override 810 protected Sort getSortField(Request request) 811 { 812 if (request.getParameter("sort-by-title-for-sorting") != null || request.getParameter("sort-by-title") != null) 813 { 814 return new Sort(TITLE_SORT, Order.ASC); 815 } 816 else if (request.getParameter("sort-by-lastValidation") != null) 817 { 818 return new Sort(LAST_VALIDATION, Order.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 Sort(fieldName, Order.ASC); 831 } 832 } 833 } 834 835 return new Sort("score", Order.DESC); 836 } 837 838 @Override 839 protected List<Sort> 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().getTypedEntries(); 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(new Locale(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 Collection<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 Arrays.asList(cTypes); 1223 } 1224 return _getAvailableContentTypes(request); 1225 } 1226 1227 private Collection<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 Arrays.asList(serviceCTypes); 1243 } 1244 else if (!contentTypeSearch.equals(ContentTypeSearch.NONE)) 1245 { 1246 return _getAllContentTypes(); 1247 } 1248 1249 return Collections.emptyList(); 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.SEARCH, 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(() -> "{!join from=" + RESOURCE_ANCESTOR_IDS + " to=" + CONTENT_OUTGOING_REFEERENCES_RESOURCE_IDS + " v=\"" + _buildSubQuery(subQuery) + "\"}"); 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(() -> "{!join from=" + RESOURCE_ANCESTOR_IDS + " to=" + PAGE_OUTGOING_REFEERENCES_RESOURCE_IDS + " v=\"" + _buildSubQuery(subQuery) + "\"}"); 1508 return queries; 1509 } 1510 1511 private String _buildSubQuery(Query subQuery) throws QuerySyntaxException 1512 { 1513 return ClientUtils.escapeQueryChars(subQuery.build()); 1514 } 1515 1516 /* 1517 * Do not escape double quotes for exact searching 1518 */ 1519 private String _escapeQueryCharsButNotQuotes(String text) 1520 { 1521 StringBuilder sb = new StringBuilder(); 1522 String[] parts = StringUtils.splitPreserveAllTokens(text, '"'); 1523 for (int i = 0; i < parts.length; i++) 1524 { 1525 if (i != 0) 1526 { 1527 sb.append("\""); 1528 } 1529 String part = parts[i]; 1530 sb.append(ClientUtils.escapeQueryChars(part)); 1531 } 1532 1533 return sb.toString(); 1534 } 1535 1536 /** 1537 * Add query for each attribute 1538 * 1539 * @param queries The list of query 1540 * @param language The lang 1541 * @param request The request 1542 * @throws IllegalArgumentException If an error occurred 1543 */ 1544 protected void addAttributeQuery(Collection<Query> queries, String language, Request request) throws IllegalArgumentException 1545 { 1546 ZoneItem zoneItem = getZoneItem(request); 1547 Set<String> facetFields = getFacets(request).keySet(); 1548 if (zoneItem != null && zoneItem.getServiceParameters().hasValue("search-by-metadata")) 1549 { 1550 String[] attributePaths = zoneItem.getServiceParameters().getValue("search-by-metadata"); 1551 for (String attributePath : attributePaths) 1552 { 1553 if (facetFields.contains(attributePath)) 1554 { 1555 // will be handled in org.ametys.web.frontoffice.SearchGenerator.getFacetValues(Request, Collection<String>, String) 1556 continue; 1557 } 1558 String value = request.getParameter("metadata-" + attributePath.replaceAll("/", ".")); 1559 1560 if (StringUtils.isNotBlank(value)) 1561 { 1562 Collection<String> contentTypes = getContentTypes(request); 1563 ModelItem modelItem = _getModelItemFromContentTypes(contentTypes, attributePath); 1564 queries.add(_getStringAttributeQuery(language, attributePath, value, modelItem)); 1565 } 1566 } 1567 } 1568 } 1569 1570 /** 1571 * Get query from string attribute 1572 * @param language the language 1573 * @param attributePath the attribute path 1574 * @param value the value 1575 * @param modelItem the model item 1576 * @return the query 1577 */ 1578 protected Query _getStringAttributeQuery(String language, String attributePath, String value, ModelItem modelItem) 1579 { 1580 Query query; 1581 if (modelItem != null && modelItem instanceof ElementDefinition 1582 && (((ElementDefinition) modelItem).getEnumerator() != null || modelItem.getType().getId().equals(ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID))) 1583 { 1584 // Exact search 1585 query = new StringQuery(attributePath, Operator.EQ, value, language); 1586 } 1587 else if (value.startsWith("\"") && value.endsWith("\"")) 1588 { 1589 String escapedText = ClientUtils.escapeQueryChars(value.substring(1, value.length() - 1)); // the StringQuery with EQ operator will add quotes characters anyway, so remove them 1590 query = new StringQuery(attributePath, Operator.EQ, escapedText, language, true); 1591 } 1592 else 1593 { 1594 String escapedText = _escapedTextForSearch(value); 1595 Operator op = escapedText.startsWith("*") || escapedText.endsWith("*") ? Operator.LIKE : Operator.SEARCH; 1596 boolean doNotUseLanguage = op == Operator.LIKE && modelItem != null && !(modelItem.getType().getId().equals(ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID)); 1597 query = new StringQuery(attributePath, op, escapedText, doNotUseLanguage ? null : language, true); 1598 } 1599 return query; 1600 } 1601 1602 private String _escapedTextForSearch(String value) 1603 { 1604 // Allow wildcards at the beginning and at the end 1605 // Ex.: 1606 // zert will stay zert to not match azerty 1607 // *zert will stay *zert to not match azerty and match azert 1608 // zert* will stay zert* to not match azerty and match zerty 1609 // *zert* will stay *zert* to match azerty 1610 // where zert is escaped 1611 1612 String valueToEscape = value; 1613 String begin = ""; 1614 String end = ""; 1615 boolean startsWithStar = value.startsWith("*"); 1616 boolean endsWithStar = value.endsWith("*"); 1617 if (startsWithStar) 1618 { 1619 valueToEscape = valueToEscape.substring(1); 1620 begin = "*"; 1621 } 1622 if (endsWithStar) 1623 { 1624 valueToEscape = valueToEscape.substring(0, valueToEscape.length() - 1); 1625 end = "*"; 1626 } 1627 1628 // escape special characters 1629 String escapedValue = ClientUtils.escapeQueryChars(valueToEscape).toLowerCase(); 1630 String escapedText = begin + escapedValue + end; 1631 return escapedText; 1632 } 1633 1634 /** 1635 * Add content query for "all words" 1636 * @param queries The queries 1637 * @param language The current language 1638 * @param request the request 1639 */ 1640 protected void addAllWordsTextFieldQuery(Collection<Query> queries, String language, Request request) 1641 { 1642 // TODO All words? OrQuery of AndQuery instead? 1643 String words = request.getParameter("all-words"); 1644 1645 if (StringUtils.isNotBlank(words)) 1646 { 1647 StringBuilder allWords = new StringBuilder(); 1648 for (String word : StringUtils.split(words)) 1649 { 1650 String escapedWord = ClientUtils.escapeQueryChars(word); 1651 if (allWords.length() > 0) 1652 { 1653 allWords.append(' '); 1654 } 1655 allWords.append('+').append(escapedWord); 1656 } 1657 1658 // queries.add(new FullTextQuery(allWords.toString(), language, 1659 // Operator.SEARCH)); 1660 1661 queries.add(new FullTextQuery(allWords.toString(), language, Operator.SEARCH, true)); 1662 } 1663 } 1664 1665 /** 1666 * Add content query for exact wording or phrase 1667 * @param queries The queries 1668 * @param language The current language 1669 * @param request the request 1670 */ 1671 protected void addExactWordingTextFieldQuery(Collection<Query> queries, String language, Request request) 1672 { 1673 String exact = request.getParameter("exact-wording"); 1674 1675 if (StringUtils.isNotBlank(exact)) 1676 { 1677 queries.add(new FullTextQuery(exact, language, Operator.EQ)); 1678 } 1679 } 1680 1681 /** 1682 * Add content query for "none of these words" 1683 * @param queries The queries 1684 * @param language The current language 1685 * @param request the request 1686 */ 1687 protected void addNoWordsTextFieldQuery(Collection<Query> queries, String language, Request request) 1688 { 1689 String noWords = request.getParameter("no-words"); 1690 1691 if (StringUtils.isNotBlank(noWords)) 1692 { 1693 StringBuilder allWords = new StringBuilder(); 1694 for (String word : StringUtils.split(noWords)) 1695 { 1696 String escapedWord = ClientUtils.escapeQueryChars(word); 1697 if (allWords.length() > 0) 1698 { 1699 allWords.append(' '); 1700 } 1701 allWords.append('+').append(escapedWord); 1702 } 1703 1704 // queries.add(new NotQuery(new FullTextQuery(allWords.toString(), 1705 // language, Operator.SEARCH))); 1706 1707 queries.add(new NotQuery(new FullTextQuery(allWords.toString(), language, Operator.SEARCH, true))); 1708 } 1709 } 1710 1711 /** 1712 * Add the content type query 1713 * @param queries The queries 1714 * @param request The request 1715 */ 1716 protected void addContentTypeQuery(Collection<Query> queries, Request request) 1717 { 1718 Collection<String> cTypes = new ArrayList<>(getContentTypes(request)); 1719 1720 // Resource is handled in the "document types" filter. 1721 cTypes.remove("resource"); 1722 1723 if (!cTypes.isEmpty()) 1724 { 1725 Query notAPageQuery = new DocumentTypeQuery(TYPE_PAGE, false); 1726 Query pageWithContentOfTypesQuery = new PageContentQuery(new ContentTypeQuery(cTypes)); 1727 // If document is not a page, it will match this fq (it will be filtered if necessary in the "document types" filter, see #getDocumentTypesQuery()) 1728 // If document is a page, then one of its contents must match one of the given content types 1729 queries.add(new OrQuery(notAPageQuery, pageWithContentOfTypesQuery)); 1730 } 1731 } 1732 1733 /** 1734 * Add the tags query 1735 * @param queries The queries 1736 * @param request The request 1737 */ 1738 protected void addTagsQuery(Collection<Query> queries, Request request) 1739 { 1740 String size = request.getParameter("tags-size"); 1741 if (!StringUtils.isEmpty(size)) 1742 { 1743 @SuppressWarnings("unchecked") 1744 Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); 1745 if (parentContext == null) 1746 { 1747 parentContext = Collections.emptyMap(); 1748 } 1749 1750 boolean isStrictSearch = parameters.getParameterAsBoolean("strict-search-on-tags", true); 1751 1752 int nbCat = Integer.parseInt(size); 1753 for (int i = 1; i < nbCat + 1; i++) 1754 { 1755 String[] tags = request.getParameterValues("tags-" + i); 1756 if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals(""))) 1757 { 1758 List<Query> tagQueries = new ArrayList<>(); 1759 1760 for (String tag : tags) 1761 { 1762 String[] values = StringUtils.split(tag, ','); 1763 1764 tagQueries.add(new TagQuery(Operator.EQ, !isStrictSearch, values)); 1765 } 1766 1767 queries.add(new PageContentQuery(new OrQuery(tagQueries))); 1768 } 1769 } 1770 } 1771 } 1772 1773 /** 1774 * Add the pages query 1775 * @param queries The queries 1776 * @param request The request 1777 */ 1778 protected void addPagesQuery(Collection<Query> queries, Request request) 1779 { 1780 List<Query> pageQueries = new ArrayList<>(); 1781 1782 String[] pages = request.getParameterValues("pages"); 1783 if (pages != null && pages.length > 0 && !(pages.length == 1 && pages[0].equals(""))) 1784 { 1785 for (String pageIds : pages) 1786 { 1787 for (String pageId : StringUtils.split(pageIds, ",")) 1788 { 1789 pageQueries.add(new PageQuery(pageId, true)); 1790 } 1791 } 1792 } 1793 1794 queries.add(new OrQuery(pageQueries)); 1795 } 1796 1797 /** 1798 * Add the query on start and end dates 1799 * @param queries The queries 1800 * @param request The request 1801 */ 1802 protected void addDateQuery(Collection<Query> queries, Request request) 1803 { 1804 String startDateId = parameters.getParameter("startDate", ""); 1805 String endDateId = parameters.getParameter("endDate", ""); 1806 1807 String startDateStr = request.getParameter("startDate"); 1808 String endDateStr = request.getParameter("endDate"); 1809 1810 // We check if startDateStr < endDateStr. If not, we swap. 1811 if (StringUtils.isNotBlank(endDateStr) && StringUtils.isNotBlank(startDateStr) && startDateStr.compareTo(endDateStr) > 0) 1812 { 1813 String dateAux = startDateStr; 1814 startDateStr = endDateStr; 1815 endDateStr = dateAux; 1816 } 1817 1818 String startPropId = startDateId.equals("last-modified") ? SolrFieldNames.LAST_MODIFIED : startDateId; 1819 String endPropId = endDateId.equals("last-modified") ? SolrFieldNames.LAST_MODIFIED : endDateId; 1820 1821 if (StringUtils.isNotBlank(endDateStr)) 1822 { 1823 LocalDate endDate = _toDate(endDateStr); 1824 // Join for returning page documents 1825 queries.add(new PageContentQuery(new DateQuery(startPropId, Operator.LE, endDate))); 1826 } 1827 1828 if (StringUtils.isNotBlank(startDateStr)) 1829 { 1830 LocalDate startDate = _toDate(startDateStr); 1831 1832 if (startDate != null) 1833 { 1834 // Two cases are possible : 1835 // An event could have an end date (query 1) 1836 // If not, the start date is also the end date (query 2) 1837 List<Query> endDateQueries = new ArrayList<>(); 1838 1839 if (StringUtils.isNotBlank(endPropId)) 1840 { 1841 endDateQueries.add(new DateQuery(endPropId, Operator.GE, startDate)); 1842 } 1843 endDateQueries.add(new DateQuery(startPropId, Operator.GE, startDate)); 1844 1845 // Join for returning page documents 1846 queries.add(new PageContentQuery(new OrQuery(endDateQueries))); 1847 } 1848 } 1849 } 1850 1851 private LocalDate _toDate(String dateStr) 1852 { 1853 try 1854 { 1855 return LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE); 1856 } 1857 catch (DateTimeParseException e) 1858 { 1859 getLogger().error("Invalid date format " + dateStr, e); 1860 } 1861 return null; 1862 } 1863 1864 private List<QueryAdapterFOSearch> _getSortedListQueryAdapter() 1865 { 1866 return _queryAdapterFOSearchEP.getExtensionsIds() 1867 .stream() 1868 .map(_queryAdapterFOSearchEP::getExtension) 1869 .collect(Collectors.toList()); 1870 } 1871 1872 private ModelItem _getModelItemFromContentTypes(Collection<String> contentTypeIds, String attributePath) 1873 { 1874 if (contentTypeIds.isEmpty()) 1875 { 1876 return null; 1877 } 1878 1879 try 1880 { 1881 String[] contentTypeIdsAsArray = contentTypeIds.toArray(new String[contentTypeIds.size()]); 1882 return _contentTypesHelper.getModelItem(attributePath, contentTypeIdsAsArray, new String[0]); 1883 } 1884 catch (UndefinedItemPathException e) 1885 { 1886 // No such attribute exists, retrieve null 1887 return null; 1888 } 1889 } 1890 1891 static class ContentTypeSearchField implements SearchField 1892 { 1893 @Override 1894 public String getSortField() 1895 { 1896 return PAGE_CONTENT_TYPES; 1897 } 1898 1899 @Override 1900 public String getName() 1901 { 1902 return PAGE_CONTENT_TYPES; 1903 } 1904 1905 @Override 1906 public String getFacetField() 1907 { 1908 return PAGE_CONTENT_TYPES + "_s_dv"; 1909 } 1910 1911 @Override 1912 public boolean isJoined() 1913 { 1914 return false; 1915 } 1916 1917 @Override 1918 public List<String> getJoinedPaths() 1919 { 1920 return Collections.emptyList(); 1921 } 1922 } 1923 1924}