001/*
002 *  Copyright 2011 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.plugins.blog;
017
018import java.time.ZonedDateTime;
019import java.util.ArrayList;
020import java.util.Calendar;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.Comparator;
024import java.util.Date;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.Iterator;
028import java.util.LinkedHashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Map.Entry;
032import java.util.Set;
033import java.util.concurrent.TimeUnit;
034
035import org.apache.avalon.framework.activity.Initializable;
036import org.apache.avalon.framework.component.Component;
037import org.apache.avalon.framework.logger.AbstractLogEnabled;
038import org.apache.avalon.framework.service.ServiceException;
039import org.apache.avalon.framework.service.ServiceManager;
040import org.apache.avalon.framework.service.Serviceable;
041
042import org.ametys.cms.ObservationConstants;
043import org.ametys.cms.repository.Content;
044import org.ametys.cms.repository.ContentTypeExpression;
045import org.ametys.cms.repository.LanguageExpression;
046import org.ametys.cms.tag.Tag;
047import org.ametys.cms.tag.TagHelper;
048import org.ametys.cms.tag.TagProviderExtensionPoint;
049import org.ametys.core.cache.AbstractCacheManager;
050import org.ametys.core.cache.Cache;
051import org.ametys.core.observation.Event;
052import org.ametys.core.observation.ObservationManager;
053import org.ametys.core.user.UserIdentity;
054import org.ametys.plugins.blog.posts.PostContentType;
055import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
056import org.ametys.plugins.repository.AmetysObjectIterable;
057import org.ametys.plugins.repository.AmetysObjectResolver;
058import org.ametys.plugins.repository.provider.WorkspaceSelector;
059import org.ametys.plugins.repository.query.QueryHelper;
060import org.ametys.plugins.repository.query.SortCriteria;
061import org.ametys.plugins.repository.query.expression.AndExpression;
062import org.ametys.plugins.repository.query.expression.Expression;
063import org.ametys.plugins.repository.query.expression.Expression.Operator;
064import org.ametys.plugins.repository.query.expression.MetadataExpression;
065import org.ametys.plugins.repository.query.expression.StringExpression;
066import org.ametys.runtime.i18n.I18nizableText;
067import org.ametys.web.WebConstants;
068import org.ametys.web.repository.SiteAwareAmetysObject;
069import org.ametys.web.repository.site.Site;
070import org.ametys.web.repository.site.SiteManager;
071import org.ametys.web.site.SiteConfigurationExtensionPoint;
072
073import com.google.common.collect.Multimap;
074import com.google.common.collect.TreeMultimap;
075
076/**
077 * Blog cache manager.
078 */
079public class BlogCacheManager extends AbstractLogEnabled implements Component, Serviceable, Initializable
080{
081    /** The name of the date attribute of post */
082    public static final String POST_DATE_ATTRIBUTE = "date";
083
084    /** The name of the tags attribute of post */
085    public static final String POST_TAGS_ATTRIBUTE = "tags";
086    
087    /** The avalon role. */
088    public static final String ROLE = BlogCacheManager.class.getName();
089
090    private static final String __LAST_UPDATE_FUTURE_POST_CACHE = ROLE + "$lastUpdate";
091
092    private static final String __POST_BY_DATE_CACHE = ROLE + "$postsByDate";
093    
094    private static final String __POST_BY_TAG_CACHE = ROLE + "$postsByTag";
095    
096    private static final String __ALL_POSTS_CACHE = ROLE + "$allPosts";
097    
098    private static final String __FUTURE_POSTS_CACHE = ROLE + "$futurePosts";
099    
100    /** The ametys object resolver. */
101    protected AmetysObjectResolver _ametysResolver;
102    
103    /** Th tag provider */
104    protected TagProviderExtensionPoint _tagProviderEP;
105    
106    /** The Site manager. */
107    protected SiteManager _siteManager;
108    
109    /** The site configuration extension point */
110    protected SiteConfigurationExtensionPoint _siteConf;
111    
112    /** The blog root page handler. */
113    protected BlogPageHandler _blogRootHandler;
114    
115    /** The workspace selector. */
116    protected WorkspaceSelector _workspaceSelector;
117    
118    /** The Ametys resolver */
119    protected AmetysObjectResolver _resolver;
120    
121    /** The observation manager */
122    protected ObservationManager _observationManager;
123    
124    /** CacheManager used to create and get cache */
125    protected AbstractCacheManager _cacheManager;
126    
127    @Override
128    public void service(ServiceManager serviceManager) throws ServiceException
129    {
130        _ametysResolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
131        _siteManager = (SiteManager) serviceManager.lookup(SiteManager.ROLE);
132        _siteConf = (SiteConfigurationExtensionPoint) serviceManager.lookup(SiteConfigurationExtensionPoint.ROLE);
133        _blogRootHandler = (BlogPageHandler) serviceManager.lookup(BlogPageHandler.ROLE);
134        _workspaceSelector = (WorkspaceSelector) serviceManager.lookup(WorkspaceSelector.ROLE);
135        _tagProviderEP = (TagProviderExtensionPoint) serviceManager.lookup(TagProviderExtensionPoint.ROLE);
136        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
137        _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE);
138        _cacheManager = (AbstractCacheManager) serviceManager.lookup(AbstractCacheManager.ROLE);
139    }
140    
141    @Override
142    public void initialize() throws Exception
143    {
144        _cacheManager.createMemoryCache(__LAST_UPDATE_FUTURE_POST_CACHE, 
145                new I18nizableText("plugin.blog", "PLUGINS_CORE_LAST_UPDATE_FUTURE_POST_CACHE_LABEL"),
146                new I18nizableText("plugin.blog", "PLUGINS_CORE_LAST_UPDATE_FUTURE_POST_CACHE_DESCRIPTION"),
147                true,
148                null);
149        _cacheManager.createMemoryCache(__POST_BY_DATE_CACHE, 
150                new I18nizableText("plugin.blog", "PLUGINS_CORE_CACHE_POST_BY_DATE_LABEL"),
151                new I18nizableText("plugin.blog", "PLUGINS_CORE_CACHE_POST_BY_DATE_DESCRIPTION"),
152                true,
153                null);
154        _cacheManager.createMemoryCache(__POST_BY_TAG_CACHE, 
155                new I18nizableText("plugin.blog", "PLUGINS_CORE_CACHE_POST_BY_TAG_LABEL"),
156                new I18nizableText("plugin.blog", "PLUGINS_CORE_CACHE_POST_BY_TAG_DESCRIPTION"),
157                true,
158                null);
159        _cacheManager.createMemoryCache(__ALL_POSTS_CACHE, 
160                new I18nizableText("plugin.blog", "PLUGINS_CORE_CACHE_ALL_POSTS_LABEL"),
161                new I18nizableText("plugin.blog", "PLUGINS_CORE_CACHE_ALL_POSTS_DESCRIPTION"),
162                true,
163                null);
164        _cacheManager.createMemoryCache(__FUTURE_POSTS_CACHE, 
165                new I18nizableText("plugin.blog", "PLUGINS_CORE_CACHE_FUTURE_POSTS_LABEL"),
166                new I18nizableText("plugin.blog", "PLUGINS_CORE_CACHE_FUTURE_POSTS_DESCRIPTION"),
167                true,
168                null);
169    }
170    
171    /**
172     * Get the years with posts.
173     * @param siteName the site name.
174     * @param language the language.
175     * @param year the year
176     * @return the years with posts as a Set of Integer.
177     */
178    public boolean hasYear(String siteName, String language, int year)
179    {
180        PostDateCache cache = getPostDateCache(siteName, language);
181        
182        return cache.hasYear(year);
183    }
184    
185    /**
186     * Get the years with posts.
187     * @param siteName the site name.
188     * @param language the language.
189     * @return the years with posts as a Set of Integer.
190     */
191    public Set<Integer> getYears(String siteName, String language)
192    {
193        PostDateCache cache = getPostDateCache(siteName, language);
194        
195        return cache.getYears();
196    }
197    
198    /**
199     * Get the years with posts.
200     * @param siteName the site name.
201     * @param language the language.
202     * @param year the year
203     * @param month the month
204     * @return the years with posts as a Set of Integer.
205     */
206    public boolean hasMonth(String siteName, String language, int year, int month)
207    {
208        PostDateCache cache = getPostDateCache(siteName, language);
209        
210        return cache.hasMonth(year, month);
211    }
212    
213    /**
214     * Get the months with posts for a specified year.
215     * @param siteName the site name.
216     * @param language the language.
217     * @param year the year.
218     * @return the months with posts in the specified year as a Set of Integer.
219     */
220    public Set<Integer> getMonths(String siteName, String language, int year)
221    {
222        PostDateCache cache = getPostDateCache(siteName, language);
223        
224        return cache.getMonths(year);
225    }
226    
227    /**
228     * Get the posts for a year-month couple.
229     * @param siteName the site name.
230     * @param language the language.
231     * @param year the year.
232     * @param month the month.
233     * @return the posts in the specified year and month.
234     */
235    public Map<String, Post> getPostsByMonth(String siteName, String language, int year, int month)
236    {
237        PostDateCache cache = getPostDateCache(siteName, language);
238        
239        return cache.getPosts(year, month);
240    }
241    
242    /**
243     * Get the posts for a year-month couple.
244     * @param siteName the site name.
245     * @param language the language.
246     * @param year the year.
247     * @param month the month.
248     * @return the posts in the specified year and month.
249     */
250    public Collection<Post> getSortedPostsByMonth(String siteName, String language, int year, int month)
251    {
252        PostDateCache cache = getPostDateCache(siteName, language);
253        
254        List<Post> sortedPosts = new ArrayList<>(cache.getPosts(year, month).values());
255        
256        Collections.sort(sortedPosts, new ReverseDatePostComparator());
257        
258        return sortedPosts;
259    }
260    
261    /**
262     * Get the posts for a year.
263     * @param siteName the site name.
264     * @param language the language.
265     * @param year the year.
266     * @return the posts in the specified year.
267     */
268    public Map<String, Post> getPostsByYear(String siteName, String language, int year)
269    {
270        PostDateCache cache = getPostDateCache(siteName, language);
271        
272        return cache.getPosts(year);
273    }
274    
275    /**
276     * Get the posts for a year.
277     * @param siteName the site name.
278     * @param language the language.
279     * @param year the year.
280     * @return the posts in the specified year.
281     */
282    public Collection<Post> getSortedPostsByYear(String siteName, String language, int year)
283    {
284        PostDateCache cache = getPostDateCache(siteName, language);
285        
286        List<Post> sortedPosts = new ArrayList<>(cache.getPosts(year).values());
287        
288        Collections.sort(sortedPosts, new ReverseDatePostComparator());
289        
290        return sortedPosts;
291    }
292    
293    /**
294     * Get the posts for a year-month couple.
295     * @param siteName the site name.
296     * @param language the language.
297     * @param year the year.
298     * @param month the month.
299     * @param name the name 
300     * @return the posts in the specified year and month.
301     */
302    public Post getPostByName(String siteName, String language, int year, int month, String name)
303    {
304        PostDateCache cache = getPostDateCache(siteName, language);
305        
306        return cache.getPostByName(year, month, name);
307    }
308    
309    /**
310     * Get the posts for a year-month couple.
311     * @param siteName the site name.
312     * @param language the language.
313     * @param year the year.
314     * @param month the month.
315     * @param id the id of post
316     * @return the posts in the specified year and month.
317     */
318    public boolean hasPost(String siteName, String language, int year, int month, String id)
319    {
320        PostDateCache cache = getPostDateCache(siteName, language);
321        
322        return cache.hasPost(year, month, id);
323    }
324    
325    /**
326     * Get the posts for a year-month couple.
327     * @param siteName the site name.
328     * @param language the language.
329     * @param year the year.
330     * @param month the month.
331     * @param name the name of post
332     * @return the posts in the specified year and month.
333     */
334    public boolean hasPostByName(String siteName, String language, int year, int month, String name)
335    {
336        PostDateCache cache = getPostDateCache(siteName, language);
337        
338        return cache.hasPostByName(year, month, name);
339    }
340    
341    /////////////////////////////////////////////////
342    ///            POSTS BY TAG METHODS           ///
343    /////////////////////////////////////////////////
344    
345    /**
346     * Get the tags for a site and language.
347     * @param siteName The site name
348     * @param language The language
349     * @return the tag list.
350     */
351    public Set<String> getTags(String siteName, String language)
352    {
353        PostTagCache cache = getPostTagCache(siteName, language);
354        
355        return cache.getTags();
356    }
357    
358    /**
359     * Test if a tag exists in the given site and language.
360     * @param siteName The site name
361     * @param language The language
362     * @param tagName The tag name
363     * @return true if the tag exists, false otherwise.
364     */
365    public boolean hasTag(String siteName, String language, String tagName)
366    {
367        PostTagCache cache = getPostTagCache(siteName, language);
368        
369        Map<String, Object> params = new HashMap<>();
370        params.put("siteName", siteName);
371        Tag tag = _tagProviderEP.getTag(tagName, params);
372        Set<String> descendantNamesAndItSelf = TagHelper.getDescendantNames(tag, true);
373        
374        for (String descendantName : descendantNamesAndItSelf)
375        {
376            if (cache.hasTag(descendantName))
377            {
378                return true;
379            }
380        }
381        
382        return false;
383    }
384    
385    /**
386     * Get the posts for a given tag.
387     * @param siteName The site name
388     * @param language The language
389     * @param tagName The tag name
390     * @return the tag posts, indexed by ID.
391     */
392    public Map<String, Post> getPostsByTag(String siteName, String language, String tagName)
393    {
394        PostTagCache cache = getPostTagCache(siteName, language);
395        
396        return cache.getPosts(tagName);
397    }
398    
399    /**
400     * Get the posts for a given tag.
401     * @param siteName The site name
402     * @param language The language
403     * @param tagName The tag name
404     * @return the tag posts, indexed by ID.
405     */
406    public Collection<Post> getSortedPostsByTag(String siteName, String language, String tagName)
407    {
408        return getSortedPostsByTag (siteName, language, tagName, false);
409    }
410    
411    /**
412     * Get the posts for a given tag.
413     * @param siteName The site name
414     * @param language the language
415     * @param tagName the tag name
416     * @param deepSearch true to perform deep search
417     * @return the tag posts, indexed by ID.
418     */
419    public Collection<Post> getSortedPostsByTag(String siteName, String language, String tagName, boolean deepSearch)
420    {
421        PostTagCache cache = getPostTagCache(siteName, language);
422        
423        List<Post> sortedPosts = new ArrayList<>(cache.getPosts(tagName).values());
424        
425        if (deepSearch)
426        {
427            Map<String, Object> contextualParams = new HashMap<>();
428            contextualParams.put("siteName", siteName);
429            
430            Tag tag = _tagProviderEP.getTag(tagName, contextualParams);
431            if (tag != null)
432            {
433                Set<String> descendantsNames = TagHelper.getDescendantNames(tag, false);
434                for (String descendantsName : descendantsNames)
435                {
436                    sortedPosts.addAll(cache.getPosts(descendantsName).values());
437                }
438            }
439        }
440        
441        Collections.sort(sortedPosts, new ReverseDatePostComparator());
442        
443        return sortedPosts;
444    }
445    
446    /**
447     * Test if a given post has a tag, by its ID.
448     * @param siteName The site name
449     * @param language The language
450     * @param tagName The tag name
451     * @param postId The id of post
452     * @return true if the given post has the given tag.
453     */
454    public boolean hasPostByTag(String siteName, String language, String tagName, String postId)
455    {
456        PostTagCache cache = getPostTagCache(siteName, language);
457        
458        return cache.hasPost(tagName, postId);
459    }
460    
461    /**
462     * Get the posts for a year-month couple.
463     * @param siteName The site name
464     * @param language The language
465     * @param tagName The tag name
466     * @param id The id of post
467     * @return the posts in the specified year and month.
468     */
469    public Post getPost(String siteName, String language, String tagName, String id)
470    {
471        PostTagCache cache = getPostTagCache(siteName, language);
472        
473        return cache.getPost(tagName, id);
474    }
475    
476    /**
477     * Get the posts for a year-month couple.
478     * @param siteName The site name
479     * @param language The language
480     * @param tagName The tag name
481     * @param name the name of post
482     * @return the posts in the specified year and month.
483     */
484    public Post getPostByName(String siteName, String language, String tagName, String name)
485    {
486        PostTagCache cache = getPostTagCache(siteName, language);
487        
488        return cache.getPostByName(tagName, name);
489    }
490    
491    /**
492     * Get the posts for a year-month couple.
493     * @param siteName The site name
494     * @param language The language
495     * @param tagName The tag name
496     * @param id The id of post
497     * @return the posts in the specified year and month.
498     */
499    public boolean hasPost(String siteName, String language, String tagName, String id)
500    {
501        PostTagCache cache = getPostTagCache(siteName, language);
502        
503        return cache.hasPost(tagName, id);
504    }
505    
506    /**
507     * Get the posts for a year-month couple.
508     * @param siteName The site name
509     * @param language The language
510     * @param tagName The tag name
511     * @param name the name of post
512     * @return the posts in the specified year and month.
513     */
514    public boolean hasPostByName(String siteName, String language, String tagName, String name)
515    {
516        PostTagCache cache = getPostTagCache(siteName, language);
517        
518        return cache.hasPostByName(tagName, name);
519    }
520    
521    /////////////////////////////////////////////////
522    ///              ALL POSTS METHODS            ///
523    /////////////////////////////////////////////////
524    
525    /**
526     * Get a list of all the posts' IDs.
527     * @param siteName the site name.
528     * @param language the language.
529     * @return all the posts' IDs as a Set.
530     */
531    public Set<String> getPostIds(String siteName, String language)
532    {
533        AllPostCache cache = getAllPostCache(siteName, language);
534        
535        return cache.getPostIds();
536    }
537    
538    /**
539     * Get the exhaustive list of Posts.
540     * @param siteName the site name.
541     * @param language the language.
542     * @return the exhaustive list of Posts, indexed by their ID.
543     */
544    public Map<String, Post> getPosts(String siteName, String language)
545    {
546        AllPostCache cache = getAllPostCache(siteName, language);
547        
548        return cache.getPosts();
549    }
550    
551    /**
552     * Get the exhaustive list of Posts.
553     * @param siteName the site name.
554     * @param language the language.
555     * @return the exhaustive list of Posts, indexed by their ID.
556     */
557    public Collection<Post> getSortedPosts(String siteName, String language)
558    {
559        AllPostCache cache = getAllPostCache(siteName, language);
560        
561        List<Post> sortedPosts = new ArrayList<>(cache.getPosts().values());
562        
563        Collections.sort(sortedPosts, new ReverseDatePostComparator());
564        
565        return sortedPosts;
566    }
567    
568    /**
569     * Test if a post exists, provided its ID.
570     * @param id the post content ID.
571     * @param siteName the site name.
572     * @param language the language.
573     * @return the Post.
574     */
575    public Post getPost(String siteName, String language, String id)
576    {
577        AllPostCache cache = getAllPostCache(siteName, language);
578        
579        return cache.getPost(id);
580    }
581    
582    /**
583     * Test if a post exists, provided its content name.
584     * @param name the post content name.
585     * @param siteName the site name.
586     * @param language the language.
587     * @return true if the post exists.
588     */
589    public Post getPostByName(String siteName, String language, String name)
590    {
591        AllPostCache cache = getAllPostCache(siteName, language);
592        
593        return cache.getPostByName(name);
594    }
595    
596    
597    /**
598     * Test if a post exists, provided its ID.
599     * @param id the post content ID.
600     * @param siteName the site name.
601     * @param language the language.
602     * @return true if the post exists.
603     */
604    public boolean hasPost(String siteName, String language, String id)
605    {
606        AllPostCache cache = getAllPostCache(siteName, language);
607        
608        return cache.hasPost(id);
609    }
610    
611    /**
612     * Test if a post exists, provided its content name.
613     * @param name the post content name.
614     * @param siteName the site name.
615     * @param language the language.
616     * @return true if the post exists.
617     */
618    public boolean hasPostByName(String siteName, String language, String name)
619    {
620        AllPostCache cache = getAllPostCache(siteName, language);
621        
622        return cache.hasPostByName(name);
623    }
624    
625    /**
626     * Add a post content to the cache.
627     * @param siteName The site name
628     * @param language The language
629     * @param postContent The content
630     */
631    public void addPost(String siteName, String language, Content postContent)
632    {
633        PostDateCache dateCache = getPostDateCache(siteName, language);
634        PostTagCache tagCache = getPostTagCache(siteName, language);
635        AllPostCache allCache = getAllPostCache(siteName, language);
636        FuturePostCache futureCache = getFuturePostCache(siteName, language);
637        
638        boolean displayFuturePosts = _getDisplayFuturePost(siteName);
639
640        boolean isFuturePost = _isFuturePost(postContent);
641        if (displayFuturePosts || !isFuturePost)
642        {
643            dispatchPost(dateCache, postContent);
644            dispatchPost(tagCache, postContent);
645            dispatchPost(allCache, postContent);
646        }
647        else // => !displayFuturePosts && isFuturePost
648        {
649            dispatchPost(futureCache, postContent);
650        }
651    }
652    
653    /**
654     * Modify a post content in the cache.
655     * @param siteName The site name
656     * @param language The language
657     * @param postContent The content
658     */
659    public void modifyPost(String siteName, String language, Content postContent)
660    {
661        PostDateCache dateCache = getPostDateCache(siteName, language);
662        PostTagCache tagCache = getPostTagCache(siteName, language);
663        AllPostCache allCache = getAllPostCache(siteName, language);
664        FuturePostCache futureCache = getFuturePostCache(siteName, language);
665        
666        Post post = allCache.getPost(postContent.getId());
667        Post futurePost = futureCache.getPost(postContent.getId());
668        
669        boolean displayFuturePosts = _getDisplayFuturePost(siteName);
670        boolean isFuturePost = _isFuturePost(postContent);
671        
672        if (displayFuturePosts || !isFuturePost)
673        {
674            if (futurePost != null)
675            {
676                removePost(futureCache, futurePost);
677            }
678            
679            if (post == null)
680            {
681                dispatchPost(dateCache, postContent);
682                dispatchPost(tagCache, postContent);
683                dispatchPost(allCache, postContent);
684            }
685            else
686            {
687                modifyPost(dateCache, post, postContent);
688                modifyPost(tagCache, post, postContent);
689                modifyPost(allCache, post, postContent);
690            }
691        }
692        else // => !displayFuturePosts && isFuturePost
693        {
694            if (futurePost == null)
695            {
696                dispatchPost(futureCache, postContent);
697            }
698            else
699            {
700                modifyPost(futureCache, futurePost, postContent);
701            }
702            
703            if (post != null)
704            {
705                removePost(dateCache, post);
706                removePost(tagCache, post);
707                removePost(allCache, post);
708            }
709        }
710    }
711    
712    /**
713     * Validate a post content.
714     * @param siteName The site name
715     * @param language The language
716     * @param postContent The content
717     */
718    public void validatePost(String siteName, String language, Content postContent)
719    {
720        boolean displayFuturePosts = _getDisplayFuturePost(siteName);
721
722        // Get the "live" caches.
723        PostDateCache dateCache = _getPostByDateCache().get(BlogPageElementKey.of(WebConstants.LIVE_WORKSPACE, siteName, language), k-> new PostDateCache());
724        PostTagCache tagCache = _getPostByTagCache().get(BlogPageElementKey.of(WebConstants.LIVE_WORKSPACE, siteName, language), k-> new PostTagCache());
725        AllPostCache allCache = _getAllPosts().get(BlogPageElementKey.of(WebConstants.LIVE_WORKSPACE, siteName, language), k-> new AllPostCache());
726        FuturePostCache futureCache = _getFuturePosts().get(BlogPageElementKey.of(WebConstants.LIVE_WORKSPACE, siteName, language), k-> new FuturePostCache());
727        
728        if (displayFuturePosts || !_isFuturePost(postContent))
729        {
730            // Dispatch the post in the live caches.
731            removePost(futureCache, postContent);
732            
733            dispatchPost(dateCache, postContent);
734            dispatchPost(tagCache, postContent);
735            dispatchPost(allCache, postContent);
736        }
737        else // => !displayFuturePosts && isFuturePost
738        {
739            dispatchPost(futureCache, postContent);
740            
741            removePost(dateCache, postContent);
742            removePost(tagCache, postContent);
743            removePost(allCache, postContent);
744        }
745    }
746    
747    /**
748     * Remove a post content from the cache.
749     * @param siteName The site name
750     * @param language The language
751     * @param postContent The content
752     */
753    public void removePost(String siteName, String language, Content postContent)
754    {
755        PostDateCache dateCache = getPostDateCache(siteName, language);
756        PostTagCache tagCache = getPostTagCache(siteName, language);
757        AllPostCache allCache = getAllPostCache(siteName, language);
758        FuturePostCache futureCache = getFuturePostCache(siteName, language);
759        
760        removePost(dateCache, postContent);
761        removePost(tagCache, postContent);
762        removePost(allCache, postContent);
763        removePost(futureCache, postContent);
764    }
765    
766    /**
767     * Remove a post content from the cache.
768     * @param siteName the site name.
769     * @param postId the post ID.
770     */
771    public void removePostById(String siteName, String postId)
772    {
773        PostSiteLang postWithSiteLang = getPost(siteName, postId);
774        PostSiteLang futurePostWithSiteLang = getFuturePost(siteName, postId);
775        
776        if (postWithSiteLang != null)
777        {
778            Post post = postWithSiteLang.getPost();
779            String language = postWithSiteLang.getLanguage();
780            
781            // Get cache from current workspace
782            PostDateCache dateCache = getPostDateCache(siteName, language);
783            PostTagCache tagCache = getPostTagCache(siteName, language);
784            AllPostCache allCache = getAllPostCache(siteName, language);
785            
786            // Get cache from live workspace
787            PostDateCache workspaceDateCache = _getPostByDateCache().get(BlogPageElementKey.of(WebConstants.LIVE_WORKSPACE, siteName, language), k-> new PostDateCache());
788            PostTagCache workspaceTagCache = _getPostByTagCache().get(BlogPageElementKey.of(WebConstants.LIVE_WORKSPACE, siteName, language), k-> new PostTagCache());
789            AllPostCache workspaceAllCache = _getAllPosts().get(BlogPageElementKey.of(WebConstants.LIVE_WORKSPACE, siteName, language), k-> new AllPostCache());
790            
791            // Update the cache
792            removePost(dateCache, post);
793            removePost(tagCache, post);
794            removePost(allCache, post);
795            
796            removePost(workspaceDateCache, post);
797            removePost(workspaceTagCache, post);
798            removePost(workspaceAllCache, post);
799        }
800        
801        if (futurePostWithSiteLang != null)
802        {
803            Post post = futurePostWithSiteLang.getPost();
804            String language = futurePostWithSiteLang.getLanguage();
805            
806            // Get cache from current workspace
807            FuturePostCache futureCache = getFuturePostCache(siteName, language);
808            
809            // Get cache from live workspace
810            FuturePostCache workspaceFutureCache = _getFuturePosts().get(BlogPageElementKey.of(WebConstants.LIVE_WORKSPACE, siteName, language), k-> new FuturePostCache());
811            
812            // Update the cache
813            removePost(futureCache, post);
814            removePost(workspaceFutureCache, post);
815        }
816    }
817    
818    /**
819     * Clear the caches.
820     */
821    public void clearCaches()
822    {
823        _getPostByDateCache().invalidateAll();
824        _getPostByTagCache().invalidateAll();
825        _getAllPosts().invalidateAll();
826        _getFuturePosts().invalidateAll();
827    }
828    
829    /**
830     * Clear the caches for a given siteName and language.
831     * @param siteName The site name
832     * @param language The language
833     */
834    public void clearCaches(String siteName, String language)
835    {
836        for (BlogPageElementKey key : _getPostByDateCache().asMap().keySet())
837        {
838            PostDateCache cache = _getPostByDateCache().get(BlogPageElementKey.of((String) key.getFields().get(0), siteName, language));
839            cache.clear();
840        }
841
842        for (BlogPageElementKey key : _getPostByTagCache().asMap().keySet())
843        {
844            PostTagCache cache = _getPostByTagCache().get(BlogPageElementKey.of((String) key.getFields().get(0), siteName, language));
845            cache.clear();
846        }
847        
848        for (BlogPageElementKey key : _getAllPosts().asMap().keySet())
849        {
850            AllPostCache cache = _getAllPosts().get(BlogPageElementKey.of((String) key.getFields().get(0), siteName, language));
851            cache.clear();
852        }
853        
854        for (BlogPageElementKey key : _getFuturePosts().asMap().keySet())
855        {
856            FuturePostCache cache = _getFuturePosts().get(BlogPageElementKey.of((String) key.getFields().get(0), siteName, language));
857            cache.clear();
858        }
859    }
860    
861    /**
862     * Get the post date cache.
863     * @param siteName The site name
864     * @param language The language
865     * @return the post date cache for the given site and language.
866     */
867    protected PostDateCache getPostDateCache(String siteName, String language)
868    {
869        // Get the workspace name.
870        String workspaceName = _workspaceSelector.getWorkspace();
871        
872        // Ensure the cache is created and initialized for the site and language.
873        if (!_getPostByDateCache().hasKey(BlogPageElementKey.of(workspaceName, siteName, language)))
874        {
875            initializeCaches(workspaceName, siteName, language);
876        }
877        
878        // Update the future posts cache.
879        _updateFuturePosts(workspaceName, siteName, language);
880        return _getPostByDateCache().get(BlogPageElementKey.of(workspaceName, siteName, language));
881    }
882    
883    /**
884     * Get the post tag cache.
885     * @param siteName The site name
886     * @param language The language
887     * @return the post tag cache for the given site and language.
888     */
889    protected PostTagCache getPostTagCache(String siteName, String language)
890    {
891        // Get the workspace name.
892        String workspaceName = _workspaceSelector.getWorkspace();
893        
894        // Ensure the cache is created and initialized for the site and language.
895        if (!_getPostByTagCache().hasKey(BlogPageElementKey.of(workspaceName, siteName, language)))
896        {
897            initializeCaches(workspaceName, siteName, language);
898        }
899        
900        // Update the future posts cache.
901        _updateFuturePosts(workspaceName, siteName, language);
902        
903        return _getPostByTagCache().get(BlogPageElementKey.of(workspaceName, siteName, language));
904    }
905    
906    /**
907     * Get the exhaustive post cache.
908     * @param siteName The site name
909     * @param language The language
910     * @return the post date cache for the given site and language.
911     */
912    protected AllPostCache getAllPostCache(String siteName, String language)
913    {
914        // Get the workspace name.
915        String workspaceName = _workspaceSelector.getWorkspace();
916        
917        // Ensure the cache is created and initialized for the site and language.
918        if (!_getAllPosts().hasKey(BlogPageElementKey.of(workspaceName, siteName, language)))
919        {
920            initializeCaches(workspaceName, siteName, language);
921        }
922        
923        // Update the future posts cache.
924        _updateFuturePosts(workspaceName, siteName, language);
925        return _getAllPosts().get(BlogPageElementKey.of(workspaceName, siteName, language));
926    }
927        
928    
929    /**
930     * Get the future post cache.
931     * @param siteName the name of the site
932     * @param language the language
933     * @return the post date cache for the given site and language.
934     */
935    protected FuturePostCache getFuturePostCache(String siteName, String language)
936    {
937        // Get the workspace name.
938        String workspaceName = _workspaceSelector.getWorkspace();
939        
940        // Ensure the cache is created and initialized for the site and language.
941        if (!_getFuturePosts().hasKey(BlogPageElementKey.of(workspaceName, siteName, language)))
942        {
943            initializeCaches(workspaceName, siteName, language);
944        }
945        
946        return _getFuturePosts().get(BlogPageElementKey.of(workspaceName, siteName, language));
947    }
948               
949    /**
950     * Initialize all the caches.
951     * @param workspaceName The workspace name
952     * @param siteName The site name
953     * @param language The language
954     */
955    protected void initializeCaches(String workspaceName, String siteName, String language)
956    {
957        PostDateCache dateCache = _getPostByDateCache().get(BlogPageElementKey.of(workspaceName, siteName, language), k-> new PostDateCache());
958        PostTagCache tagCache = _getPostByTagCache().get(BlogPageElementKey.of(workspaceName, siteName, language), k-> new PostTagCache());
959        AllPostCache allCache = _getAllPosts().get(BlogPageElementKey.of(workspaceName, siteName, language), k-> new AllPostCache());
960        FuturePostCache futureCache = _getFuturePosts().get(BlogPageElementKey.of(workspaceName, siteName, language), k-> new FuturePostCache());
961        
962        ContentTypeExpression cTypeExpr = new ContentTypeExpression(Operator.EQ, PostContentType.CONTENT_TYPE);
963        Expression siteExpr = new StringExpression(SiteAwareAmetysObject.METADATA_SITE, Operator.EQ, siteName);
964        Expression languageExpr = new LanguageExpression(Operator.EQ, language);
965        MetadataExpression dateExpr = new MetadataExpression(POST_DATE_ATTRIBUTE);
966        
967        Expression expression = new AndExpression(cTypeExpr, siteExpr, languageExpr, dateExpr);
968        
969        boolean displayFuturePosts = _getDisplayFuturePost(siteName);
970        
971        SortCriteria sort = new SortCriteria();
972        sort.addCriterion(POST_DATE_ATTRIBUTE, false, false);
973        sort.addJCRPropertyCriterion("ametys:lastModified", false, false);
974        
975        String query = QueryHelper.getXPathQuery(null, "ametys:content", expression, sort);
976        
977        try (AmetysObjectIterable<Content> posts = _ametysResolver.query(query);)
978        {
979            for (Content postContent : posts)
980            {
981                if (displayFuturePosts || !_isFuturePost(postContent))
982                {
983                    dispatchPost(dateCache, postContent);
984                    dispatchPost(tagCache, postContent);
985                    dispatchPost(allCache, postContent);
986                }
987                else
988                {
989                    dispatchPost(futureCache, postContent);
990                }
991            }
992        }
993    }
994    
995    private boolean _getDisplayFuturePost(String siteName)
996    {
997        boolean displayFuturePosts = true;
998        if (siteName != null)
999        {
1000            Site site = _siteManager.getSite(siteName);
1001            displayFuturePosts = site.getValue("display-future-posts", false, false);
1002        }
1003        return displayFuturePosts;
1004    }
1005    
1006    private void _updateFuturePosts(String workspaceName, String siteName, String language)
1007    {
1008        Date now = new Date();
1009        
1010        Calendar calendar = Calendar.getInstance();
1011        calendar.setTime(now);
1012
1013        int minutes = calendar.get(Calendar.MINUTE);
1014        
1015        int mod = minutes % 15;
1016        calendar.add(Calendar.MINUTE, -mod);
1017        calendar.set(Calendar.SECOND, 0);   
1018        calendar.set(Calendar.MILLISECOND, 0);
1019        
1020        Date roundedNow = calendar.getTime();
1021        
1022        if (_needUpdateFuturePosts(workspaceName, siteName, language, roundedNow))
1023        {
1024            // Update the last update time
1025            _getLastUpdateFuturePosts().put(BlogPageElementKey.of(workspaceName, siteName, language), roundedNow);
1026            
1027            // Remove past "future posts"
1028            FuturePostCache futureCache = _getFuturePosts().get(BlogPageElementKey.of(workspaceName, siteName, language), k-> new FuturePostCache());
1029            Collection<Post> pastPosts = futureCache.removePastPosts();
1030            
1031            // Notify that the posts are pasts, and must be visible now.
1032            for (Post pastPost : pastPosts)
1033            {
1034                if (_resolver.hasAmetysObjectForId(pastPost.getId()))
1035                {
1036                    Content content = _resolver.resolveById(pastPost.getId());
1037                    
1038                    Map<String, Object> eventParams = new HashMap<>();
1039                    eventParams.put(ObservationConstants.ARGS_CONTENT, content);
1040                    _observationManager.notify(new Event(BlogObservationConstants.EVENT_FUTURE_CONTENT_VISIBLE, new UserIdentity("admin", "admin"), eventParams));
1041                }
1042            }
1043        }
1044    }
1045    
1046    private boolean _needUpdateFuturePosts(String workspaceName, String siteName, String language, Date roundedNow)
1047    {
1048        Date lastUpdate = _getLastUpdateFuturePosts().get(BlogPageElementKey.of(workspaceName, siteName, language));
1049        
1050        if (lastUpdate != null)
1051        {
1052            long duration  = roundedNow.getTime() - lastUpdate.getTime();
1053            long diffInMinutes = TimeUnit.MILLISECONDS.toMinutes(duration);
1054            
1055            return diffInMinutes >= 15;
1056        }
1057        
1058        return true;
1059    }
1060    
1061    private boolean _isFuturePost (Content post)
1062    {
1063        ZonedDateTime postDate = post.getValue(POST_DATE_ATTRIBUTE);
1064        return postDate != null && postDate.isAfter(ZonedDateTime.now());
1065    }
1066    
1067    /**
1068     * Get a post by its site name and id.
1069     * @param siteName the site name.
1070     * @param postId the post ID.
1071     * @return the Post if it exists in the cache, or null if it doesn't.
1072     */
1073    protected PostSiteLang getPost(String siteName, String postId)
1074    {
1075        String workspaceName = _workspaceSelector.getWorkspace();
1076
1077        for (Entry<BlogPageElementKey, AllPostCache> langPosts : _getAllPosts().getAll(BlogPageElementKey.of(workspaceName, siteName)).entrySet())
1078        {
1079            BlogPageElementKey blogPageElementKey = langPosts.getKey();
1080            List<Object> fields = blogPageElementKey.getFields();
1081            if (langPosts.getValue().hasPost(postId))
1082            {
1083                String language = (String) fields.get(2);
1084                Post post = langPosts.getValue().getPost(postId);
1085                
1086                return new PostSiteLang(post, siteName, language);
1087            }
1088        }
1089        
1090        return null;
1091    }
1092    
1093    /**
1094     * Get a future post by its site name and id.
1095     * @param siteName the site name.
1096     * @param postId the post ID.
1097     * @return the Post if it exists in the cache, or null if it doesn't.
1098     */
1099    protected PostSiteLang getFuturePost(String siteName, String postId)
1100    {
1101
1102        String workspaceName = _workspaceSelector.getWorkspace();
1103
1104        
1105        for (Entry<BlogPageElementKey, FuturePostCache> langPosts : _getFuturePosts().getAll(BlogPageElementKey.of(workspaceName, siteName)).entrySet())
1106        {
1107            BlogPageElementKey blogPageElementKey = langPosts.getKey();
1108            List<Object> fields = blogPageElementKey.getFields();
1109            if (langPosts.getValue().hasPost(postId))
1110            {
1111                String language = (String) fields.get(2);
1112                Post post = langPosts.getValue().getPost(postId);
1113                
1114                return new PostSiteLang(post, siteName, language);
1115            }
1116            
1117        }
1118        
1119        return null;
1120    }
1121    
1122    /**
1123     * Dispatch a post content in the post date cache.
1124     * @param dateCache the post date cache.
1125     * @param content the Post content.
1126     */
1127    protected void dispatchPost(PostDateCache dateCache, Content content)
1128    {
1129        ZonedDateTime postDate = content.getValue(POST_DATE_ATTRIBUTE);
1130        
1131        // Dispatch by date.
1132        if (postDate != null)
1133        {
1134            int month = postDate.getMonthValue();
1135            int year = postDate.getYear();
1136            
1137            dateCache.addPost(content, year, month);
1138        }
1139    }
1140    
1141    /**
1142     * Add a post in the post date cache.
1143     * @param dateCache the post date cache.
1144     * @param post the Post to add.
1145     */
1146    protected void dispatchPost(PostDateCache dateCache, Post post)
1147    {
1148        ZonedDateTime postDate = post.getDate();
1149        
1150        // Dispatch by date.
1151        if (postDate != null)
1152        {
1153            int month = postDate.getMonthValue();
1154            int year = postDate.getYear();
1155            
1156            dateCache.addPost(post, year, month);
1157        }
1158    }
1159    
1160    /**
1161     * Dispatch a post content in the post tag cache.
1162     * @param tagCache the post tag cache.
1163     * @param content the Post content.
1164     */
1165    protected void dispatchPost(PostTagCache tagCache, Content content)
1166    {
1167        String[] postTags = content.getValue(POST_TAGS_ATTRIBUTE, false, new String[0]);
1168        
1169        for (String tagName : postTags)
1170        {
1171            tagCache.addPost(content, tagName);
1172        }
1173    }
1174    
1175    /**
1176     * Dispatch a post in the post tag cache.
1177     * @param tagCache the post tag cache.
1178     * @param post the Post to add.
1179     */
1180    protected void dispatchPost(PostTagCache tagCache, Post post)
1181    {
1182        String[] postTags = post.getTags();
1183        
1184        for (String tagName : postTags)
1185        {
1186            tagCache.addPost(post, tagName);
1187        }
1188    }
1189    
1190    /**
1191     * Dispatch a post content in the post cache.
1192     * @param cache the post cache.
1193     * @param content the Post content.
1194     */
1195    protected void dispatchPost(AllPostCache cache, Content content)
1196    {
1197        cache.addPost(content);
1198    }
1199    
1200    /**
1201     * Dispatch a post in the post cache.
1202     * @param cache the post cache.
1203     * @param post the Post content.
1204     */
1205    protected void dispatchPost(AllPostCache cache, Post post)
1206    {
1207        cache.addPost(post);
1208    }
1209    
1210    /**
1211     * Dispatch a post content in the future post cache.
1212     * @param cache the future post cache.
1213     * @param content the Post content.
1214     */
1215    protected void dispatchPost(FuturePostCache cache, Content content)
1216    {
1217        cache.addPost(content);
1218    }
1219    
1220    /**
1221     * Remove a post content.
1222     * @param dateCache the post date cache.
1223     * @param content the Post content.
1224     */
1225    protected void removePost(PostDateCache dateCache, Content content)
1226    {
1227        ZonedDateTime postDate = content.getValue(POST_DATE_ATTRIBUTE);
1228        
1229        // Dispatch by date.
1230        if (postDate != null)
1231        {
1232            int month = postDate.getMonthValue();
1233            int year = postDate.getYear();
1234            
1235            dateCache.removePost(content.getId(), year, month);
1236        }
1237    }
1238    
1239    /**
1240     * Remove a post content.
1241     * @param dateCache the post date cache.
1242     * @param post the Post object to remove.
1243     */
1244    protected void removePost(PostDateCache dateCache, Post post)
1245    {
1246        ZonedDateTime postDate = post.getDate();
1247        
1248        // Dispatch by date.
1249        if (postDate != null)
1250        {
1251            int month = postDate.getMonthValue();
1252            int year = postDate.getYear();
1253            
1254            dateCache.removePost(post.getId(), year, month);
1255        }
1256    }
1257    
1258    /**
1259     * Remove a post content.
1260     * @param cache the post tag cache.
1261     * @param content the Post content.
1262     */
1263    protected void removePost(PostTagCache cache, Content content)
1264    {
1265        String[] postTags = content.getValue(POST_TAGS_ATTRIBUTE, false, new String[0]);
1266        
1267        for (String tagName : postTags)
1268        {
1269            cache.removePost(content.getId(), tagName);
1270        }
1271    }
1272    
1273    /**
1274     * Remove a post content.
1275     * @param cache the post tag cache.
1276     * @param post the Post to remove.
1277     */
1278    protected void removePost(PostTagCache cache, Post post)
1279    {
1280        String[] postTags = post.getTags();
1281        
1282        for (String tagName : postTags)
1283        {
1284            cache.removePost(post.getId(), tagName);
1285        }
1286    }
1287    
1288    /**
1289     * Remove a post content.
1290     * @param cache the post cache.
1291     * @param content the Post content.
1292     */
1293    protected void removePost(AllPostCache cache, Content content)
1294    {
1295        cache.removePost(content);
1296    }
1297    
1298    /**
1299     * Remove a post content.
1300     * @param cache the post cache.
1301     * @param post the Post to remove.
1302     */
1303    protected void removePost(AllPostCache cache, Post post)
1304    {
1305        cache.removePost(post.getId(), post.getName());
1306    }
1307    
1308    /**
1309     * Remove a future post content.
1310     * @param cache the future post cache.
1311     * @param content the Post content to remove.
1312     */
1313    protected void removePost(FuturePostCache cache, Content content)
1314    {
1315        cache.removePost(content);
1316    }
1317    
1318    /**
1319     * Remove a future post content.
1320     * @param cache the future post cache.
1321     * @param post the Post to remove.
1322     */
1323    protected void removePost(FuturePostCache cache, Post post)
1324    {
1325        cache.removePost(post);
1326    }
1327    
1328    /**
1329     * Remove a post content.
1330     * @param dateCache the post date cache.
1331     * @param post the post content
1332     * @param postContent the post Content.
1333     */
1334    protected void modifyPost(PostDateCache dateCache, Post post, Content postContent)
1335    {
1336        ZonedDateTime oldDate = post.getDate();
1337        ZonedDateTime newDate = postContent.getValue(POST_DATE_ATTRIBUTE);
1338        
1339        if (!newDate.equals(oldDate))
1340        {
1341            if (oldDate != null)
1342            {
1343                int month = oldDate.getMonthValue();
1344                int year = oldDate.getYear();
1345                dateCache.removePost(postContent.getId(), year, month);
1346            }
1347
1348            int month = newDate.getMonthValue();
1349            int year = newDate.getYear();
1350            dateCache.addPost(postContent, year, month);
1351        }
1352    }
1353    
1354    /**
1355     * Remove a post content.
1356     * @param cache the post tag cache.
1357     * @param post the post content
1358     * @param postContent the post Content.
1359     */
1360    protected void modifyPost(PostTagCache cache, Post post, Content postContent)
1361    {
1362        String[] oldPostTags = post.getTags();
1363        String[] newPostTags = postContent.getValue(POST_TAGS_ATTRIBUTE, false, new String[0]);
1364        
1365        for (String oldTagName : oldPostTags)
1366        {
1367            cache.removePost(postContent.getId(), oldTagName);
1368        }
1369        
1370        for (String newTagName : newPostTags)
1371        {
1372            cache.addPost(postContent, newTagName);
1373        }
1374    }
1375    
1376    /**
1377     * Modify a post content.
1378     * @param cache the post cache.
1379     * @param post the post content
1380     * @param postContent the post Content.
1381     */
1382    protected void modifyPost(AllPostCache cache, Post post, Content postContent)
1383    {
1384        cache.removePost(postContent);
1385        cache.addPost(postContent);
1386    }
1387    
1388    /**
1389     * Modify a future post content.
1390     * @param cache the post cache.
1391     * @param post the post
1392     * @param postContent the post Content.
1393     */
1394    protected void modifyPost(FuturePostCache cache, Post post, Content postContent)
1395    {
1396        cache.removePost(postContent);
1397        cache.addPost(postContent);
1398    }
1399    
1400    /**
1401     * Class representing a Post in the cache.
1402     */
1403    public static class Post
1404    {
1405        
1406        /** The post content id. */
1407        protected String _id;
1408        
1409        /** The post content name. */
1410        protected String _name;
1411        
1412        /** The post date. */
1413        protected ZonedDateTime _date;
1414        
1415        /** The post tags. */
1416        protected String[] _tags;
1417        
1418        /**
1419         * Get the id.
1420         * @return the id
1421         */
1422        public String getId()
1423        {
1424            return _id;
1425        }
1426
1427        /**
1428         * Set the id.
1429         * @param id the id to set
1430         */
1431        public void setId(String id)
1432        {
1433            this._id = id;
1434        }
1435
1436        /**
1437         * Get the name.
1438         * @return the name
1439         */
1440        public String getName()
1441        {
1442            return _name;
1443        }
1444
1445        /**
1446         * Set the name.
1447         * @param name the name to set
1448         */
1449        public void setName(String name)
1450        {
1451            this._name = name;
1452        }
1453
1454        /**
1455         * Get the tags.
1456         * @return the tags
1457         */
1458        public String[] getTags()
1459        {
1460            return _tags;
1461        }
1462        
1463        /**
1464         * Set the tags.
1465         * @param tags the tags to set
1466         */
1467        public void setTags(String[] tags)
1468        {
1469            this._tags = new String[tags.length];
1470            System.arraycopy(tags, 0, _tags, 0, tags.length);
1471        }
1472        
1473        /**
1474         * Get the date.
1475         * @return the date
1476         */
1477        public ZonedDateTime getDate()
1478        {
1479            return _date;
1480        }
1481        
1482        /**
1483         * Set the date.
1484         * @param date the date to set
1485         */
1486        public void setDate(ZonedDateTime date)
1487        {
1488            this._date = date;
1489        }
1490        
1491        @Override
1492        public int hashCode()
1493        {
1494            return _id.hashCode();
1495        }
1496        
1497        @Override
1498        public boolean equals(Object obj)
1499        {
1500            if (this == obj)
1501            {
1502                return true;
1503            }
1504            if (obj == null)
1505            {
1506                return false;
1507            }
1508            if (getClass() != obj.getClass())
1509            {
1510                return false;
1511            }
1512            Post other = (Post) obj;
1513            if (_id == null)
1514            {
1515                if (other._id != null)
1516                {
1517                    return false;
1518                }
1519            }
1520            else if (!_id.equals(other._id))
1521            {
1522                return false;
1523            }
1524            return true;
1525        }
1526        
1527    }
1528    
1529    /**
1530     * A Post with site name and language.
1531     */
1532    protected static class PostSiteLang extends Post
1533    {
1534        
1535        /** The post. */
1536        protected Post _post;
1537        
1538        /** The post site name. */
1539        protected String _siteName;
1540        
1541        /** The post language. */
1542        protected String _language;
1543        
1544        /**
1545         * Default constructor.
1546         */
1547        public PostSiteLang()
1548        {
1549            // Nothing to do.
1550        }
1551        
1552        /**
1553         * Constructor with all parameters.
1554         * @param post the Post.
1555         * @param siteName the site name.
1556         * @param language the language.
1557         */
1558        public PostSiteLang(Post post, String siteName, String language)
1559        {
1560            super();
1561            this._post = post;
1562            this._siteName = siteName;
1563            this._language = language;
1564        }
1565
1566        /**
1567         * Get the post.
1568         * @return the post
1569         */
1570        public Post getPost()
1571        {
1572            return _post;
1573        }
1574
1575        /**
1576         * Set the post.
1577         * @param post the post to set
1578         */
1579        public void setPost(Post post)
1580        {
1581            this._post = post;
1582        }
1583
1584        /**
1585         * Get the siteName.
1586         * @return the siteName
1587         */
1588        public String getSiteName()
1589        {
1590            return _siteName;
1591        }
1592
1593        /**
1594         * Set the siteName.
1595         * @param siteName the siteName to set
1596         */
1597        public void setSiteName(String siteName)
1598        {
1599            this._siteName = siteName;
1600        }
1601
1602        /**
1603         * Get the language.
1604         * @return the language
1605         */
1606        public String getLanguage()
1607        {
1608            return _language;
1609        }
1610
1611        /**
1612         * Set the language.
1613         * @param language the language to set
1614         */
1615        public void setLanguage(String language)
1616        {
1617            this._language = language;
1618        }
1619        
1620    }
1621    
1622    /**
1623     * Post cache by date.
1624     */
1625    protected static class PostDateCache
1626    {
1627        /** The posts, indexed by year, month and ID. */
1628        private Map<Integer, Map<Integer, Map<String, Post>>> _posts;
1629        
1630        /**
1631         * Construct a PostDateCache instance.
1632         */
1633        public PostDateCache()
1634        {
1635            _posts = new HashMap<>();
1636        }
1637        
1638        /**
1639         * Determines if the year page exists
1640         * @param year the year
1641         * @return true if there are posts for this year
1642         */
1643        public boolean hasYear(int year)
1644        {
1645            return _posts.containsKey(year);
1646        }
1647        
1648        /**
1649         * Get the years with posts.
1650         * @return the years with posts as a Set of Integer.
1651         */
1652        public Set<Integer> getYears()
1653        {
1654            return _posts.keySet();
1655        }
1656        
1657        /**
1658         * Determines if the page month exists in given year
1659         * @param year The year
1660         * @param month  The month
1661         * @return true if there are posts for this year/month couple
1662         */
1663        public boolean hasMonth(int year, int month)
1664        {
1665            boolean hasMonth = false;
1666            
1667            if (_posts.containsKey(year))
1668            {
1669                hasMonth = _posts.get(year).containsKey(month);
1670            }
1671            
1672            return hasMonth;
1673        }
1674        
1675        /**
1676         * Get the months with posts for a specified year.
1677         * @param year the year.
1678         * @return the months with posts in the specified year as a Set of Integer.
1679         */
1680        public Set<Integer> getMonths(int year)
1681        {
1682            Set<Integer> months = new HashSet<>();
1683            
1684            if (_posts.containsKey(year))
1685            {
1686                months = _posts.get(year).keySet();
1687            }
1688            
1689            return months;
1690        }
1691        
1692        /**
1693         * Get the posts for a year couple.
1694         * @param year the year.
1695         * @return the posts in the specified year.
1696         */
1697        public Map<String, Post> getPosts(int year)
1698        {
1699            Map<String, Post> posts = new HashMap<>();
1700            
1701            if (_posts.containsKey(year))
1702            {
1703                Map<Integer, Map<String, Post>> months = _posts.get(year);
1704                
1705                for (Map<String, Post> month : months.values())
1706                {
1707                    posts.putAll(month);
1708                }
1709            }
1710            
1711            return posts;
1712        }
1713        
1714        /**
1715         * Get the posts for a year-month couple.
1716         * @param year the year.
1717         * @param month the month.
1718         * @return the posts in the specified year and month.
1719         */
1720        public Map<String, Post> getPosts(int year, int month)
1721        {
1722            Map<String, Post> posts = new HashMap<>();
1723            
1724            if (_posts.containsKey(year))
1725            {
1726                Map<Integer, Map<String, Post>> months = _posts.get(year);
1727                
1728                if (months.containsKey(month))
1729                {
1730                    posts = months.get(month);
1731                }
1732            }
1733            
1734            return posts;
1735        }
1736        
1737        /**
1738         * Add a post to the cache.
1739         * @param content The content
1740         * @param year the year
1741         * @param month the month
1742         */
1743        public void addPost(Content content, int year, int month)
1744        {
1745            Map<String, Post> posts = getOrCreatePostMap(year, month);
1746            
1747            Post post = new Post();
1748            post.setId(content.getId());
1749            post.setName(content.getName());
1750            post.setDate(content.getValue(POST_DATE_ATTRIBUTE));
1751            post.setTags(content.getValue(POST_TAGS_ATTRIBUTE, false, new String[0]));
1752            
1753            posts.put(post.getId(), post);
1754        }
1755        
1756        /**
1757         * Add a post to the cache.
1758         * @param post the post
1759         * @param year the year of the post
1760         * @param month the month of the post
1761         */
1762        public void addPost(Post post, int year, int month)
1763        {
1764            Map<String, Post> posts = getOrCreatePostMap(year, month);
1765            posts.put(post.getId(), post);
1766        }
1767        
1768        /**
1769         * Remove a post from the cache.
1770         * @param contentId The content id
1771         * @param year the year
1772         * @param month the month
1773         */
1774        public void removePost(String contentId, int year, int month)
1775        {
1776            if (_posts.containsKey(year))
1777            {
1778                Map<Integer, Map<String, Post>> months = _posts.get(year);
1779                
1780                if (months.containsKey(month))
1781                {
1782                    Map<String, Post> posts = months.get(month);
1783                    
1784                    posts.remove(contentId);
1785                    if (posts.isEmpty())
1786                    {
1787                        months.remove(month);
1788                    }
1789                }
1790                
1791                if (months.isEmpty())
1792                {
1793                    _posts.remove(year);
1794                }
1795            }
1796        }
1797        
1798        /**
1799         * Test if the post with the given ID exists for this year and month.
1800         * @param year the year.
1801         * @param month the month.
1802         * @param id the id of post
1803         * @return the posts in the specified year and month.
1804         */
1805        public boolean hasPost(int year, int month, String id)
1806        {
1807            return getPosts(year, month).containsKey(id);
1808        }
1809        
1810        /**
1811         * Get the posts for a year-month couple.
1812         * @param year the year.
1813         * @param month the month.
1814         * @param name the name of post
1815         * @return the posts in the specified year and month.
1816         */
1817        public Post getPostByName(int year, int month, String name)
1818        {
1819            Post post = null;
1820            
1821            Iterator<Post> posts = getPosts(year, month).values().iterator();
1822            while (posts.hasNext() && post == null)
1823            {
1824                Post currentPost = posts.next();
1825                if (currentPost.getName().equals(name))
1826                {
1827                    post = currentPost;
1828                }
1829            }
1830            
1831            return post;
1832        }
1833        
1834        /**
1835         * Get the posts for a year-month couple.
1836         * @param year the year.
1837         * @param month the month.
1838         * @param name the name of post
1839         * @return the posts in the specified year and month.
1840         */
1841        public boolean hasPostByName(int year, int month, String name)
1842        {
1843            boolean hasPost = false;
1844            
1845            Iterator<Post> posts = getPosts(year, month).values().iterator();
1846            while (posts.hasNext() && !hasPost)
1847            {
1848                hasPost = posts.next().getName().equals(name);
1849            }
1850            
1851            return hasPost;
1852        }
1853        
1854        /**
1855         * Clear the cache.
1856         */
1857        public void clear()
1858        {
1859            _posts.clear();
1860        }
1861        
1862        /**
1863         * Get or create the post set for the specified year and month.
1864         * @param year the year.
1865         * @param month the month.
1866         * @return the post set.
1867         */
1868        protected Map<String, Post> getOrCreatePostMap(int year, int month)
1869        {
1870            Map<String, Post> posts;
1871            
1872            Map<Integer, Map<String, Post>> months;
1873            
1874            if (_posts.containsKey(year))
1875            {
1876                months = _posts.get(year);
1877            }
1878            else
1879            {
1880                months = new HashMap<>();
1881                _posts.put(year, months);
1882            }
1883            
1884            if (months.containsKey(month))
1885            {
1886                posts = months.get(month);
1887            }
1888            else
1889            {
1890                posts = new LinkedHashMap<>();
1891                months.put(month, posts);
1892            }
1893            
1894            return posts;
1895        }
1896                                
1897    }
1898    
1899    /**
1900     * Post cache by tag.
1901     */
1902    protected static class PostTagCache
1903    {
1904        
1905        /** The posts, indexed by tag and ID. */
1906        private Map<String, Map<String, Post>> _posts;
1907        
1908        /**
1909         * Construct a PostTagCache instance.
1910         */
1911        public PostTagCache()
1912        {
1913            _posts = new HashMap<>();
1914        }
1915        
1916        /**
1917         * Get the years with posts.
1918         * @param tagId The id of tag
1919         * @return the years with posts as a Set of Integer.
1920         */
1921        public boolean hasTag(String tagId)
1922        {
1923            return _posts.containsKey(tagId);
1924        }
1925        
1926        /**
1927         * Get the years with posts.
1928         * @return the years with posts as a Set of Integer.
1929         */
1930        public Set<String> getTags()
1931        {
1932            return _posts.keySet();
1933        }
1934        
1935        /**
1936         * Get the posts for a year-month couple.
1937         * @param tagId the year.
1938         * @return the posts in the specified year and month.
1939         */
1940        public Map<String, Post> getPosts(String tagId)
1941        {
1942            Map<String, Post> posts = new HashMap<>();
1943            
1944            if (_posts.containsKey(tagId))
1945            {
1946                posts = _posts.get(tagId);
1947            }
1948            
1949            return posts;
1950        }
1951        
1952        /**
1953         * Add a post to the cache.
1954         * @param content The content
1955         * @param tagId the tag ID.
1956         */
1957        public void addPost(Content content, String tagId)
1958        {
1959            Map<String, Post> posts = getOrCreatePostMap(tagId);
1960            
1961            Post post = new Post();
1962            post.setId(content.getId());
1963            post.setName(content.getName());
1964            post.setDate(content.getValue(POST_DATE_ATTRIBUTE));
1965            post.setTags(content.getValue(POST_TAGS_ATTRIBUTE, false, new String[0]));
1966            
1967            posts.put(post.getId(), post);
1968        }
1969        
1970        /**
1971         * Add a post to the cache.
1972         * @param post the post
1973         * @param tagId the tag ID.
1974         */
1975        public void addPost(Post post, String tagId)
1976        {
1977            Map<String, Post> posts = getOrCreatePostMap(tagId);
1978            posts.put(post.getId(), post);
1979        }
1980        
1981        /**
1982         * Remove a post from the cache.
1983         * @param contentId The id of content
1984         * @param tagId  the id of tag
1985         */
1986        public void removePost(String contentId, String tagId)
1987        {
1988            if (_posts.containsKey(tagId))
1989            {
1990                Map<String, Post> posts = _posts.get(tagId);
1991                
1992                posts.remove(contentId);
1993                if (posts.isEmpty())
1994                {
1995                    _posts.remove(tagId);
1996                }
1997            }
1998        }
1999        
2000        /**
2001         * Test if the post with the given ID exists for this year and month.
2002         * @param tagId the tag id.
2003         * @param id the post ID.
2004         * @return the posts in the specified year and month.
2005         */
2006        public Post getPost(String tagId, String id)
2007        {
2008            return getPosts(tagId).get(id);
2009        }
2010        
2011        /**
2012         * Test if the post with the given ID exists for this year and month.
2013         * @param tagId the tag id.
2014         * @param id the post ID.
2015         * @return the posts in the specified year and month.
2016         */
2017        public boolean hasPost(String tagId, String id)
2018        {
2019            return getPosts(tagId).containsKey(id);
2020        }
2021        
2022        /**
2023         * Get the posts for a year-month couple.
2024         * @param tagId the year.
2025         * @param name The name of post
2026         * @return the posts in the specified year and month.
2027         */
2028        public Post getPostByName(String tagId, String name)
2029        {
2030            Post post = null;
2031            
2032            Iterator<Post> posts = getPosts(tagId).values().iterator();
2033            while (posts.hasNext() && post == null)
2034            {
2035                Post currentPost = posts.next();
2036                if (currentPost.getName().equals(name))
2037                {
2038                    post = currentPost;
2039                }
2040            }
2041            
2042            return post;
2043        }
2044        
2045        /**
2046         * Get the posts for a year-month couple.
2047         * @param tagId the year.
2048         * @param name the name of post
2049         * @return the posts in the specified year and month.
2050         */
2051        public boolean hasPostByName(String tagId, String name)
2052        {
2053            boolean hasPost = false;
2054            
2055            Iterator<Post> posts = getPosts(tagId).values().iterator();
2056            while (posts.hasNext() && !hasPost)
2057            {
2058                hasPost = posts.next().getName().equals(name);
2059            }
2060            
2061            return hasPost;
2062        }
2063        
2064        /**
2065         * Clear the cache.
2066         */
2067        public void clear()
2068        {
2069            _posts.clear();
2070        }
2071        
2072        /**
2073         * Get or create the post set for the specified year and month.
2074         * @param tagId the year.
2075         * @return the post set, indexed by id.
2076         */
2077        protected Map<String, Post> getOrCreatePostMap(String tagId)
2078        {
2079            Map<String, Post> posts;
2080            
2081            if (_posts.containsKey(tagId))
2082            {
2083                posts = _posts.get(tagId);
2084            }
2085            else
2086            {
2087                posts = new LinkedHashMap<>();
2088                _posts.put(tagId, posts);
2089            }
2090            
2091            return posts;
2092        }
2093        
2094    }
2095    
2096    /**
2097     * Cache of all the posts.
2098     */
2099    protected static class AllPostCache
2100    {
2101        
2102        /** The posts, indexed by ID. */
2103        private Map<String, Post> _posts;
2104        
2105        /** The posts, indexed by name. */
2106        private Map<String, Post> _postsByName;
2107        
2108        /**
2109         * Construct a AllPostCache instance.
2110         */
2111        public AllPostCache()
2112        {
2113            _posts = new LinkedHashMap<>();
2114            _postsByName = new LinkedHashMap<>();
2115        }
2116        
2117        /**
2118         * Get a list of all the posts' IDs.
2119         * @return all the posts' IDs as a Set.
2120         */
2121        public Set<String> getPostIds()
2122        {
2123            return Collections.unmodifiableSet(_posts.keySet());
2124        }
2125        
2126        /**
2127         * Get the exhaustive list of Posts.
2128         * @return the exhaustive list of Posts.
2129         */
2130        public Map<String, Post> getPosts()
2131        {
2132            return Collections.unmodifiableMap(_posts);
2133        }
2134        
2135        /**
2136         * Test if a post exists, provided its ID.
2137         * @param id the post content ID.
2138         * @return true if the post exists.
2139         */
2140        public Post getPost(String id)
2141        {
2142            return _posts.get(id);
2143        }
2144        
2145        /**
2146         * Test if a post exists, provided its ID.
2147         * @param id the post content ID.
2148         * @return true if the post exists.
2149         */
2150        public boolean hasPost(String id)
2151        {
2152            return _posts.containsKey(id);
2153        }
2154        
2155        /**
2156         * Test if a post exists, provided its content name.
2157         * @param name the post content name.
2158         * @return true if the post exists.
2159         */
2160        public Post getPostByName(String name)
2161        {
2162            return _postsByName.get(name);
2163        }
2164        
2165        /**
2166         * Test if a post exists, provided its content name.
2167         * @param name the post content name.
2168         * @return true if the post exists.
2169         */
2170        public boolean hasPostByName(String name)
2171        {
2172            return _postsByName.containsKey(name);
2173        }
2174        
2175        /**
2176         * Add a post to the cache.
2177         * @param content the post content.
2178         */
2179        public void addPost(Content content)
2180        {
2181            Post post = new Post();
2182            post.setId(content.getId());
2183            post.setName(content.getName());
2184            if (content.hasValue(POST_DATE_ATTRIBUTE))
2185            {
2186                post.setDate(content.getValue(POST_DATE_ATTRIBUTE));
2187            }
2188            post.setTags(content.getValue(POST_TAGS_ATTRIBUTE, false, new String[0]));
2189            
2190            _posts.put(post.getId(), post);
2191            _postsByName.put(post.getName(), post);
2192        }
2193        
2194        /**
2195         * Add a post to the cache.
2196         * @param post The post
2197         */
2198        public void addPost(Post post)
2199        {
2200            _posts.put(post.getId(), post);
2201            _postsByName.put(post.getName(), post);
2202        }
2203        
2204        /**
2205         * Remove a post from the cache.
2206         * @param content the post content to remove from the cache.
2207         */
2208        public void removePost(Content content)
2209        {
2210            removePost(content.getId(), content.getName());
2211        }
2212        
2213        /**
2214         * Remove a post from the cache.
2215         * @param id The id of post
2216         * @param name The name of post
2217         */
2218        public void removePost(String id, String name)
2219        {
2220            _posts.remove(id);
2221            _postsByName.remove(name);
2222        }
2223        
2224        /**
2225         * Clear the cache.
2226         */
2227        public void clear()
2228        {
2229            _posts.clear();
2230            _postsByName.clear();
2231        }
2232    }
2233    
2234    /**
2235     * Cache of identifier of the future posts.
2236     */
2237    protected static class FuturePostCache
2238    {
2239        /** The posts, indexed by ID. */
2240        private Map<String, Post> _posts;
2241        
2242        /** The posts id, indexed by date. */
2243        private Multimap<ZonedDateTime, String> _sortedPostIds;
2244        
2245        /**
2246         * Construct a AllPostCache instance.
2247         */
2248        public FuturePostCache()
2249        {
2250            _posts = new HashMap<>();
2251            _sortedPostIds = TreeMultimap.create();
2252        }
2253        
2254        /**
2255         * Remove the post that are not future anymore (given the current date) 
2256         * @return The collection of past posts
2257         */
2258        public Collection<Post> removePastPosts()
2259        {
2260            Set<Post> pastPosts = new HashSet<>();
2261            
2262            Set<ZonedDateTime> sortedDates = _sortedPostIds.keySet();
2263            Iterator<ZonedDateTime> iterator = sortedDates.iterator();
2264            boolean isFuture = false;
2265            ZonedDateTime now = ZonedDateTime.now();
2266            
2267            while (iterator.hasNext() && !isFuture)
2268            {
2269                ZonedDateTime date = iterator.next();
2270                isFuture = date.isAfter(now);
2271                
2272                if (!isFuture)
2273                {
2274                    Collection<String> pastPostIds = _sortedPostIds.get(date);
2275                    for (String id : pastPostIds)
2276                    {
2277                        Post pastPost = _posts.get(id);
2278                        if (pastPost != null)
2279                        {
2280                            pastPosts.add(pastPost);
2281                        }
2282                    }
2283                }
2284            }
2285            
2286            for (Post pastPost : pastPosts)
2287            {
2288                removePost(pastPost);
2289            }
2290            
2291            return pastPosts;
2292        }
2293
2294        /**
2295         * Test if a post exists, provided its ID.
2296         * @param id the post content ID.
2297         * @return true if the post exists.
2298         */
2299        public Post getPost(String id)
2300        {
2301            return _posts.get(id);
2302        }
2303        
2304        /**
2305         * Test if a post exists, provided its ID.
2306         * @param id the post content ID.
2307         * @return true if the post exists.
2308         */
2309        public boolean hasPost(String id)
2310        {
2311            return _posts.containsKey(id);
2312        }
2313        
2314        /**
2315         * Add a post to the cache.
2316         * @param content the post content.
2317         */
2318        public void addPost(Content content)
2319        {
2320            String id = content.getId();
2321            
2322            if (_posts.containsKey(id))
2323            {
2324                removePost(content);
2325            }
2326            
2327            ZonedDateTime date = content.getValue(POST_DATE_ATTRIBUTE);
2328            
2329            Post post = new Post();
2330            post.setId(id);
2331            post.setName(content.getName());
2332            post.setDate(date);
2333            post.setTags(content.getValue(POST_TAGS_ATTRIBUTE, false, new String[0]));
2334            
2335            _posts.put(post.getId(), post);
2336            _sortedPostIds.put(date, id);
2337        }
2338        
2339        /**
2340         * Remove a post from the cache.
2341         * @param content the post content to remove from the cache.
2342         */
2343        public void removePost(Content content)
2344        {
2345            Post post = _posts.remove(content.getId());
2346            if (post != null)
2347            {
2348                _sortedPostIds.remove(post.getDate(), post.getId());
2349            }
2350        }
2351        
2352        /**
2353         * Remove a post from the cache.
2354         * @param post the post content to remove from the cache.
2355         */
2356        public void removePost(Post post)
2357        {
2358            _posts.remove(post.getId());
2359            _sortedPostIds.remove(post.getDate(), post.getId());
2360        }
2361        
2362        /**
2363         * Clear the cache.
2364         */
2365        public void clear()
2366        {
2367            _posts.clear();
2368            _sortedPostIds.clear();
2369        }
2370    }
2371    
2372    /**
2373     * Reverse date post comparator.
2374     */
2375    public class ReverseDatePostComparator implements Comparator<Post>
2376    {
2377        @Override
2378        public int compare(Post p1, Post p2)
2379        {
2380            if (p1.getDate() == null)
2381            {
2382                return 1;
2383            }
2384            if (p2.getDate() == null)
2385            {
2386                return -1;
2387            }
2388            return p2.getDate().compareTo(p1.getDate());
2389        }
2390    }
2391
2392    static class BlogPageElementKey extends AbstractCacheKey
2393    {
2394        BlogPageElementKey(String workspace, String siteName, String sitemap)
2395        {
2396            super(workspace, siteName, sitemap);
2397        }
2398
2399        static BlogPageElementKey of(String workspace, String site, String sitemap)
2400        {
2401            return new BlogPageElementKey(workspace, site, sitemap);
2402        } 
2403        
2404        static BlogPageElementKey of(String workspace, String site)
2405        {
2406            return new BlogPageElementKey(workspace, site, null);
2407        } 
2408        
2409        static BlogPageElementKey of(String workspace)
2410        {
2411            return of(workspace, null);
2412        }
2413    }
2414
2415    private Cache<BlogPageElementKey, PostDateCache> _getPostByDateCache() 
2416    {
2417        return this._cacheManager.get(__POST_BY_DATE_CACHE);
2418    }  
2419
2420    private Cache<BlogPageElementKey, PostTagCache> _getPostByTagCache() 
2421    {
2422        return this._cacheManager.get(__POST_BY_TAG_CACHE);
2423    }   
2424
2425    private Cache<BlogPageElementKey, AllPostCache> _getAllPosts() 
2426    {
2427        return this._cacheManager.get(__ALL_POSTS_CACHE);
2428    } 
2429
2430    private Cache<BlogPageElementKey, FuturePostCache> _getFuturePosts() 
2431    {
2432        return this._cacheManager.get(__FUTURE_POSTS_CACHE);
2433    }   
2434    
2435    private Cache<BlogPageElementKey, Date> _getLastUpdateFuturePosts() 
2436    {
2437        return this._cacheManager.get(__LAST_UPDATE_FUTURE_POST_CACHE);
2438    }   
2439}