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