001/* 002 * Copyright 2010 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.filter; 017 018import java.util.ArrayList; 019import java.util.Calendar; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.Comparator; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Optional; 027import java.util.Set; 028import java.util.function.Function; 029 030import javax.jcr.Node; 031import javax.jcr.Property; 032import javax.jcr.PropertyType; 033import javax.jcr.RepositoryException; 034 035import org.apache.commons.lang.StringUtils; 036import org.apache.jackrabbit.util.ISO9075; 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039 040import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 041import org.ametys.cms.filter.DefaultContentFilter; 042import org.ametys.cms.repository.Content; 043import org.ametys.cms.repository.DefaultContent; 044import org.ametys.cms.repository.LanguageExpression; 045import org.ametys.cms.tag.Tag; 046import org.ametys.cms.tag.TagHelper; 047import org.ametys.cms.tag.TagProviderExtensionPoint; 048import org.ametys.core.util.SizeUtils.ExcludeFromSizeCalculation; 049import org.ametys.plugins.repository.AmetysObjectIterable; 050import org.ametys.plugins.repository.AmetysObjectResolver; 051import org.ametys.plugins.repository.AmetysRepositoryException; 052import org.ametys.plugins.repository.ChainedAmetysObjectIterable; 053import org.ametys.plugins.repository.CollatingUniqueAmetysObjectIterable; 054import org.ametys.plugins.repository.CollectionIterable; 055import org.ametys.plugins.repository.EmptyIterable; 056import org.ametys.plugins.repository.UnknownAmetysObjectException; 057import org.ametys.plugins.repository.jcr.JCRAmetysObject; 058import org.ametys.plugins.repository.query.SortCriteria; 059import org.ametys.plugins.repository.query.SortCriteria.SortCriterion; 060import org.ametys.plugins.repository.query.expression.AndExpression; 061import org.ametys.plugins.repository.query.expression.Expression; 062import org.ametys.plugins.repository.query.expression.Expression.Operator; 063import org.ametys.plugins.repository.query.expression.MetadataExpression; 064import org.ametys.plugins.repository.query.expression.NotExpression; 065import org.ametys.plugins.repository.query.expression.OrExpression; 066import org.ametys.plugins.repository.query.expression.StringExpression; 067import org.ametys.runtime.i18n.I18nizableText; 068import org.ametys.web.repository.content.jcr.DefaultWebContent; 069import org.ametys.web.repository.page.Page; 070import org.ametys.web.repository.site.Site; 071import org.ametys.web.repository.site.SiteManager; 072import org.ametys.web.tags.TagExpression; 073import org.ametys.web.tags.TagExpression.LogicalOperator; 074 075/** 076 * This is the default implementation of a {@link WebContentFilter}. The filter's property are set by setter function and constructor 077 */ 078public class DefaultWebContentFilter extends DefaultContentFilter implements WebContentFilter 079{ 080 private static final Map<String, Function<Content, Object>> __CONTENT_METADATA_FUNCTIONS; 081 static 082 { 083 __CONTENT_METADATA_FUNCTIONS = new HashMap<>(); 084 __CONTENT_METADATA_FUNCTIONS.put(DefaultContent.METADATA_CREATION, content -> content.getCreationDate()); 085 __CONTENT_METADATA_FUNCTIONS.put(DefaultContent.METADATA_FIRST_VALIDATION, content -> content.getFirstValidationDate()); 086 __CONTENT_METADATA_FUNCTIONS.put(DefaultContent.METADATA_LAST_VALIDATION, content -> content.getLastValidationDate()); 087 __CONTENT_METADATA_FUNCTIONS.put(DefaultContent.METADATA_LAST_MAJORVALIDATION, content -> content.getLastMajorValidationDate()); 088 __CONTENT_METADATA_FUNCTIONS.put(DefaultContent.METADATA_MODIFIED, content -> content.getLastModified()); 089 } 090 091 /** The search contexts. */ 092 protected List<FilterSearchContext> _searchContexts; 093 094 /** The mask orphan contents property */ 095 protected boolean _maskOrphan; 096 /** The access limitation */ 097 protected AccessLimitation _accessLimitation; 098 /** The title */ 099 protected I18nizableText _title; 100 /** The description */ 101 protected I18nizableText _description; 102 /** The logger */ 103 @ExcludeFromSizeCalculation 104 protected Logger _logger = LoggerFactory.getLogger(this.getClass()); 105 /** The site manager */ 106 @ExcludeFromSizeCalculation 107 protected SiteManager _siteManager; 108 /** The tag provider */ 109 @ExcludeFromSizeCalculation 110 protected TagProviderExtensionPoint _tagProviderEP; 111 112 /** 113 * Constructor 114 */ 115 public DefaultWebContentFilter () 116 { 117 // Empty 118 _searchContexts = new ArrayList<>(); 119 } 120 121 /** 122 * Creates a new filter 123 * @param id The filter unique identifier 124 * @param resolver The ametys object resolver 125 * @param contentTypeExtensionPoint The extension point for content types 126 * @param siteManager The site manager 127 * @param tagProviderEP The tag provider 128 */ 129 public DefaultWebContentFilter(String id, AmetysObjectResolver resolver, ContentTypeExtensionPoint contentTypeExtensionPoint, SiteManager siteManager, TagProviderExtensionPoint tagProviderEP) 130 { 131 super(id, resolver, contentTypeExtensionPoint); 132 _siteManager = siteManager; 133 _tagProviderEP = tagProviderEP; 134 _searchContexts = new ArrayList<>(); 135 } 136 137 /** 138 * Creates a new filter from copy of another 139 * @param id The filter unique identifier 140 * @param originalFilter The original filter to be copied 141 * @param resolver The ametys object resolver 142 * @param contentTypeExtensionPoint The extension point for content types 143 * @param siteManager The site manager 144 * @param tagProviderEP The tag provider 145 */ 146 public DefaultWebContentFilter(String id, DefaultWebContentFilter originalFilter, AmetysObjectResolver resolver, ContentTypeExtensionPoint contentTypeExtensionPoint, SiteManager siteManager, TagProviderExtensionPoint tagProviderEP) 147 { 148 super(id, originalFilter, resolver, contentTypeExtensionPoint); 149 _siteManager = siteManager; 150 _tagProviderEP = tagProviderEP; 151 _searchContexts = new ArrayList<>(originalFilter._searchContexts); 152 _title = originalFilter._title; 153 _description = originalFilter._description; 154 _maskOrphan = originalFilter._maskOrphan; 155 _accessLimitation = originalFilter._accessLimitation; 156 } 157 158 @Override 159 public I18nizableText getTitle() 160 { 161 return _title; 162 } 163 164 @Override 165 public void setTitle(I18nizableText title) 166 { 167 _title = title; 168 } 169 170 @Override 171 public I18nizableText getDescription() 172 { 173 return _description; 174 } 175 176 @Override 177 public void setDescription(I18nizableText description) 178 { 179 _description = description; 180 } 181 182 @Override 183 public ContextLanguage getContextLanguage() 184 { 185 throw new UnsupportedOperationException("This implementation use multiple context languages, use getSearchContexts instead."); 186 } 187 188 @Override 189 public void setContextLanguage(ContextLanguage context) 190 { 191 throw new UnsupportedOperationException("This implementation use multiple context languages, use setSearchContexts instead."); 192 } 193 194 @Override 195 public void addMetadata(String metadataId, String value) 196 { 197 if (_metadata == null) 198 { 199 _metadata = new HashMap<>(); 200 } 201 _metadata.put(metadataId, value); 202 } 203 204 @Override 205 public List<FilterSearchContext> getSearchContexts() 206 { 207 return Collections.unmodifiableList(_searchContexts); 208 } 209 210 @Override 211 public FilterSearchContext addSearchContext() 212 { 213 FilterSearchContext context = createSeachContext(); 214 _searchContexts.add(context); 215 return context; 216 } 217 218 /** 219 * Create a search context. 220 * @return the created search context. 221 */ 222 protected FilterSearchContext createSeachContext() 223 { 224 return new DefaultFilterSearchContext(_siteManager); 225 } 226 227 @Override 228 public void setMaskOrphanContents(boolean mask) 229 { 230 _maskOrphan = mask; 231 } 232 233 @Override 234 public boolean maskOrphanContents() 235 { 236 return _maskOrphan; 237 } 238 239 @Override 240 public AccessLimitation getAccessLimitation() 241 { 242 // Default to PAGE_ACCESS. 243 return _accessLimitation == null ? AccessLimitation.PAGE_ACCESS : _accessLimitation; 244 } 245 246 @Override 247 public void setAccessLimitation(AccessLimitation limitation) 248 { 249 _accessLimitation = limitation; 250 } 251 252 @Override 253 public AmetysObjectIterable<Content> getMatchingContents(String siteName, String lang, Page page) 254 { 255 List<AmetysObjectIterable<Content>> iterables = new ArrayList<>(); 256 257 for (FilterSearchContext context : _searchContexts) 258 { 259 AmetysObjectIterable<Content> contents = getMatchingContents(siteName, lang, page, context); 260 if (contents != null) 261 { 262 iterables.add(contents); 263 } 264 } 265 266 AmetysObjectIterable<Content> contents = null; 267 268 if (!iterables.isEmpty()) 269 { 270 Comparator<Content> comparator = new ContentComparator(_sortCriteria); 271 contents = new CollatingUniqueAmetysObjectIterable<>(iterables, comparator); 272 } 273 else 274 { 275 if (_logger.isInfoEnabled()) 276 { 277 _logger.info("The filter '" + _id + "' has a null content collection"); 278 } 279 contents = new EmptyIterable<>(); 280 } 281 282 return contents; 283 } 284 285 /** 286 * Get the matching contents for the given search context. 287 * @param siteName the site name. 288 * @param lang the language. 289 * @param page the context page. 290 * @param filterContext the search context. 291 * @return An iterable over matching Contents. 292 */ 293 protected AmetysObjectIterable<Content> getMatchingContents(String siteName, String lang, Page page, FilterSearchContext filterContext) 294 { 295 AmetysObjectIterable<Content> contents = null; 296 297 Context context = filterContext.getContext(); 298 int depth = filterContext.getDepth(); 299 300 Page parentPage = page; 301 if (filterContext.getPageId() != null) 302 { 303 parentPage = _resolver.resolveById(filterContext.getPageId()); 304 try 305 { 306 parentPage = _resolver.resolveById(filterContext.getPageId()); 307 } 308 catch (UnknownAmetysObjectException e) 309 { 310 _logger.warn("the page '" + filterContext.getPageId() + "' can not be found for content's filter of id '" + _id + "'"); 311 Collection<Content> emptyList = Collections.emptyList(); 312 return new CollectionIterable<>(emptyList); 313 } 314 } 315 String xpathQuery = null; 316 if (parentPage == null && context.equals(Context.CHILD_PAGES)) 317 { 318 // Leave null. 319 _logger.warn("The current page can not be null for content's filter of id '" + _id + "'"); 320 } 321 else if (context.equals(Context.CHILD_PAGES) && (depth == 0 || depth == 1)) 322 { 323 xpathQuery = getXPathQuery(siteName, parentPage, depth, filterContext); 324 contents = _resolver.query(xpathQuery); 325 } 326 else if (context.equals(Context.CHILD_PAGES)) 327 { 328 List<AmetysObjectIterable<Content>> itList = new ArrayList<>(); 329 for (int i = 1; i <= depth; i++) 330 { 331 xpathQuery = getXPathQuery(siteName, parentPage, i, filterContext); 332 AmetysObjectIterable<Content> it = _resolver.query(xpathQuery); 333 itList.add(it); 334 } 335 contents = new ChainedAmetysObjectIterable<>(itList); 336 } 337 else 338 { 339 xpathQuery = getXPathQuery(siteName, lang, filterContext); 340 contents = _resolver.query(xpathQuery); 341 } 342 343 return contents; 344 } 345 346 /** 347 * Creates the XPath query corresponding to this filter. 348 * @param siteName The current site name 349 * @param lang The current language 350 * @param filterContext the filter search context. 351 * @return the created XPath query 352 */ 353 public String getXPathQuery(String siteName, String lang, FilterSearchContext filterContext) 354 { 355 List<Expression> exprs = new ArrayList<>(); 356 357 Expression filterExpr = getFilterExpression(); 358 if (filterExpr != null) 359 { 360 exprs.add(filterExpr); 361 } 362 363 Expression contextExpr = filterContext.getFullExpression(siteName, lang); 364 if (contextExpr != null) 365 { 366 exprs.add(contextExpr); 367 } 368 369 Expression expr = exprs.size() > 0 ? new AndExpression(exprs.toArray(new Expression[exprs.size()])) : null; 370 371 return org.ametys.plugins.repository.query.QueryHelper.getXPathQuery(null, "ametys:content", expr, _sortCriteria); 372 } 373 374 /** 375 * Creates the XPath query corresponding to specified {@link Page}, {@link Expression} and {@link SortCriteria}. 376 * @param siteName The current site name 377 * @param page The page where to start the search 378 * @param depth the search depth 379 * @param filterContext the filter search context. 380 * @return the created XPath query 381 */ 382 protected String getXPathQuery(String siteName, Page page, int depth, FilterSearchContext filterContext) 383 { 384 String depthPath = ""; 385 if (depth == 0) 386 { 387 depthPath = "/"; 388 } 389 else if (depth == 1) 390 { 391 depthPath = ""; 392 } 393 else 394 { 395 for (int i = 0; i < depth; i++) 396 { 397 depthPath += "/*"; 398 } 399 } 400 401 // Build the content expression 402 Expression expr = getFilterExpression(siteName, filterContext); 403 404 String xpath = "//element(" + page.getSite().getName() + ", ametys:site)/ametys-internal:sitemaps/" + _encode(page.getSitemap().getName()) 405 + "/" + page.getPathInSitemap() 406 + depthPath 407 + "/element(*, ametys:page)/ametys-internal:zones/*/ametys-internal:zoneItems/*/jcr:deref(@ametys-internal:content, '*')" + (expr != null ? "[" + expr.build() + "]" : "") 408 + (_sortCriteria != null ? (" " + _sortCriteria.build()) : ""); 409 return xpath; 410 411 } 412 413 /** 414 * Get the filter expression for a given search context. 415 * @param siteName The current site name 416 * @param filterContext the filter search context. 417 * @return the filter expression. 418 */ 419 protected Expression getFilterExpression(String siteName, FilterSearchContext filterContext) 420 { 421 Expression expr = super.getFilterExpression(); 422 423 Expression tagExpr = filterContext.getTagsExpression(siteName); 424 if (tagExpr != null) 425 { 426 expr = expr != null ? new AndExpression(expr, tagExpr) : tagExpr; 427 } 428 429 return expr; 430 } 431 432 private static String _encode(String path) 433 { 434 if (path == null || path.length() == 0) 435 { 436 return ""; 437 } 438 439 int pos = path.indexOf("/"); 440 if (pos == -1) 441 { 442 return ISO9075.encode(path); 443 } 444 else 445 { 446 return ISO9075.encode(path.substring(0, pos)) + "/" + _encode(path.substring(pos + 1)); 447 } 448 } 449 450 /** 451 * Default implementation of a filter search context. 452 */ 453 public class DefaultFilterSearchContext implements FilterSearchContext 454 { 455 /** The tags. */ 456 protected List<String> _tags; 457 /** The tags condition*/ 458 protected Condition _tagsCondition; 459 /** The tags auto posting */ 460 protected boolean _tagsAutoPosting; 461 /** The context for search */ 462 protected Context _context; 463 /** The list of sites to match*/ 464 protected List<String> _sites; 465 /** The search depth */ 466 protected int _depth; 467 /** The list of content languages to match */ 468 @SuppressWarnings("hiding") 469 protected ContextLanguage _contextLang; 470 /** The parent page Id */ 471 protected String _pageId; 472 /** The list of content languages to match */ 473 @SuppressWarnings("hiding") 474 @ExcludeFromSizeCalculation 475 protected SiteManager _siteManager; 476 477 /** 478 * Build a DefaultFilterSearchContext. 479 * @param siteManager the site manager. 480 */ 481 public DefaultFilterSearchContext(SiteManager siteManager) 482 { 483 _siteManager = siteManager; 484 _tags = new ArrayList<>(); 485 _sites = new ArrayList<>(); 486 } 487 488 @Override 489 public int getDepth() 490 { 491 return _depth; 492 } 493 494 @Override 495 public List<String> getTags() 496 { 497 return _tags; 498 } 499 500 @Override 501 public Condition getTagsCondition () 502 { 503 return _tagsCondition; 504 } 505 506 @Override 507 public boolean getTagsAutoPosting() 508 { 509 return _tagsAutoPosting; 510 } 511 512 @Override 513 public Context getContext() 514 { 515 return _context; 516 } 517 518 @Override 519 public List<String> getSites() 520 { 521 return _sites; 522 } 523 524 @Override 525 public void addTag(String tag) 526 { 527 _tags.add(tag); 528 } 529 530 @Override 531 public void setTagsCondition(Condition condition) 532 { 533 _tagsCondition = condition; 534 } 535 536 @Override 537 public void setTagsAutoPosting(boolean enable) 538 { 539 _tagsAutoPosting = enable; 540 } 541 542 @Override 543 public void setContext(Context context) 544 { 545 _context = context; 546 } 547 548 @Override 549 public void addSite(String siteName) 550 { 551 _sites.add(siteName); 552 } 553 554 @Override 555 public void setDepth(int depth) 556 { 557 _depth = depth; 558 } 559 560 @Override 561 public ContextLanguage getContextLanguage() 562 { 563 return _contextLang; 564 } 565 566 @Override 567 public void setContextLanguage(ContextLanguage language) 568 { 569 _contextLang = language; 570 } 571 572 @Override 573 public Expression getFullExpression(String siteName, String language) 574 { 575 List<Expression> exprs = new ArrayList<>(); 576 577 Expression contextExpr = getContextExpression(siteName); 578 if (contextExpr != null) 579 { 580 exprs.add(contextExpr); 581 } 582 583 Expression langExpr = getContextLanguagesExpression(language); 584 if (langExpr != null) 585 { 586 exprs.add(langExpr); 587 } 588 589 Expression sharedExpr = getSharedContentsExpression(siteName); 590 if (sharedExpr != null) 591 { 592 exprs.add(sharedExpr); 593 } 594 595 Expression tagExpr = getTagsExpression(siteName); 596 if (tagExpr != null) 597 { 598 exprs.add(tagExpr); 599 } 600 601 Expression expr = exprs.size() > 0 ? new AndExpression(exprs.toArray(new Expression[exprs.size()])) : null; 602 603 return expr; 604 } 605 606 @Override 607 public Expression getTagsExpression(String siteName) 608 { 609 if (_tags != null && _tags.size() > 0) 610 { 611 List<Expression> tagsExpr = new ArrayList<>(); 612 for (String tagName : _tags) 613 { 614 if (_tagsAutoPosting) 615 { 616 Tag tag = _getTag(siteName, tagName); 617 if (tag != null) 618 { 619 Set<String> descendantNames = TagHelper.getDescendantNames(tag, true); 620 tagsExpr.add(new TagExpression(Operator.EQ, descendantNames.toArray(new String[descendantNames.size()]), LogicalOperator.OR)); 621 } 622 } 623 else 624 { 625 tagsExpr.add(new TagExpression(Operator.EQ, tagName)); 626 } 627 } 628 629 if (_tagsCondition == Condition.OR) 630 { 631 return new OrExpression(tagsExpr.toArray(new Expression[tagsExpr.size()])); 632 } 633 else 634 { 635 return new AndExpression(tagsExpr.toArray(new Expression[tagsExpr.size()])); 636 } 637 } 638 639 return null; 640 } 641 642 /** 643 * Internal tag getter given the search context 644 * @param currentSiteName The name of the current site 645 * @param tagName the name of the tag 646 * @return The tag or null 647 */ 648 protected Tag _getTag(String currentSiteName, String tagName) 649 { 650 Map<String, Object> parameters = new HashMap<>(); 651 652 Tag tag = null; 653 654 switch (_context) 655 { 656 case CURRENT_SITE: 657 case CHILD_PAGES: 658 parameters.put("siteName", currentSiteName); 659 tag = _tagProviderEP.getTag(tagName, parameters); 660 break; 661 case SITES_LIST: 662 if (_sites != null && _sites.size() == 1) 663 { 664 parameters.put("siteName", _sites.get(0)); 665 tag = _tagProviderEP.getTag(tagName, parameters); 666 } 667 break; 668 case SITES: 669 case OTHER_SITES: 670 tag = _tagProviderEP.getTag(tagName, null); 671 break; 672 default: 673 // nothing 674 break; 675 } 676 677 return tag; 678 } 679 680 /** 681 * Get the {@link Expression} associated with the given site context 682 * @param siteName The current site name 683 * @return a {@link Expression} associated with the given site context 684 */ 685 protected Expression getContextExpression(String siteName) 686 { 687 Expression expr = null; 688 689 if (Context.CURRENT_SITE.equals(_context)) 690 { 691 expr = new StringExpression(DefaultWebContent.METADATA_SITE, Operator.EQ, siteName); 692 } 693 else if (Context.OTHER_SITES.equals(_context)) 694 { 695 expr = new StringExpression(DefaultWebContent.METADATA_SITE, Operator.NE, siteName); 696 } 697 else if (Context.SITES_LIST.equals(_context)) 698 { 699 List<Expression> sitesExpr = new ArrayList<>(); 700 if (_sites != null && _sites.size() > 0) 701 { 702 for (String site : _sites) 703 { 704 sitesExpr.add(new StringExpression(DefaultWebContent.METADATA_SITE, Operator.EQ, site)); 705 } 706 } 707 expr = new OrExpression(sitesExpr.toArray(new Expression[sitesExpr.size()])); 708 } 709 else if (Context.NO_SITE.equals(_context)) 710 { 711 expr = new NotExpression(new MetadataExpression(DefaultWebContent.METADATA_SITE)); 712 } 713 714 return expr; 715 } 716 717 /** 718 * Get the expression for shared contents 719 * @param currentSiteName the current site name 720 * @return the expression to aware of privacy of contents 721 */ 722 protected Expression getSharedContentsExpression(String currentSiteName) 723 { 724 if (Context.OTHER_SITES.equals(_context) || Context.SITES.equals(_context)) 725 { 726 return SharedContentsHelper.getSharedContentsExpression(_siteManager.getSite(currentSiteName), _siteManager.getSites()); 727 } 728 else if (Context.SITES_LIST.equals(_context)) 729 { 730 List<Site> sites = new ArrayList<>(); 731 for (String siteName : _sites) 732 { 733 sites.add(_siteManager.getSite(siteName)); 734 } 735 736 return SharedContentsHelper.getSharedContentsExpression(_siteManager.getSite(currentSiteName), new CollectionIterable<>(sites)); 737 } 738 739 return null; 740 741 } 742 743 /** 744 * Get the {@link Expression} associated with the given language context 745 * @param lang The current language 746 * @return a {@link Expression} associated with the given language context 747 */ 748 protected Expression getContextLanguagesExpression(String lang) 749 { 750 if (lang == null) 751 { 752 return null; 753 } 754 755 Expression expr = null; 756 757 if (ContextLanguage.CURRENT.equals(_contextLang)) 758 { 759 expr = new LanguageExpression(Operator.EQ, lang); 760 } 761 else if (ContextLanguage.OTHERS.equals(_contextLang)) 762 { 763 expr = new LanguageExpression(Operator.NE, lang); 764 } 765 766 return expr; 767 } 768 769 @Override 770 public String getPageId() 771 { 772 return this._pageId; 773 } 774 775 @Override 776 public void setPageId(String pageId) 777 { 778 this._pageId = pageId; 779 } 780 } 781 782 /** 783 * Compares two contents based on a given list of sort criteria. 784 * In jackrabbit, in ascending order, if the first content does not have the wanted value set, 785 * it's considered to be ordered *before* ("less than") the second content (the JCR spec specifies this behavior as "implementation-defined"). 786 */ 787 protected class ContentComparator implements Comparator<Content> 788 { 789 /** The sort criteria. */ 790 protected SortCriteria _sort; 791 792 /** 793 * Build a content comparator from sort criteria. 794 * @param sortCriteria The sort criteria 795 */ 796 public ContentComparator(SortCriteria sortCriteria) 797 { 798 _sort = sortCriteria; 799 } 800 801 @Override 802 public int compare(Content c1, Content c2) 803 { 804 try 805 { 806 for (SortCriterion criterion : _sort.getCriteria()) 807 { 808 boolean ascending = criterion.isAscending(); 809 String dataPath = criterion.getMetadataPath(); 810 String jcrProperty = criterion.getJcrProperty(); 811 boolean normalize = criterion.isNormalizedSort(); 812 813 int compareAsc = 0; 814 815 if (StringUtils.isNotEmpty(dataPath)) 816 { 817 if (__CONTENT_METADATA_FUNCTIONS.containsKey(dataPath)) 818 { 819 compareAsc = _compareMetadataAscending(c1, c2, dataPath, normalize); 820 } 821 else 822 { 823 compareAsc = _compareAttributesAscending(c1, c2, dataPath, normalize); 824 } 825 } 826 else 827 { 828 compareAsc = compareJcrPropertyAscending(c1, c2, jcrProperty, normalize); 829 } 830 831 if (compareAsc != 0) 832 { 833 return ascending ? compareAsc : (0 - compareAsc); 834 } 835 } 836 } 837 catch (RepositoryException e) 838 { 839 _logger.warn("A repository error occurred trying to compare two contents.", e); 840 } 841 catch (AmetysRepositoryException e) 842 { 843 _logger.warn("A repository error occurred trying to compare two contents.", e); 844 } 845 846 return 0; 847 } 848 849 private int _compareAttributesAscending(Content content1, Content content2, String dataPath, boolean normalize) 850 { 851 // If the first content does not have the data set, then it's before ("less than") the second. 852 if (!content1.hasNonEmptyValue(dataPath)) 853 { 854 return -1; 855 } 856 if (!content2.hasNonEmptyValue(dataPath)) 857 { 858 return 1; 859 } 860 861 Object value1 = content1.getValue(dataPath); 862 Object value2 = content2.getValue(dataPath); 863 864 return _compareValuesAscending(value1, value2, normalize); 865 } 866 867 private int _compareMetadataAscending(Content content1, Content content2, String metadataPath, boolean normalize) 868 { 869 Optional<Object> value1 = Optional.ofNullable(__CONTENT_METADATA_FUNCTIONS.get(metadataPath).apply(content1)); 870 Optional<Object> value2 = Optional.ofNullable(__CONTENT_METADATA_FUNCTIONS.get(metadataPath).apply(content2)); 871 872 // If the first content does not have the data set, then it's before ("less than") the second. 873 if (value1.isEmpty()) 874 { 875 return -1; 876 } 877 if (value2.isEmpty()) 878 { 879 return 1; 880 } 881 882 return _compareValuesAscending(value1.get(), value2.get(), normalize); 883 } 884 885 @SuppressWarnings("unchecked") 886 private int _compareValuesAscending(Object value1, Object value2, boolean normalize) 887 { 888 if (normalize && value1 instanceof String && value2 instanceof String) 889 { 890 return ((String) value1).compareToIgnoreCase((String) value2); 891 } 892 else if (value1 instanceof Comparable) 893 { 894 return ((Comparable) value1).compareTo(value2); 895 } 896 else 897 { 898 return 0; 899 } 900 } 901 902 private int compareJcrPropertyAscending(Content c1, Content c2, String jcrProperty, boolean normalize) throws RepositoryException 903 { 904 if (c1 instanceof JCRAmetysObject && c2 instanceof JCRAmetysObject) 905 { 906 Node node1 = ((JCRAmetysObject) c1).getNode(); 907 Node node2 = ((JCRAmetysObject) c2).getNode(); 908 909 // If the first content does not have the property set, then it's before ("less than") the second. 910 if (!node1.hasProperty(jcrProperty)) 911 { 912 return -1; 913 } 914 if (!node2.hasProperty(jcrProperty)) 915 { 916 return 1; 917 } 918 919 Property prop1 = node1.getProperty(jcrProperty); 920 Property prop2 = node2.getProperty(jcrProperty); 921 922 // Compare values depending on theit type. 923 switch (prop1.getType()) 924 { 925 case PropertyType.STRING: 926 String string1 = prop1.getString(); 927 String string2 = prop2.getString(); 928 return normalize ? string1.compareToIgnoreCase(string2) : string1.compareTo(string2); 929 case PropertyType.DATE: 930 Calendar date1 = prop1.getDate(); 931 Calendar date2 = prop2.getDate(); 932 return date1.compareTo(date2); 933 case PropertyType.LONG: 934 Long long1 = prop1.getLong(); 935 Long long2 = prop2.getLong(); 936 return long1.compareTo(long2); 937 case PropertyType.DOUBLE: 938 Double double1 = prop1.getDouble(); 939 Double double2 = prop2.getDouble(); 940 return double1.compareTo(double2); 941 default: 942 // Cannot compare these values, return 0; 943 return 0; 944 } 945 } 946 947 return 0; 948 } 949 950 } 951}