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