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}