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