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.util.DateUtils; 073import org.ametys.core.util.FilenameUtils; 074import org.ametys.core.util.LambdaUtils; 075import org.ametys.core.util.URIUtils; 076import org.ametys.plugins.explorer.resources.Resource; 077import org.ametys.plugins.explorer.resources.metadata.TikaProvider; 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 * Get the searchable fields 606 * @return The fields 607 */ 608 protected abstract Collection<String> getFields(); 609 610 /** 611 * SAX a hit of type page. 612 * @param score The score of the page 613 * @param maxScore The maximum score of the search results 614 * @param page The page 615 * @throws SAXException If an error occurs while SAXing 616 */ 617 protected void saxPageHit(float score, float maxScore, Page page) throws SAXException 618 { 619 int percent = Math.min(Math.round(score * 100f / maxScore), 100); 620 621 XMLUtils.startElement(contentHandler, "hit"); 622 XMLUtils.createElement(contentHandler, "score", Float.toString(score)); 623 XMLUtils.createElement(contentHandler, "percent", Integer.toString(percent)); 624 XMLUtils.createElement(contentHandler, "title", page.getTitle()); 625 626 _saxPageContents(page); 627 628 XMLUtils.createElement(contentHandler, "type", "page"); 629 XMLUtils.createElement(contentHandler, "uri", page.getSitemap().getName() + "/" + page.getPathInSitemap()); 630 631 _saxLastModifiedDate(page); 632 _saxLastValidationDate(page); 633 634 String siteName = page.getSiteName(); 635 if (siteName != null) 636 { 637 Site site = _siteManager.getSite(siteName); 638 XMLUtils.createElement(contentHandler, "siteName", siteName); 639 XMLUtils.createElement(contentHandler, "siteTitle", site.getTitle()); 640 String url = site.getUrl(); 641 if (url != null) 642 { 643 XMLUtils.createElement(contentHandler, "siteUrl", url); 644 } 645 } 646 647 saxAdditionalInfosOnPageHit(page); 648 649 XMLUtils.endElement(contentHandler, "hit"); 650 } 651 652 private void _saxPageContents(Page page) throws SAXException 653 { 654 if (page.getType() == PageType.CONTAINER) 655 { 656 for (Zone zone : page.getZones()) 657 { 658 AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems(); 659 for (ZoneItem zoneItem : zoneItems) 660 { 661 if (zoneItem.getType() == ZoneType.CONTENT) 662 { 663 // Content 664 Content content = zoneItem.getContent(); 665 saxContent(content.getId(), "index", new Locale(page.getSitemapName())); 666 } 667 } 668 } 669 } 670 } 671 672 private void _saxLastModifiedDate(Page page) throws SAXException 673 { 674 ZonedDateTime lastModified = null; 675 676 if (page.getType() == PageType.CONTAINER) 677 { 678 for (Zone zone : page.getZones()) 679 { 680 AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems(); 681 for (ZoneItem zoneItem : zoneItems) 682 { 683 switch (zoneItem.getType()) 684 { 685 case SERVICE: 686 // A service has no last modification date 687 break; 688 case CONTENT: 689 ZonedDateTime contentLastModified = zoneItem.getContent().getLastModified(); 690 691 if (contentLastModified != null && (lastModified == null || contentLastModified.isAfter(lastModified))) 692 { 693 // Keep the latest modification date 694 lastModified = contentLastModified; 695 } 696 break; 697 default: 698 break; 699 } 700 } 701 } 702 } 703 if (lastModified != null) 704 { 705 XMLUtils.createElement(contentHandler, "lastModified", DateUtils.zonedDateTimeToString(lastModified)); 706 } 707 } 708 709 private void _saxLastValidationDate(Page page) throws SAXException 710 { 711 ZonedDateTime lastValidated = null; 712 713 if (page.getType() == PageType.CONTAINER) 714 { 715 for (Zone zone : page.getZones()) 716 { 717 AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems(); 718 for (ZoneItem zoneItem : zoneItems) 719 { 720 switch (zoneItem.getType()) 721 { 722 case SERVICE: 723 // A service has no last validation date 724 break; 725 case CONTENT: 726 ZonedDateTime contentLastValidation = zoneItem.getContent().getLastValidationDate(); 727 728 if (contentLastValidation != null && (lastValidated == null || contentLastValidation.isAfter(lastValidated))) 729 { 730 // Keep the latest validation date 731 lastValidated = contentLastValidation; 732 } 733 break; 734 default: 735 break; 736 } 737 } 738 } 739 } 740 if (lastValidated != null) 741 { 742 XMLUtils.createElement(contentHandler, "lastValidation", DateUtils.zonedDateTimeToString(lastValidated)); 743 } 744 } 745 746 /** 747 * SAX additional information on page hit 748 * @param page the page 749 * @throws SAXException if something goes wrong when saxing the information 750 */ 751 protected void saxAdditionalInfosOnPageHit(Page page) throws SAXException 752 { 753 // Nothing to do here. 754 } 755 756 /** 757 * SAX a hit of type "resource". 758 * @param score The score of the page 759 * @param maxScore The maximum score of the search results 760 * @param resource The resource 761 * @throws SAXException If an error occurs while SAXing 762 */ 763 protected void saxResourceHit(float score, float maxScore, Resource resource) throws SAXException 764 { 765 int percent = Math.min(Math.round(score * 100f / maxScore), 100); 766 767 String filename = resource.getName(); 768 769 XMLUtils.startElement(contentHandler, "hit"); 770 XMLUtils.createElement(contentHandler, "score", Float.toString(score)); 771 XMLUtils.createElement(contentHandler, "percent", Integer.toString(percent)); 772 XMLUtils.createElement(contentHandler, "filename", filename); 773 XMLUtils.createElement(contentHandler, "title", StringUtils.substringBeforeLast(resource.getName(), ".")); 774 XMLUtils.createElement(contentHandler, "id", resource.getId()); 775 776 String dcDescription = resource.getDCDescription(); 777 String excerpt = _getResourceExcerpt(resource); 778 if (StringUtils.isNotBlank(dcDescription)) 779 { 780 XMLUtils.createElement(contentHandler, "excerpt", dcDescription); 781 } 782 else if (StringUtils.isNotBlank(excerpt)) 783 { 784 XMLUtils.createElement(contentHandler, "excerpt", excerpt + "..."); 785 } 786 787 XMLUtils.createElement(contentHandler, "type", "resource"); 788 789 Page page = _getResourcePage(resource); 790 if (page != null) 791 { 792 String pageUri = page.getSitemapName() + "/" + page.getPathInSitemap(); 793 String encodedPath = FilenameUtils.encodePath(resource.getResourcePath()); 794 795 String uri = URIUtils.encodeURI(pageUri + "/_attachment" + encodedPath, Collections.singletonMap("download", "true")); 796 XMLUtils.createElement(contentHandler, "uri", uri); 797 } 798 799 XMLUtils.createElement(contentHandler, "mime-types", resource.getMimeType()); 800 _saxSize(resource.getLength()); 801 _saxIcon(filename); 802 803 Date lastModified = resource.getLastModified(); 804 if (lastModified != null) 805 { 806 XMLUtils.createElement(contentHandler, "lastModified", DateUtils.dateToString(lastModified)); 807 } 808 if (page != null) 809 { 810 Site site = page.getSite(); 811 XMLUtils.createElement(contentHandler, "siteName", site.getName()); 812 XMLUtils.createElement(contentHandler, "siteTitle", site.getTitle()); 813 XMLUtils.createElement(contentHandler, "siteUrl", site.getUrl()); 814 } 815 816 XMLUtils.endElement(contentHandler, "hit"); 817 } 818 819 private String _getResourceExcerpt(Resource resource) 820 { 821 try (InputStream is = resource.getInputStream()) 822 { 823 TikaProvider tikaProvider = (TikaProvider) manager.lookup(TikaProvider.ROLE); 824 Tika tika = tikaProvider.getTika(); 825 String value = tika.parseToString(is); 826 if (StringUtils.isNotBlank(value)) 827 { 828 int summaryEndIndex = value.lastIndexOf(' ', 200); 829 if (summaryEndIndex == -1) 830 { 831 summaryEndIndex = value.length(); 832 } 833 return value.substring(0, summaryEndIndex) + (summaryEndIndex != value.length() ? "…" : ""); 834 } 835 } 836 catch (Exception e) 837 { 838 getLogger().error("Unable to index resource at " + resource.getPath(), e); 839 } 840 return null; 841 } 842 843 private Page _getResourcePage(Resource resource) 844 { 845 if (resource != null) 846 { 847 AmetysObject parent = resource.getParent(); 848 while (parent != null) 849 { 850 if (parent instanceof Page) 851 { 852 // We have gone up to the page 853 return (Page) parent; 854 } 855 parent = parent.getParent(); 856 } 857 } 858 859 return null; 860 } 861 862 /** 863 * SAX elements for pagination 864 * @param totalHits The total number of result 865 * @param start The start index of search 866 * @param offset The max number of results per page 867 * @throws SAXException SAXException If an error occurs while SAXing 868 */ 869 protected void saxPagination(long totalHits, int start, int offset) throws SAXException 870 { 871 int nbPages = (int) Math.ceil((double) totalHits / (double) offset); 872 873 AttributesImpl atts = new AttributesImpl(); 874 atts.addCDATAAttribute("total", String.valueOf(nbPages)); // Number of pages 875 atts.addCDATAAttribute("start", String.valueOf(start)); // Index of the first hit 876 atts.addCDATAAttribute("end", start + offset > totalHits ? String.valueOf(totalHits) : String.valueOf(start + offset)); // Index of the last hit 877 878 XMLUtils.startElement(contentHandler, "pagination", atts); 879 880 for (int i = 0; i < nbPages; i++) 881 { 882 AttributesImpl attr = new AttributesImpl(); 883 attr.addAttribute("", "index", "index", "CDATA", String.valueOf(i + 1)); 884 attr.addAttribute("", "start", "start", "CDATA", String.valueOf(i * offset)); 885 XMLUtils.createElement(contentHandler, "page", attr); 886 } 887 888 XMLUtils.endElement(contentHandler, "pagination"); 889 } 890 891 /** 892 * Get the page index 893 * @param request The request 894 * @return The page index 895 */ 896 protected int getPageIndex(Request request) 897 { 898 Enumeration paramNames = request.getParameterNames(); 899 while (paramNames.hasMoreElements()) 900 { 901 String param = (String) paramNames.nextElement(); 902 if (param.startsWith("page-")) 903 { 904 return Integer.parseInt(param.substring("page-".length())); 905 } 906 } 907 return 1; 908 } 909 910 private void _saxIcon(String filename) throws SAXException 911 { 912 int index = filename.lastIndexOf('.'); 913 String extension = filename.substring(index + 1); 914 915 XMLUtils.createElement(contentHandler, "icon", "plugins/explorer/icon-medium/" + extension + ".png"); 916 } 917 918 private void _saxSize(long size) throws SAXException 919 { 920 org.ametys.core.util.StringUtils.toReadableDataSize(size).toSAX(contentHandler, "size"); 921 } 922 923 /** 924 * Generate the service identifiers: service group ID, ZoneItem ID, ... 925 * @throws SAXException if an error occurs SAXing data. 926 * @throws IOException if an error occurs SAXing data. 927 * @throws ProcessingException if a processing error occurs. 928 */ 929 protected void saxServiceIdentifiers() throws SAXException, IOException, ProcessingException 930 { 931 Request request = ObjectModelHelper.getRequest(objectModel); 932 ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM); 933 String serviceGroupId = parameters.getParameter("service-group-id", ""); 934 935 // The service group ID. 936 if (StringUtils.isNotEmpty(serviceGroupId)) 937 { 938 XMLUtils.createElement(contentHandler, "group-id", serviceGroupId); 939 } 940 941 // Generate the ZoneItem ID if it exists. 942 if (zoneItem != null) 943 { 944 AttributesImpl atts = new AttributesImpl(); 945 atts.addCDATAAttribute("id", zoneItem.getId()); 946 XMLUtils.createElement(contentHandler, "zone-item", atts); 947 } 948 } 949 950 /** 951 * Generate any additional information. 952 * @throws SAXException if an error occurs SAXing data. 953 * @throws IOException if an error occurs SAXing data. 954 * @throws ProcessingException if a processing error occurs. 955 */ 956 protected void saxAdditionalInfos() throws SAXException, IOException, ProcessingException 957 { 958 // Nothing to do here. 959 } 960 961 /** 962 * Get the search mode. 963 * @return the search mode as a string. 964 */ 965 protected String getSearchMode() 966 { 967 return parameters.getParameter("search-mode", "criteria-and-results"); 968 } 969 970 /** 971 * Check if the input is valid. 972 * @return true if the input is valid, false otherwise. 973 */ 974 protected boolean isInputValid() 975 { 976 boolean valid = true; 977 978 Request request = ObjectModelHelper.getRequest(objectModel); 979 ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM); 980 String serviceGroupId = parameters.getParameter("service-group-id", ""); 981 String requestParamGroupId = StringUtils.defaultString(request.getParameter("submit-form")); 982 String requestParamZoneItemId = StringUtils.defaultString(request.getParameter("zone-item-id")); 983 984 // If the generator is not used as part of a service (zoneItem is null), consider the input valid. 985 if (zoneItem != null) 986 { 987 // If the generator is used as part of a service and both "group ID" 988 // and "zone item ID" are missing from the input, consider the input valid. 989 if (StringUtils.isNotEmpty(requestParamGroupId) || StringUtils.isNotEmpty(requestParamZoneItemId)) 990 { 991 if (StringUtils.isEmpty(serviceGroupId)) 992 { 993 // No specified group ID: check the provided ZoneItem ID. 994 valid = requestParamZoneItemId.equals(zoneItem.getId()); 995 } 996 else 997 { 998 // Check the group ID against the one sent by the form. 999 valid = requestParamGroupId.equals(serviceGroupId); 1000 } 1001 } 1002 } 1003 return valid; 1004 } 1005 1006 /** 1007 * SAX the view of a content if exists 1008 * @param contentId the id of the content 1009 * @param viewName The name of view to sax 1010 * @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. 1011 * @throws SAXException if an exception occurs while saxing 1012 */ 1013 protected void saxContent(String contentId, String viewName, Locale defaultLocale) throws SAXException 1014 { 1015 try 1016 { 1017 Content content = _resolver.resolveById(contentId); 1018 String[] contentTypes = content.getTypes(); 1019 1020 // content-name 1021 XMLUtils.createElement(contentHandler, "content-name", content.getName()); 1022 1023 // content-types 1024 XMLUtils.startElement(contentHandler, "content-types"); 1025 Arrays.asList(contentTypes).forEach(LambdaUtils.wrapConsumer(cType -> XMLUtils.createElement(contentHandler, "content-type", cType))); 1026 XMLUtils.endElement(contentHandler, "content-types"); 1027 1028 View view = _contentTypesHelper.getView(viewName, contentTypes, content.getMixinTypes()); 1029 if (view != null) 1030 { 1031 Set<String> richTextAttributesPaths = ViewHelper.getModelItemsFromView(view) 1032 .parallelStream() 1033 .filter(Objects::nonNull) 1034 .filter(item -> ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID.equals(item.getType().getId())) 1035 .map(ModelItem::getPath) 1036 .collect(Collectors.toSet()); 1037 1038 for (String richTextAttributePath : richTextAttributesPaths) 1039 { 1040 Object value = content.getValue(richTextAttributePath, true); 1041 if (value != null && value instanceof RichText) 1042 { 1043 saxRichTextExcerpt(richTextAttributePath, contentId, (RichText) value); 1044 } 1045 else if (value instanceof RichText[]) 1046 { 1047 for (Object v : (RichText[]) value) 1048 { 1049 saxRichTextExcerpt(richTextAttributePath, contentId, (RichText) v); 1050 } 1051 } 1052 } 1053 1054 AttributesImpl attrs = new AttributesImpl(); 1055 attrs.addCDATAAttribute("id", content.getId()); 1056 attrs.addCDATAAttribute("name", content.getName()); 1057 if (content.getLanguage() != null) 1058 { 1059 attrs.addCDATAAttribute("language", content.getLanguage()); 1060 } 1061 XMLUtils.startElement(contentHandler, "content", attrs); 1062 1063 content.dataToSAX(contentHandler, view, DataContext.newInstance().withLocale(defaultLocale)); 1064 1065 XMLUtils.endElement(contentHandler, "content"); 1066 } 1067 } 1068 catch (AmetysRepositoryException e) 1069 { 1070 getLogger().error("Cannot sax information about the content " + contentId, e); 1071 } 1072 } 1073 1074 /** 1075 * SAX excerpt for rich text 1076 * @param attributePath The path of attribute 1077 * @param contentId The content id 1078 * @param richText The rich text 1079 */ 1080 protected void saxRichTextExcerpt(String attributePath, String contentId, RichText richText) 1081 { 1082 SAXParser saxParser = null; 1083 try (InputStream is = richText.getInputStream()) 1084 { 1085 RichTextHandler txtHandler = new RichTextHandler(200); 1086 saxParser = (SAXParser) manager.lookup(SAXParser.ROLE); 1087 saxParser.parse(new InputSource(is), txtHandler); 1088 String textValue = txtHandler.getValue(); 1089 if (textValue != null) 1090 { 1091 XMLUtils.createElement(contentHandler, "excerpt", textValue); 1092 } 1093 } 1094 catch (Exception e) 1095 { 1096 getLogger().error("Cannot convert a richtextvalue at path '" + attributePath + "' of content '" + contentId + "'", e); 1097 } 1098 finally 1099 { 1100 manager.release(saxParser); 1101 } 1102 } 1103 1104 /** 1105 * <code>true</code> to check rights during search 1106 * @return <code>true</code> to check rights during search 1107 */ 1108 protected boolean _checkRights() 1109 { 1110 return parameters.getParameterAsBoolean("check-rights", true); 1111 } 1112 1113 /** 1114 * Interface representing a facet field 1115 * 1116 */ 1117 protected interface FacetField 1118 { 1119 /** 1120 * Get the search field for this facet 1121 * @return the search field 1122 */ 1123 public SearchField getSearchField(); 1124 1125 /** 1126 * Get the label of the facet 1127 * @return the label 1128 */ 1129 public I18nizableText getLabel(); 1130 1131 /** 1132 * Get the label for a facet value 1133 * @param value the value 1134 * @param currentLocale the current locale 1135 * @return the label for this value 1136 */ 1137 public I18nizableText getFacetLabel(String value, Locale currentLocale); 1138 } 1139 1140 /** 1141 * Facet field for content types 1142 * 1143 */ 1144 protected class ContentTypeFacetField implements FacetField 1145 { 1146 private SearchField _field; 1147 1148 /** 1149 * Constructor 1150 * @param field The search field 1151 */ 1152 public ContentTypeFacetField(SearchField field) 1153 { 1154 _field = field; 1155 } 1156 1157 @Override 1158 public SearchField getSearchField() 1159 { 1160 return _field; 1161 } 1162 1163 @Override 1164 public I18nizableText getLabel() 1165 { 1166 return new I18nizableText("plugin.web", "PLUGINS_WEB_SERVICE_FRONT_SEARCH_CONTENT_TYPE_FACET_LABEL"); 1167 } 1168 1169 @Override 1170 public I18nizableText getFacetLabel(String value, Locale currentLocale) 1171 { 1172 ContentType cType = _cTypeExtPt.getExtension(value); 1173 if (cType != null) 1174 { 1175 return cType.getLabel(); 1176 } 1177 1178 return new I18nizableText(value); 1179 } 1180 } 1181 1182 /** 1183 * Facet field for an attribute 1184 * 1185 */ 1186 protected class AttributeFacetField implements FacetField 1187 { 1188 private SearchField _field; 1189 private ModelItem _modelItem; 1190 private Logger _logger; 1191 1192 /** 1193 * Constructor 1194 * @param field The search field 1195 * @param modelItem The model item 1196 * @param logger The logger 1197 */ 1198 public AttributeFacetField(SearchField field, ModelItem modelItem, Logger logger) 1199 { 1200 _field = field; 1201 _modelItem = modelItem; 1202 _logger = logger; 1203 } 1204 1205 @Override 1206 public SearchField getSearchField() 1207 { 1208 return _field; 1209 } 1210 1211 @Override 1212 public I18nizableText getLabel() 1213 { 1214 return _modelItem.getLabel(); 1215 } 1216 1217 @Override 1218 public I18nizableText getFacetLabel(String value, Locale currentLocale) 1219 { 1220 try 1221 { 1222 if (_modelItem != null && _modelItem instanceof ElementDefinition) 1223 { 1224 ElementDefinition elementDefinition = (ElementDefinition) _modelItem; 1225 if (elementDefinition.getEnumerator() != null) 1226 { 1227 return elementDefinition.getEnumerator().getEntry(value); 1228 } 1229 else if (elementDefinition.getType().getId().equals(ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID)) 1230 { 1231 Content content = _resolver.resolveById(value); 1232 return new I18nizableText(content.getTitle(currentLocale)); 1233 } 1234 } 1235 } 1236 catch (Exception e) 1237 { 1238 _logger.error("Failed to get label of facet value '" + value + "'. Raw value itself will be used.", e); 1239 } 1240 1241 return new I18nizableText(value); 1242 } 1243 } 1244}