001/* 002 * Copyright 2015 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 */ 016package org.ametys.web.frontoffice; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.time.ZonedDateTime; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.Date; 026import java.util.Enumeration; 027import java.util.List; 028import java.util.Locale; 029import java.util.Map; 030import java.util.Map.Entry; 031import java.util.Objects; 032import java.util.Set; 033import java.util.regex.Pattern; 034import java.util.stream.Collectors; 035 036import org.apache.avalon.framework.context.Context; 037import org.apache.avalon.framework.context.ContextException; 038import org.apache.avalon.framework.context.Contextualizable; 039import org.apache.avalon.framework.service.ServiceException; 040import org.apache.avalon.framework.service.ServiceManager; 041import org.apache.cocoon.ProcessingException; 042import org.apache.cocoon.environment.ObjectModelHelper; 043import org.apache.cocoon.environment.Request; 044import org.apache.cocoon.generation.ServiceableGenerator; 045import org.apache.cocoon.xml.AttributesImpl; 046import org.apache.cocoon.xml.XMLUtils; 047import org.apache.commons.lang.StringUtils; 048import org.apache.excalibur.xml.sax.SAXParser; 049import org.apache.solr.client.solrj.util.ClientUtils; 050import org.apache.tika.Tika; 051import org.slf4j.Logger; 052import org.xml.sax.InputSource; 053import org.xml.sax.SAXException; 054 055import org.ametys.cms.content.RichTextHandler; 056import org.ametys.cms.contenttype.ContentType; 057import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 058import org.ametys.cms.contenttype.ContentTypesHelper; 059import org.ametys.cms.data.RichText; 060import org.ametys.cms.data.type.ModelItemTypeConstants; 061import org.ametys.cms.repository.Content; 062import org.ametys.cms.search.SearchField; 063import org.ametys.cms.search.SearchResults; 064import org.ametys.cms.search.Sort; 065import org.ametys.cms.search.content.ContentSearchHelper; 066import org.ametys.cms.search.query.DocumentTypeQuery; 067import org.ametys.cms.search.query.OrQuery; 068import org.ametys.cms.search.query.Query; 069import org.ametys.cms.search.solr.SearcherFactory.Searcher; 070import org.ametys.cms.tag.TagProviderExtensionPoint; 071import org.ametys.cms.transformation.URIResolverExtensionPoint; 072import org.ametys.core.file.TikaProvider; 073import org.ametys.core.util.DateUtils; 074import org.ametys.core.util.FilenameUtils; 075import org.ametys.core.util.LambdaUtils; 076import org.ametys.core.util.URIUtils; 077import org.ametys.plugins.explorer.resources.Resource; 078import org.ametys.plugins.repository.AmetysObject; 079import org.ametys.plugins.repository.AmetysObjectIterable; 080import org.ametys.plugins.repository.AmetysObjectResolver; 081import org.ametys.plugins.repository.AmetysRepositoryException; 082import org.ametys.plugins.repository.UnknownAmetysObjectException; 083import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 084import org.ametys.runtime.i18n.I18nizableText; 085import org.ametys.runtime.model.ElementDefinition; 086import org.ametys.runtime.model.ModelItem; 087import org.ametys.runtime.model.View; 088import org.ametys.runtime.model.ViewHelper; 089import org.ametys.runtime.model.type.DataContext; 090import org.ametys.web.WebConstants; 091import org.ametys.web.frontoffice.FrontOfficeSearcherFactory.QueryFacet; 092import org.ametys.web.indexing.solr.SolrWebFieldNames; 093import org.ametys.web.repository.page.Page; 094import org.ametys.web.repository.page.Page.PageType; 095import org.ametys.web.repository.page.Zone; 096import org.ametys.web.repository.page.ZoneItem; 097import org.ametys.web.repository.page.ZoneItem.ZoneType; 098import org.ametys.web.repository.site.Site; 099import org.ametys.web.repository.site.SiteManager; 100 101/** 102 * Abstract class for solr search 103 */ 104public abstract class AbstractSearchGenerator extends ServiceableGenerator implements Contextualizable, SolrWebFieldNames 105{ 106 /** The name of the facet.query testing the pageResources */ 107 public static final String DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME = "isPageResource"; 108 109 /** Textfield pattern */ 110 protected static final Pattern _TEXTFIELD_PATTERN = Pattern.compile("^[^?*].*$"); 111 112 /** The {@link ContentType} manager */ 113 protected ContentTypeExtensionPoint _cTypeExtPt; 114 /** The sites manager */ 115 protected SiteManager _siteManager; 116 /** The cocoon context */ 117 protected org.apache.cocoon.environment.Context _context; 118 /** The tag extension point */ 119 protected TagProviderExtensionPoint _tagExtPt; 120 /** The Ametys resolver */ 121 protected AmetysObjectResolver _resolver; 122 /** The helper to handler content types */ 123 protected ContentTypesHelper _contentTypesHelper; 124 /** The uri resolver extension point */ 125 protected URIResolverExtensionPoint _uriResolverEP; 126 /** The searcher factory */ 127 protected FrontOfficeSearcherFactory _searcherFactory; 128 /** The content searcher */ 129 protected ContentSearchHelper _searchHelper; 130 131 @Override 132 public void service(ServiceManager smanager) throws ServiceException 133 { 134 super.service(smanager); 135 _cTypeExtPt = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 136 _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE); 137 _tagExtPt = (TagProviderExtensionPoint) smanager.lookup(TagProviderExtensionPoint.ROLE); 138 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 139 _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE); 140 _uriResolverEP = (URIResolverExtensionPoint) smanager.lookup(URIResolverExtensionPoint.ROLE); 141 _searcherFactory = (FrontOfficeSearcherFactory) smanager.lookup(FrontOfficeSearcherFactory.ROLE); 142 _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE); 143 } 144 145 @Override 146 public void contextualize(Context context) throws ContextException 147 { 148 _context = (org.apache.cocoon.environment.Context) context.get(org.apache.cocoon.Constants.CONTEXT_ENVIRONMENT_CONTEXT); 149 } 150 151 @Override 152 public void generate() throws IOException, SAXException, ProcessingException 153 { 154 Request request = ObjectModelHelper.getRequest(objectModel); 155 156 String currentSiteName = null; 157 String lang = null; 158 Page page = (Page) request.getAttribute(WebConstants.REQUEST_ATTR_PAGE); 159 if (page != null) 160 { 161 currentSiteName = page.getSiteName(); 162 lang = page.getSitemapName(); 163 } 164 else 165 { 166 currentSiteName = parameters.getParameter("siteName", request.getParameter("siteName")); 167 lang = parameters.getParameter("lang", request.getParameter("lang")); 168 } 169 170 int pageIndex = getPageIndex(request); 171 // TODO Rename to maxResults 172 int maxResults = parameters.getParameterAsInteger("offset", 10); 173 int start = (pageIndex - 1) * maxResults; 174 175 String[] sites = request.getParameterValues("sites"); 176 List<String> siteNames = new ArrayList<>(); 177 if (sites != null && sites.length > 0 && !(sites.length == 1 && sites[0].equals(""))) 178 { 179 for (String site : sites) 180 { 181 siteNames.add(site); 182 } 183 } 184 else 185 { 186 siteNames.add(currentSiteName); 187 } 188 189 contentHandler.startDocument(); 190 191 AttributesImpl attrs = new AttributesImpl(); 192 attrs.addCDATAAttribute("site", currentSiteName); 193 attrs.addCDATAAttribute("lang", lang); 194 195 XMLUtils.startElement(contentHandler, "search", attrs); 196 197 saxServiceIdentifiers(); 198 saxAdditionalInfos(); 199 200 // The search url 201 XMLUtils.createElement(contentHandler, "url", page != null ? lang + "/" + page.getPathInSitemap() + ".html" : lang + "/_plugins/" + currentSiteName + "/" + lang + "/service/search-pages.html"); 202 203 // Display the form and results on same page? 204 String searchMode = getSearchMode(); 205 XMLUtils.createElement(contentHandler, "search-mode", searchMode); 206 207 try 208 { 209 SearchResults<AmetysObject> searchResults = null; 210 boolean submit = request.getParameter("submit-form") != null; 211 boolean criteriaOnly = "criteria-only".equals(getSearchMode()); 212 if (submit && isInputValid() && !criteriaOnly) 213 { 214 searchResults = search(request, siteNames, lang, pageIndex, start, maxResults); 215 } 216 else if (!getFacets(request).isEmpty()) 217 { 218 searchResults = search(request, siteNames, lang, pageIndex, start, maxResults, false); 219 } 220 221 saxFormParameters(request, searchResults, start, maxResults, currentSiteName, lang); 222 } 223 catch (IllegalArgumentException e) 224 { 225 getLogger().error("The search field is invalid", e); 226 XMLUtils.createElement(contentHandler, "illegal-textfield"); 227 saxPagination(0, start, maxResults); 228 } 229 catch (Exception e) 230 { 231 getLogger().error("Unable to search", e); 232 saxPagination(0, start, maxResults); 233 } 234 235 XMLUtils.endElement(contentHandler, "search"); 236 contentHandler.endDocument(); 237 } 238 239 /** 240 * Get the zone item 241 * @param request The request 242 * @return the zone item 243 */ 244 protected ZoneItem getZoneItem(Request request) 245 { 246 ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM); 247 if (zoneItem != null) 248 { 249 return zoneItem; 250 } 251 252 String zoneItemId = parameters.getParameter("zoneItemId", request.getParameter("zone-item-id")); 253 if (StringUtils.isNotEmpty(zoneItemId)) 254 { 255 try 256 { 257 return _resolver.resolveById(zoneItemId); 258 } 259 catch (UnknownAmetysObjectException e) 260 { 261 return null; 262 } 263 } 264 265 return null; 266 } 267 268 /** 269 * Search 270 * @param request the request 271 * @param siteNames The name of the sites to search in 272 * @param language The language code to search 273 * @param pageIndex the page index 274 * @param start The offset for search results 275 * @param maxResults The maximum number of results 276 * @return The search results 277 * @throws Exception If an error occurred during search 278 */ 279 protected SearchResults<AmetysObject> search(Request request, Collection<String> siteNames, String language, int pageIndex, int start, int maxResults) throws Exception 280 { 281 return search(request, siteNames, language, pageIndex, start, maxResults, true); 282 } 283 284 /** 285 * Search 286 * @param request the request 287 * @param siteNames The name of the sites to search in 288 * @param language The language code to search 289 * @param pageIndex the page index 290 * @param start The offset for search results 291 * @param maxResults The maximum number of results 292 * @param saxResults false to not sax results 293 * @return The search results 294 * @throws Exception If an error occurred during search 295 */ 296 protected SearchResults<AmetysObject> search(Request request, Collection<String> siteNames, String language, int pageIndex, int start, int maxResults, boolean saxResults) throws Exception 297 { 298 // Retrieve current workspace 299 String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 300 301 SearchResults<AmetysObject> results = null; 302 303 try 304 { 305 // Query 306 Query queryObject = getQuery(request, siteNames, language); 307 308 // Filter queries 309 Collection<Query> filterQueries = getFilterQueries(request, siteNames, language); 310 // Document types query 311 Collection<String> documentTypes = getDocumentTypes(request); 312 Query documentTypesQuery = getDocumentTypesQuery(documentTypes); 313 314 // Get first sort field 315 Sort sort = getSortField(request); 316 saxSort(sort); 317 318 Searcher searcher = _searcherFactory.create() 319 .withQuery(queryObject) 320 .withFilterQueries(filterQueries) 321 .addFilterQuery(documentTypesQuery) 322 .withLimits(0, Integer.MAX_VALUE) 323 .withSort(getPrimarySortFields(request)) 324 .addSort(sort) 325 .setCheckRights(_checkRights()); 326 327 _additionalSearchProcessing(searcher); 328 329 // Facets 330 Collection<SearchField> facets = getFacets(request).values().stream().map(f -> f.getSearchField()).collect(Collectors.toList()); 331 if (!facets.isEmpty()) 332 { 333 searcher.withFacets(facets) 334 .withFacetValues(getFacetValues(request, siteNames, language)); 335 } 336 337 try 338 { 339 results = searcher.searchWithFacets(); 340 } 341 catch (Exception e) 342 { 343 getLogger().error("An error occured with Solr query", e); 344 } 345 346 if (saxResults) 347 { 348 // SAX results 349 AttributesImpl atts = new AttributesImpl(); 350 long total = results != null ? results.getResults().getSize() : 0; 351 atts.addCDATAAttribute("total", String.valueOf(total)); 352 XMLUtils.startElement(contentHandler, "hits", atts); 353 if (results != null) 354 { 355 saxHits(results, start, maxResults); 356 } 357 XMLUtils.endElement(contentHandler, "hits"); 358 359 // SAX pagination 360 saxPagination(total, start, maxResults); 361 } 362 } 363 finally 364 { 365 // Restore context 366 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp); 367 } 368 369 return results; 370 } 371 372 /** 373 * SAX sort 374 * @param sort the current sort 375 * @throws SAXException SAXException If an error occurs while SAXing 376 */ 377 protected void saxSort(Sort sort) throws SAXException 378 { 379 XMLUtils.createElement(contentHandler, sort == null || sort.getField() == null ? "sort-by-score" : "sort-by-" + sort.getField()); 380 } 381 382 /** 383 * Allow to perform other searcher configuration 384 * @param searcher The searcher that will be used for the search operation 385 */ 386 protected void _additionalSearchProcessing(Searcher searcher) 387 { 388 // Nothing by default 389 } 390 391 /** 392 * Build the search query to have results matching at least one of the given document types 393 * @param documentTypes The document types 394 * @return The document type query 395 */ 396 protected Query getDocumentTypesQuery(Collection<String> documentTypes) 397 { 398 List<Query> queries = documentTypes.stream() 399 .map(t -> "\"" + ClientUtils.escapeQueryChars(t) + "\"") 400 .map(DocumentTypeQuery::new) 401 .collect(Collectors.toList()); 402 return new OrQuery(queries); 403 } 404 405 /** 406 * Get the document types. 407 * @param request The request. 408 * @return the document types to search. 409 */ 410 protected abstract Collection<String> getDocumentTypes(Request request); 411 412 /** 413 * Get the sort field 414 * @param request The request 415 * @return The sort field or null to sort by score 416 */ 417 protected abstract Sort getSortField(Request request); 418 419 /** 420 * Get the primary sort fields 421 * @param request The request 422 * @return the list additional sort fields or empty list. 423 */ 424 protected abstract List<Sort> getPrimarySortFields(Request request); 425 426 /** 427 * SAX the search parameters from the request parameters 428 * @param request The request 429 * @param searchResults The search results 430 * @param start The start index 431 * @param offset The number of results 432 * @param siteName The current site name 433 * @param lang The current language 434 * @throws SAXException If an error occurs while SAXing 435 */ 436 protected void saxFormParameters (Request request, SearchResults<AmetysObject> searchResults, int start, int offset, String siteName, String lang) throws SAXException 437 { 438 XMLUtils.startElement(contentHandler, "form"); 439 440 XMLUtils.startElement(contentHandler, "fields"); 441 saxFormFields(request, siteName, lang); 442 XMLUtils.endElement(contentHandler, "fields"); 443 444 saxFacets(request, searchResults, siteName, lang); 445 446 boolean submit = request.getParameter("submit-form") != null; 447 if (submit) 448 { 449 if (isInputValid()) 450 { 451 XMLUtils.startElement(contentHandler, "values"); 452 453 XMLUtils.createElement(contentHandler, "start", String.valueOf(start)); 454 XMLUtils.createElement(contentHandler, "offset", String.valueOf(offset)); 455 456 saxFormValues(request, start, offset); 457 458 XMLUtils.endElement(contentHandler, "values"); 459 } 460 } 461 XMLUtils.endElement(contentHandler, "form"); 462 } 463 464 /** 465 * SAX the form search criteria 466 * @param request The request 467 * @param siteName The current site name 468 * @param lang The current language 469 * @throws SAXException if an error occurs while SAXing 470 */ 471 protected abstract void saxFormFields(Request request, String siteName, String lang) throws SAXException; 472 473 /** 474 * SAX the facets results 475 * @param request The request 476 * @param searchResults The search result 477 * @param siteName The site name 478 * @param lang The language 479 * @throws SAXException if an error occurred while saxing 480 */ 481 protected void saxFacets(Request request, SearchResults<AmetysObject> searchResults, String siteName, String lang) throws SAXException 482 { 483 XMLUtils.startElement(contentHandler, "facets"); 484 485 Map<String, Map<String, Integer>> facetResults = searchResults != null ? searchResults.getFacetResults() : Collections.EMPTY_MAP; 486 if (!facetResults.isEmpty()) 487 { 488 Map<String, FacetField> facets = getFacets(request); 489 490 for (String fieldName : facets.keySet()) 491 { 492 if (facetResults.containsKey(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + fieldName)) 493 { 494 Map<String, Integer> values = facetResults.get(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + fieldName); 495 496 AttributesImpl attr = new AttributesImpl(); 497 attr.addCDATAAttribute("name", fieldName); 498 attr.addCDATAAttribute("total", String.valueOf(values.values().stream().mapToInt(Integer::intValue).sum())); 499 XMLUtils.startElement(contentHandler, "facet", attr); 500 501 facets.get(fieldName).getLabel().toSAX(contentHandler, "label"); 502 503 Set<Entry<String, Integer>> entrySet = values.entrySet(); 504 for (Entry<String, Integer> entry : entrySet) 505 { 506 AttributesImpl valueAttrs = new AttributesImpl(); 507 valueAttrs.addCDATAAttribute("value", entry.getKey()); 508 valueAttrs.addCDATAAttribute("count", Integer.toString(entry.getValue())); 509 510 XMLUtils.startElement(contentHandler, "item", valueAttrs); 511 facets.get(fieldName).getFacetLabel(entry.getKey(), new Locale(lang)).toSAX(contentHandler); 512 XMLUtils.endElement(contentHandler, "item"); 513 } 514 515 XMLUtils.endElement(contentHandler, "facet"); 516 } 517 518 } 519 } 520 521 XMLUtils.endElement(contentHandler, "facets"); 522 } 523 524 /** 525 * SAX the form search criteria values 526 * @param request The request 527 * @param start The start index 528 * @param offset The number of results 529 * @throws SAXException if an error occurs while SAXing 530 */ 531 protected abstract void saxFormValues (Request request, int start, int offset) throws SAXException; 532 533 /** 534 * Get the query from request parameters 535 * @param request The request 536 * @param siteNames The site names. 537 * @param language The language 538 * @return The query object. 539 * @throws IllegalArgumentException If the search field is invalid. 540 */ 541 protected abstract Query getQuery(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException; 542 543 /** 544 * Get the filter queries from the request parameters. 545 * @param request The request. 546 * @param siteNames The site names. 547 * @param language The language. 548 * @return A collection of filter queries. 549 * @throws IllegalArgumentException If a search field is invalid. 550 */ 551 protected abstract Collection<Query> getFilterQueries(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException; 552 553 /** 554 * Template methods to disable/enable the processing of the facets during the search. 555 * @return <code>true</code> to enable facets 556 */ 557 protected boolean useFacets() 558 { 559 return parameters.getParameterAsBoolean("facets", false); 560 } 561 562 /** 563 * Get the facets from request parameters 564 * @param request The request 565 * @return The facet fields 566 * @throws IllegalArgumentException If the search field is invalid. 567 */ 568 protected abstract Map<String, FacetField> getFacets(Request request) throws IllegalArgumentException; 569 570 /** 571 * Get the facet.queries 572 * @param request The request 573 * @return The facet.queries 574 * @throws IllegalArgumentException If the search field is invalid. 575 */ 576 protected abstract Set<QueryFacet> getQueryFacets(Request request); 577 578 /** 579 * Get the facet values 580 * @param request The request 581 * @param siteNames The site names 582 * @param language The language 583 * @return The facet values 584 */ 585 protected abstract Map<String, List<String>> getFacetValues(Request request, Collection<String> siteNames, String language); 586 587 /** 588 * Get the facet.query values 589 * @param request The request 590 * @return The facet.query values 591 */ 592 protected abstract Collection<String> getQueryFacetValues(Request request); 593 594 /** 595 * SAX the result hits 596 * @param results The search results. 597 * @param start The start index 598 * @param maxResults The number of results to generate. 599 * @throws SAXException If an error occurs while SAXing 600 * @throws IOException If there is a low-level IO error 601 */ 602 protected abstract void saxHits(SearchResults<AmetysObject> results, int start, int maxResults) throws SAXException, IOException; 603 604 /** 605 * SAX a hit of type page. 606 * @param score The score of the page 607 * @param maxScore The maximum score of the search results 608 * @param page The page 609 * @throws SAXException If an error occurs while SAXing 610 */ 611 protected void saxPageHit(float score, float maxScore, Page page) throws SAXException 612 { 613 int percent = Math.min(Math.round(score * 100f / maxScore), 100); 614 615 XMLUtils.startElement(contentHandler, "hit"); 616 XMLUtils.createElement(contentHandler, "score", Float.toString(score)); 617 XMLUtils.createElement(contentHandler, "percent", Integer.toString(percent)); 618 XMLUtils.createElement(contentHandler, "title", page.getTitle()); 619 620 _saxPageContents(page); 621 622 XMLUtils.createElement(contentHandler, "type", "page"); 623 XMLUtils.createElement(contentHandler, "uri", page.getSitemap().getName() + "/" + page.getPathInSitemap()); 624 625 _saxLastModifiedDate(page); 626 _saxLastValidationDate(page); 627 628 String siteName = page.getSiteName(); 629 if (siteName != null) 630 { 631 Site site = _siteManager.getSite(siteName); 632 XMLUtils.createElement(contentHandler, "siteName", siteName); 633 XMLUtils.createElement(contentHandler, "siteTitle", site.getTitle()); 634 String url = site.getUrl(); 635 if (url != null) 636 { 637 XMLUtils.createElement(contentHandler, "siteUrl", url); 638 } 639 } 640 641 saxAdditionalInfosOnPageHit(page); 642 643 XMLUtils.endElement(contentHandler, "hit"); 644 } 645 646 private void _saxPageContents(Page page) throws SAXException 647 { 648 if (page.getType() == PageType.CONTAINER) 649 { 650 for (Zone zone : page.getZones()) 651 { 652 AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems(); 653 for (ZoneItem zoneItem : zoneItems) 654 { 655 if (zoneItem.getType() == ZoneType.CONTENT) 656 { 657 // Content 658 Content content = zoneItem.getContent(); 659 saxContent(content.getId(), "index", new Locale(page.getSitemapName())); 660 } 661 } 662 } 663 } 664 } 665 666 private void _saxLastModifiedDate(Page page) throws SAXException 667 { 668 ZonedDateTime lastModified = null; 669 670 if (page.getType() == PageType.CONTAINER) 671 { 672 for (Zone zone : page.getZones()) 673 { 674 AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems(); 675 for (ZoneItem zoneItem : zoneItems) 676 { 677 switch (zoneItem.getType()) 678 { 679 case SERVICE: 680 // A service has no last modification date 681 break; 682 case CONTENT: 683 ZonedDateTime contentLastModified = zoneItem.getContent().getLastModified(); 684 685 if (contentLastModified != null && (lastModified == null || contentLastModified.isAfter(lastModified))) 686 { 687 // Keep the latest modification date 688 lastModified = contentLastModified; 689 } 690 break; 691 default: 692 break; 693 } 694 } 695 } 696 } 697 if (lastModified != null) 698 { 699 XMLUtils.createElement(contentHandler, "lastModified", DateUtils.zonedDateTimeToString(lastModified)); 700 } 701 } 702 703 private void _saxLastValidationDate(Page page) throws SAXException 704 { 705 ZonedDateTime lastValidated = null; 706 707 if (page.getType() == PageType.CONTAINER) 708 { 709 for (Zone zone : page.getZones()) 710 { 711 AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems(); 712 for (ZoneItem zoneItem : zoneItems) 713 { 714 switch (zoneItem.getType()) 715 { 716 case SERVICE: 717 // A service has no last validation date 718 break; 719 case CONTENT: 720 ZonedDateTime contentLastValidation = zoneItem.getContent().getLastValidationDate(); 721 722 if (contentLastValidation != null && (lastValidated == null || contentLastValidation.isAfter(lastValidated))) 723 { 724 // Keep the latest validation date 725 lastValidated = contentLastValidation; 726 } 727 break; 728 default: 729 break; 730 } 731 } 732 } 733 } 734 if (lastValidated != null) 735 { 736 XMLUtils.createElement(contentHandler, "lastValidation", DateUtils.zonedDateTimeToString(lastValidated)); 737 } 738 } 739 740 /** 741 * SAX additional information on page hit 742 * @param page the page 743 * @throws SAXException if something goes wrong when saxing the information 744 */ 745 protected void saxAdditionalInfosOnPageHit(Page page) throws SAXException 746 { 747 // Nothing to do here. 748 } 749 750 /** 751 * SAX a hit of type "resource". 752 * @param score The score of the page 753 * @param maxScore The maximum score of the search results 754 * @param resource The resource 755 * @throws SAXException If an error occurs while SAXing 756 */ 757 protected void saxResourceHit(float score, float maxScore, Resource resource) throws SAXException 758 { 759 int percent = Math.min(Math.round(score * 100f / maxScore), 100); 760 761 String filename = resource.getName(); 762 763 XMLUtils.startElement(contentHandler, "hit"); 764 XMLUtils.createElement(contentHandler, "score", Float.toString(score)); 765 XMLUtils.createElement(contentHandler, "percent", Integer.toString(percent)); 766 XMLUtils.createElement(contentHandler, "filename", filename); 767 XMLUtils.createElement(contentHandler, "title", StringUtils.substringBeforeLast(resource.getName(), ".")); 768 XMLUtils.createElement(contentHandler, "id", resource.getId()); 769 770 String dcDescription = resource.getDCDescription(); 771 String excerpt = _getResourceExcerpt(resource); 772 if (StringUtils.isNotBlank(dcDescription)) 773 { 774 XMLUtils.createElement(contentHandler, "excerpt", dcDescription); 775 } 776 else if (StringUtils.isNotBlank(excerpt)) 777 { 778 XMLUtils.createElement(contentHandler, "excerpt", excerpt + "..."); 779 } 780 781 XMLUtils.createElement(contentHandler, "type", "resource"); 782 783 Page page = _getResourcePage(resource); 784 if (page != null) 785 { 786 String pageUri = page.getSitemapName() + "/" + page.getPathInSitemap(); 787 String encodedPath = FilenameUtils.encodePath(resource.getResourcePath()); 788 789 String uri = URIUtils.encodeURI(pageUri + "/_attachment" + encodedPath, Collections.singletonMap("download", "true")); 790 XMLUtils.createElement(contentHandler, "uri", uri); 791 } 792 793 XMLUtils.createElement(contentHandler, "mime-types", resource.getMimeType()); 794 _saxSize(resource.getLength()); 795 _saxIcon(filename); 796 797 Date lastModified = resource.getLastModified(); 798 if (lastModified != null) 799 { 800 XMLUtils.createElement(contentHandler, "lastModified", DateUtils.dateToString(lastModified)); 801 } 802 if (page != null) 803 { 804 Site site = page.getSite(); 805 XMLUtils.createElement(contentHandler, "siteName", site.getName()); 806 XMLUtils.createElement(contentHandler, "siteTitle", site.getTitle()); 807 XMLUtils.createElement(contentHandler, "siteUrl", site.getUrl()); 808 } 809 810 XMLUtils.endElement(contentHandler, "hit"); 811 } 812 813 private String _getResourceExcerpt(Resource resource) 814 { 815 try (InputStream is = resource.getInputStream()) 816 { 817 TikaProvider tikaProvider = (TikaProvider) manager.lookup(TikaProvider.ROLE); 818 Tika tika = tikaProvider.getTika(); 819 String value = tika.parseToString(is); 820 if (StringUtils.isNotBlank(value)) 821 { 822 int summaryEndIndex = value.lastIndexOf(' ', 200); 823 if (summaryEndIndex == -1) 824 { 825 summaryEndIndex = value.length(); 826 } 827 return value.substring(0, summaryEndIndex) + (summaryEndIndex != value.length() ? "…" : ""); 828 } 829 } 830 catch (Exception e) 831 { 832 getLogger().error("Unable to index resource at " + resource.getPath(), e); 833 } 834 return null; 835 } 836 837 private Page _getResourcePage(Resource resource) 838 { 839 if (resource != null) 840 { 841 AmetysObject parent = resource.getParent(); 842 while (parent != null) 843 { 844 if (parent instanceof Page) 845 { 846 // We have gone up to the page 847 return (Page) parent; 848 } 849 parent = parent.getParent(); 850 } 851 } 852 853 return null; 854 } 855 856 /** 857 * SAX elements for pagination 858 * @param totalHits The total number of result 859 * @param start The start index of search 860 * @param offset The max number of results per page 861 * @throws SAXException SAXException If an error occurs while SAXing 862 */ 863 protected void saxPagination(long totalHits, int start, int offset) throws SAXException 864 { 865 int nbPages = (int) Math.ceil((double) totalHits / (double) offset); 866 867 AttributesImpl atts = new AttributesImpl(); 868 atts.addCDATAAttribute("total", String.valueOf(nbPages)); // Number of pages 869 atts.addCDATAAttribute("start", String.valueOf(start)); // Index of the first hit 870 atts.addCDATAAttribute("end", start + offset > totalHits ? String.valueOf(totalHits) : String.valueOf(start + offset)); // Index of the last hit 871 872 XMLUtils.startElement(contentHandler, "pagination", atts); 873 874 for (int i = 0; i < nbPages; i++) 875 { 876 AttributesImpl attr = new AttributesImpl(); 877 attr.addAttribute("", "index", "index", "CDATA", String.valueOf(i + 1)); 878 attr.addAttribute("", "start", "start", "CDATA", String.valueOf(i * offset)); 879 XMLUtils.createElement(contentHandler, "page", attr); 880 } 881 882 XMLUtils.endElement(contentHandler, "pagination"); 883 } 884 885 /** 886 * Get the page index 887 * @param request The request 888 * @return The page index 889 */ 890 protected int getPageIndex(Request request) 891 { 892 Enumeration paramNames = request.getParameterNames(); 893 while (paramNames.hasMoreElements()) 894 { 895 String param = (String) paramNames.nextElement(); 896 if (param.startsWith("page-")) 897 { 898 return Integer.parseInt(param.substring("page-".length())); 899 } 900 } 901 return 1; 902 } 903 904 private void _saxIcon(String filename) throws SAXException 905 { 906 int index = filename.lastIndexOf('.'); 907 String extension = filename.substring(index + 1); 908 909 XMLUtils.createElement(contentHandler, "icon", "plugins/explorer/icon-medium/" + extension + ".png"); 910 } 911 912 private void _saxSize(long size) throws SAXException 913 { 914 org.ametys.core.util.StringUtils.toReadableDataSize(size).toSAX(contentHandler, "size"); 915 } 916 917 /** 918 * Generate the service identifiers: service group ID, ZoneItem ID, ... 919 * @throws SAXException if an error occurs SAXing data. 920 * @throws IOException if an error occurs SAXing data. 921 * @throws ProcessingException if a processing error occurs. 922 */ 923 protected void saxServiceIdentifiers() throws SAXException, IOException, ProcessingException 924 { 925 Request request = ObjectModelHelper.getRequest(objectModel); 926 ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM); 927 String serviceGroupId = parameters.getParameter("service-group-id", ""); 928 929 // The service group ID. 930 if (StringUtils.isNotEmpty(serviceGroupId)) 931 { 932 XMLUtils.createElement(contentHandler, "group-id", serviceGroupId); 933 } 934 935 // Generate the ZoneItem ID if it exists. 936 if (zoneItem != null) 937 { 938 AttributesImpl atts = new AttributesImpl(); 939 atts.addCDATAAttribute("id", zoneItem.getId()); 940 XMLUtils.createElement(contentHandler, "zone-item", atts); 941 } 942 } 943 944 /** 945 * Generate any additional information. 946 * @throws SAXException if an error occurs SAXing data. 947 * @throws IOException if an error occurs SAXing data. 948 * @throws ProcessingException if a processing error occurs. 949 */ 950 protected void saxAdditionalInfos() throws SAXException, IOException, ProcessingException 951 { 952 // Nothing to do here. 953 } 954 955 /** 956 * Get the search mode. 957 * @return the search mode as a string. 958 */ 959 protected String getSearchMode() 960 { 961 return parameters.getParameter("search-mode", "criteria-and-results"); 962 } 963 964 /** 965 * Check if the input is valid. 966 * @return true if the input is valid, false otherwise. 967 */ 968 protected boolean isInputValid() 969 { 970 boolean valid = true; 971 972 Request request = ObjectModelHelper.getRequest(objectModel); 973 ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM); 974 String serviceGroupId = parameters.getParameter("service-group-id", ""); 975 String requestParamGroupId = StringUtils.defaultString(request.getParameter("submit-form")); 976 String requestParamZoneItemId = StringUtils.defaultString(request.getParameter("zone-item-id")); 977 978 // If the generator is not used as part of a service (zoneItem is null), consider the input valid. 979 if (zoneItem != null) 980 { 981 // If the generator is used as part of a service and both "group ID" 982 // and "zone item ID" are missing from the input, consider the input valid. 983 if (StringUtils.isNotEmpty(requestParamGroupId) || StringUtils.isNotEmpty(requestParamZoneItemId)) 984 { 985 if (StringUtils.isEmpty(serviceGroupId)) 986 { 987 // No specified group ID: check the provided ZoneItem ID. 988 valid = requestParamZoneItemId.equals(zoneItem.getId()); 989 } 990 else 991 { 992 // Check the group ID against the one sent by the form. 993 valid = requestParamGroupId.equals(serviceGroupId); 994 } 995 } 996 } 997 return valid; 998 } 999 1000 /** 1001 * SAX the view of a content if exists 1002 * @param contentId the id of the content 1003 * @param viewName The name of view to sax 1004 * @param defaultLocale The locale to use to sax localized values such as multilingual content or multilingual string. Only use if initial content's language is not null. 1005 * @throws SAXException if an exception occurs while saxing 1006 */ 1007 protected void saxContent(String contentId, String viewName, Locale defaultLocale) throws SAXException 1008 { 1009 try 1010 { 1011 Content content = _resolver.resolveById(contentId); 1012 String[] contentTypes = content.getTypes(); 1013 1014 // content-name 1015 XMLUtils.createElement(contentHandler, "content-name", content.getName()); 1016 1017 // content-types 1018 XMLUtils.startElement(contentHandler, "content-types"); 1019 Arrays.asList(contentTypes).forEach(LambdaUtils.wrapConsumer(cType -> XMLUtils.createElement(contentHandler, "content-type", cType))); 1020 XMLUtils.endElement(contentHandler, "content-types"); 1021 1022 View view = _contentTypesHelper.getView(viewName, contentTypes, content.getMixinTypes()); 1023 if (view != null) 1024 { 1025 Set<String> richTextAttributesPaths = ViewHelper.getModelItemsFromView(view) 1026 .parallelStream() 1027 .filter(Objects::nonNull) 1028 .filter(item -> ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID.equals(item.getType().getId())) 1029 .map(ModelItem::getPath) 1030 .collect(Collectors.toSet()); 1031 1032 for (String richTextAttributePath : richTextAttributesPaths) 1033 { 1034 Object value = content.getValue(richTextAttributePath, true); 1035 if (value != null && value instanceof RichText) 1036 { 1037 saxRichTextExcerpt(richTextAttributePath, contentId, (RichText) value); 1038 } 1039 else if (value instanceof RichText[]) 1040 { 1041 for (Object v : (RichText[]) value) 1042 { 1043 saxRichTextExcerpt(richTextAttributePath, contentId, (RichText) v); 1044 } 1045 } 1046 } 1047 1048 AttributesImpl attrs = new AttributesImpl(); 1049 attrs.addCDATAAttribute("id", content.getId()); 1050 attrs.addCDATAAttribute("name", content.getName()); 1051 if (content.getLanguage() != null) 1052 { 1053 attrs.addCDATAAttribute("language", content.getLanguage()); 1054 } 1055 XMLUtils.startElement(contentHandler, "content", attrs); 1056 1057 content.dataToSAX(contentHandler, view, DataContext.newInstance().withLocale(defaultLocale)); 1058 1059 XMLUtils.endElement(contentHandler, "content"); 1060 } 1061 } 1062 catch (AmetysRepositoryException e) 1063 { 1064 getLogger().error("Cannot sax information about the content " + contentId, e); 1065 } 1066 } 1067 1068 /** 1069 * SAX excerpt for rich text 1070 * @param attributePath The path of attribute 1071 * @param contentId The content id 1072 * @param richText The rich text 1073 */ 1074 protected void saxRichTextExcerpt(String attributePath, String contentId, RichText richText) 1075 { 1076 SAXParser saxParser = null; 1077 try (InputStream is = richText.getInputStream()) 1078 { 1079 RichTextHandler txtHandler = new RichTextHandler(200); 1080 saxParser = (SAXParser) manager.lookup(SAXParser.ROLE); 1081 saxParser.parse(new InputSource(is), txtHandler); 1082 String textValue = txtHandler.getValue(); 1083 if (textValue != null) 1084 { 1085 XMLUtils.createElement(contentHandler, "excerpt", textValue); 1086 } 1087 } 1088 catch (Exception e) 1089 { 1090 getLogger().error("Cannot convert a richtextvalue at path '" + attributePath + "' of content '" + contentId + "'", e); 1091 } 1092 finally 1093 { 1094 manager.release(saxParser); 1095 } 1096 } 1097 1098 /** 1099 * <code>true</code> to check rights during search 1100 * @return <code>true</code> to check rights during search 1101 */ 1102 protected boolean _checkRights() 1103 { 1104 return parameters.getParameterAsBoolean("check-rights", true); 1105 } 1106 1107 /** 1108 * Interface representing a facet field 1109 * 1110 */ 1111 protected interface FacetField 1112 { 1113 /** 1114 * Get the search field for this facet 1115 * @return the search field 1116 */ 1117 public SearchField getSearchField(); 1118 1119 /** 1120 * Get the label of the facet 1121 * @return the label 1122 */ 1123 public I18nizableText getLabel(); 1124 1125 /** 1126 * Get the label for a facet value 1127 * @param value the value 1128 * @param currentLocale the current locale 1129 * @return the label for this value 1130 */ 1131 public I18nizableText getFacetLabel(String value, Locale currentLocale); 1132 } 1133 1134 /** 1135 * Facet field for content types 1136 * 1137 */ 1138 protected class ContentTypeFacetField implements FacetField 1139 { 1140 private SearchField _field; 1141 1142 /** 1143 * Constructor 1144 * @param field The search field 1145 */ 1146 public ContentTypeFacetField(SearchField field) 1147 { 1148 _field = field; 1149 } 1150 1151 @Override 1152 public SearchField getSearchField() 1153 { 1154 return _field; 1155 } 1156 1157 @Override 1158 public I18nizableText getLabel() 1159 { 1160 return new I18nizableText("plugin.web", "PLUGINS_WEB_SERVICE_FRONT_SEARCH_CONTENT_TYPE_FACET_LABEL"); 1161 } 1162 1163 @Override 1164 public I18nizableText getFacetLabel(String value, Locale currentLocale) 1165 { 1166 ContentType cType = _cTypeExtPt.getExtension(value); 1167 if (cType != null) 1168 { 1169 return cType.getLabel(); 1170 } 1171 1172 return new I18nizableText(value); 1173 } 1174 } 1175 1176 /** 1177 * Facet field for an attribute 1178 * 1179 */ 1180 protected class AttributeFacetField implements FacetField 1181 { 1182 private SearchField _field; 1183 private ModelItem _modelItem; 1184 private Logger _logger; 1185 1186 /** 1187 * Constructor 1188 * @param field The search field 1189 * @param modelItem The model item 1190 * @param logger The logger 1191 */ 1192 public AttributeFacetField(SearchField field, ModelItem modelItem, Logger logger) 1193 { 1194 _field = field; 1195 _modelItem = modelItem; 1196 _logger = logger; 1197 } 1198 1199 @Override 1200 public SearchField getSearchField() 1201 { 1202 return _field; 1203 } 1204 1205 @Override 1206 public I18nizableText getLabel() 1207 { 1208 return _modelItem.getLabel(); 1209 } 1210 1211 @Override 1212 public I18nizableText getFacetLabel(String value, Locale currentLocale) 1213 { 1214 try 1215 { 1216 if (_modelItem != null && _modelItem instanceof ElementDefinition) 1217 { 1218 ElementDefinition elementDefinition = (ElementDefinition) _modelItem; 1219 if (elementDefinition.getEnumerator() != null) 1220 { 1221 return elementDefinition.getEnumerator().getEntry(value); 1222 } 1223 else if (elementDefinition.getType().getId().equals(ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID)) 1224 { 1225 Content content = _resolver.resolveById(value); 1226 return new I18nizableText(content.getTitle(currentLocale)); 1227 } 1228 } 1229 } 1230 catch (Exception e) 1231 { 1232 _logger.error("Failed to get label of facet value '" + value + "'. Raw value itself will be used.", e); 1233 } 1234 1235 return new I18nizableText(value); 1236 } 1237 } 1238}