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