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