001/*
002 *  Copyright 2012 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.web.cache.pageelement;
017
018import java.util.HashSet;
019import java.util.Iterator;
020import java.util.Map;
021import java.util.Set;
022
023import org.apache.avalon.framework.configuration.Configurable;
024import org.apache.avalon.framework.configuration.Configuration;
025import org.apache.avalon.framework.configuration.ConfigurationException;
026import org.apache.avalon.framework.logger.AbstractLogEnabled;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030
031import org.ametys.core.observation.Event;
032import org.ametys.core.observation.Observer;
033import org.ametys.runtime.servlet.RuntimeServlet;
034import org.ametys.runtime.servlet.RuntimeServlet.RunMode;
035import org.ametys.web.WebHelper;
036import org.ametys.web.cache.pageelement.PageElementCachePolicy.PolicyResult;
037import org.ametys.web.repository.site.Site;
038import org.ametys.web.repository.site.SiteManager;
039
040/**
041 * Handles the {@link PageElementCache} based on the various {@link PageElementCachePolicy}.
042 */
043public class InvalidatePageElementCacheObserver extends AbstractLogEnabled implements Observer, Serviceable, Configurable
044{
045    private ServiceManager _manager;
046
047    private PageElementCache _pageElementCache;
048    private PageElementCachePolicyExtensionPoint _pageElementCachePolicyExtensionPoint;
049    private SiteManager _siteManager;
050
051    @Override
052    public void service(ServiceManager manager) throws ServiceException
053    {
054        _manager = manager;
055        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
056    }
057
058    @Override
059    public void configure(Configuration configuration) throws ConfigurationException
060    {
061        String cache = configuration.getChild("cache").getValue();
062        String policies = configuration.getChild("policies").getValue();
063
064        try
065        {
066            _pageElementCache = (PageElementCache) _manager.lookup(cache);
067            _pageElementCachePolicyExtensionPoint = (PageElementCachePolicyExtensionPoint) _manager.lookup(policies);
068        }
069        catch (ServiceException e)
070        {
071            throw new ConfigurationException("Wrong value for cache and/or policies values.", configuration, e);
072        }
073    }
074
075    @Override
076    public int getPriority(Event event)
077    {
078        // processed just before front-office cache invalidation
079        return MAX_PRIORITY + 3500;
080    }
081
082    @Override
083    public boolean supports(Event event)
084    {
085        // supports all events, as they are further processed by the various cache policies
086        // except if we are starting or stopping Ametys
087        return RuntimeServlet.getRunMode() == RunMode.NORMAL
088                || RuntimeServlet.getRunMode() == RunMode.MAINTENANCE;
089    }
090
091    @Override
092    public void observe(Event event, Map<String, Object> transientVars) throws Exception
093    {
094        Site site = _getSite(event);
095
096        if (site != null)
097        {
098            _doObserve("default", site, event);
099            _doObserve("live", site, event);
100        }
101        else
102        {
103            for (Site s : _siteManager.getSites())
104            {
105                _doObserve("default", s, event);
106                _doObserve("live", s, event);
107            }
108        }
109        
110    }
111    
112    /*
113     * The algorithm is:
114     * 
115     * for each cached inputdata
116     *      for each associated InputDataCachePolicy, call the first shouldClearCache method
117     *          all null values are ignored
118     *          if there's no non-null value, or if there's at least one REMOVE, remove the cache associated to the InputData role
119     *          if there's only KEEP values, do nothing
120     *          if there's at least one NEED_INFO, call the second shouldClearCache method
121     *          if there's no non-null value, or if there's at least one REMOVE or NEED_INFO, remove the cache associated to the InputData role
122     *          if there's only KEEP values, do nothing
123     */
124    private void _doObserve(String workspace, Site site, Event event)
125    {
126        String siteName = site.getName();
127
128        for (String pageElementType : _pageElementCache.getPageElementTypes(workspace, siteName))
129        {
130            Set<PageElementCachePolicy> policies = _pageElementCachePolicyExtensionPoint.getPolicies(pageElementType);
131
132            Set<PageElementCachePolicy> needInfoPolicies = _firstPass(policies, workspace, site, pageElementType, event);
133
134            if (needInfoPolicies != null)
135            {
136                // there's at least one NEED_INFORMATION, iterate on pages
137
138                for (String elementId : _pageElementCache.getElementCache(workspace, siteName, pageElementType))
139                {
140                    _secondPass(needInfoPolicies, workspace, site, pageElementType, elementId, event);
141                }
142            }
143        }
144    }
145
146    private Set<PageElementCachePolicy> _firstPass(Set<PageElementCachePolicy> policies, String workspace, Site site, String pageElementType, Event event)
147    {
148        String siteName = site.getName();
149
150        boolean hasAtLeastOneRemove = false;
151        boolean hasAtLeastOneKeep = false;
152        boolean hasAtLeastOneNeedInfo = false;
153
154        Iterator<PageElementCachePolicy> it = policies.iterator();
155
156        Set<PageElementCachePolicy> needInfoPolicies = new HashSet<>();
157
158        StringBuilder buff = new StringBuilder();
159        if (getLogger().isDebugEnabled())
160        {
161            buff.append("First pass: testing ").append(event).append(" on workspace '").append(workspace).append("', site '").append(siteName).append("' and type '").append(pageElementType).append("'");
162        }
163
164        while (!hasAtLeastOneRemove && it.hasNext())
165        {
166            PageElementCachePolicy policy = it.next();
167            PolicyResult result = policy.shouldClearCache(workspace, site, pageElementType, event);
168
169            if (getLogger().isDebugEnabled())
170            {
171                buff.append("\n - Policy ").append(policy.getClass().getName()).append(" answered ").append(result);
172            }
173
174            if (result != null)
175            {
176                switch (result)
177                {
178                    case KEEP:
179                        hasAtLeastOneKeep = true;
180                        break;
181                    case REMOVE:
182                        hasAtLeastOneRemove = true;
183                        break;
184                    case NEED_MORE_INFORMATION:
185                        hasAtLeastOneNeedInfo = true;
186                        needInfoPolicies.add(policy);
187                        break;
188                    default:
189                        throw new IllegalArgumentException("Should never occur.");
190                }
191            }
192        }
193
194        if (getLogger().isDebugEnabled())
195        {
196            getLogger().debug(buff.toString());
197        }
198
199        if (hasAtLeastOneRemove || !hasAtLeastOneRemove && !hasAtLeastOneKeep && !hasAtLeastOneNeedInfo)
200        {
201            // there's no result or at least one REMOVE
202            _pageElementCache.clear(workspace, siteName, pageElementType);
203        }
204        else if (hasAtLeastOneNeedInfo)
205        {
206            return needInfoPolicies;
207        }
208
209        return null;
210    }
211
212    private void _secondPass(Set<PageElementCachePolicy> policies, String workspace, Site site, String pageElementType, String elementId, Event event)
213    {
214        String siteName = site.getName();
215
216        boolean hasAtLeastOneRemove = false;
217        boolean hasAtLeastOneKeep = false;
218        boolean hasAtLeastOneNeedInfo = false;
219
220        Iterator<PageElementCachePolicy> it = policies.iterator();
221
222        StringBuilder buff = new StringBuilder();
223        if (getLogger().isDebugEnabled())
224        {
225            buff.append("Second pass: testing ").append(event).append(" on workspace '").append(workspace).append("', site '").append(siteName).append("', type '").append(pageElementType).append("' and ID '").append(elementId).append("'");
226        }
227
228        while (!hasAtLeastOneRemove && !hasAtLeastOneNeedInfo && it.hasNext())
229        {
230            PageElementCachePolicy policy = it.next();
231            PolicyResult result = policy.shouldClearCache(workspace, site, pageElementType, elementId, event);
232
233            if (getLogger().isDebugEnabled())
234            {
235                buff.append("\n - Policy ").append(policy.getClass().getName()).append(" answered ").append(result);
236            }
237
238            if (result != null)
239            {
240                switch (result)
241                {
242                    case KEEP:
243                        hasAtLeastOneKeep = true;
244                        break;
245                    case REMOVE:
246                        hasAtLeastOneRemove = true;
247                        break;
248                    case NEED_MORE_INFORMATION:
249                        hasAtLeastOneNeedInfo = true;
250                        break;
251                    default:
252                        throw new IllegalArgumentException("Should never occur.");
253                }
254            }
255        }
256
257        if (getLogger().isDebugEnabled())
258        {
259            getLogger().debug(buff.toString());
260        }
261
262        _removeItemIfNecessary(workspace, pageElementType, elementId, siteName, hasAtLeastOneRemove, hasAtLeastOneKeep, hasAtLeastOneNeedInfo);
263    }
264
265    private void _removeItemIfNecessary(String workspace, String pageElementType, String elementId, String siteName, boolean hasAtLeastOneRemove, boolean hasAtLeastOneKeep, boolean hasAtLeastOneNeedInfo)
266    {
267        if (hasAtLeastOneRemove || hasAtLeastOneNeedInfo || !hasAtLeastOneRemove && !hasAtLeastOneKeep && !hasAtLeastOneNeedInfo)
268        {
269            // there's no result or at least one REMOVE or one NEED_INFO
270            _pageElementCache.removeItem(workspace, siteName, pageElementType, elementId);
271        }
272    }
273
274    private Site _getSite(Event event)
275    {
276        return WebHelper.findSite(event);
277    }
278}