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