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