001/*
002 *  Copyright 2017 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;
017
018import java.util.HashSet;
019import java.util.Map;
020import java.util.Set;
021import java.util.regex.Matcher;
022import java.util.regex.Pattern;
023
024import org.apache.avalon.framework.service.ServiceException;
025import org.apache.avalon.framework.service.ServiceManager;
026import org.apache.avalon.framework.service.Serviceable;
027import org.apache.jackrabbit.util.ISO9075;
028
029import org.ametys.core.observation.Event;
030import org.ametys.core.observation.Observer;
031import org.ametys.plugins.explorer.ObservationConstants;
032import org.ametys.plugins.repository.AmetysObjectIterable;
033import org.ametys.plugins.repository.AmetysObjectIterator;
034import org.ametys.plugins.repository.AmetysObjectResolver;
035import org.ametys.runtime.plugin.component.AbstractLogEnabled;
036import org.ametys.web.repository.page.Page;
037import org.ametys.web.repository.page.SitemapElement;
038import org.ametys.web.repository.page.ZoneItem;
039import org.ametys.web.repository.site.Site;
040import org.ametys.web.repository.site.SiteManager;
041
042/**
043 * Invalidate the page cache based on explorer events.
044 */
045public class InvalidatePageCacheExplorerObserver extends AbstractLogEnabled implements Observer, Serviceable
046{
047    private static final Set<String> _TYPES = new HashSet<>();
048    static
049    {
050        _TYPES.add("SERVICE:org.ametys.web.service.AttachmentsService");
051        _TYPES.add("SERVICE:org.ametys.web.service.ExplorerFolderService");
052    }
053
054    private static final Set<String> __XPATH_ZONEITEM_SERVICE = new HashSet<>(); 
055    static
056    {
057        __XPATH_ZONEITEM_SERVICE.add("//element(*, ametys:page)/ametys-internal:zones//ametys:zoneItem[@ametys-internal:service=\"org.ametys.web.service.AttachmentsService\"]");
058        __XPATH_ZONEITEM_SERVICE.add("//element(*, ametys:page)/ametys-internal:zones//ametys:zoneItem[@ametys-internal:service=\"org.ametys.web.service.ExplorerFolderService\"]");
059    }
060    
061    private static final Pattern __RESOURCE_PATTERN = Pattern.compile("^.*/ametys-internal:sites/([^/]+)/ametys-internal:(.*)$");
062    private static final Pattern __ROOT_SITE_RESOURCE_PATTERN = Pattern.compile("^.*/ametys-internal:sites/[^/]+/[^/]+/([^/]+)/ametys-internal:(.*)$");
063    
064    
065    private SiteManager _siteManager;
066    private AmetysObjectResolver _resolver;
067
068    @Override
069    public void service(ServiceManager manager) throws ServiceException
070    {
071        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
072        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
073    }
074    
075    @Override
076    public int getPriority(Event event)
077    {
078        return MAX_PRIORITY;
079    }
080    
081    @Override
082    public boolean supports(Event event)
083    {
084        String eventType = event.getId();
085        
086        return eventType.equals(ObservationConstants.EVENT_RESOURCE_RENAMED) 
087                || eventType.equals(ObservationConstants.EVENT_RESOURCE_CREATED)
088                || eventType.equals(ObservationConstants.EVENT_RESOURCE_DELETED)
089                || eventType.equals(ObservationConstants.EVENT_RESOURCE_MOVED)
090                || eventType.equals(ObservationConstants.EVENT_RESOURCE_UPDATED)
091                || eventType.equals(ObservationConstants.EVENT_COLLECTION_RENAMED)
092                || eventType.equals(ObservationConstants.EVENT_COLLECTION_MOVED)
093                || eventType.equals(ObservationConstants.EVENT_COLLECTION_COPIED)
094                || eventType.equals(ObservationConstants.EVENT_COLLECTION_DELETED);
095    }
096    
097    @Override
098    public void observe(Event event, Map<String, Object> transientVars) throws Exception
099    {
100        String siteName = getSiteName(event);
101        
102        if (siteName != null)
103        {
104            Site site = _siteManager.getSite(siteName);
105            doObserve(site, event);
106        }
107        else
108        {
109            doObserve(null, event);
110        }
111    }
112    
113    private void doObserve(Site site, Event event)
114    {
115        if (getLogger().isInfoEnabled())
116        {
117            getLogger().info("New event on explorer resources : " + event + ", invalidating site cache");
118        }
119
120        try
121        {
122            Set<Page> clearedPages = new HashSet<>();
123            for (String xpathZoneItem : __XPATH_ZONEITEM_SERVICE)
124            {
125                String xpathQuery = xpathZoneItem;
126                if (site != null)
127                {
128                    String relativeJcrPath = site.getNode().getPath().substring(AmetysObjectResolver.ROOT_REPO.length() + 1);
129                    xpathQuery = "/" + ISO9075.encodePath(relativeJcrPath) + xpathQuery; 
130                }
131                try (AmetysObjectIterable<ZoneItem> zoneItems = _resolver.query(xpathQuery))
132                {
133                    AmetysObjectIterator<ZoneItem> it = zoneItems.iterator();
134                    while (it.hasNext())
135                    {
136                        ZoneItem zoneItem = it.next();
137                        SitemapElement sitemapElement = zoneItem.getZone().getSitemapElement();
138                        if (sitemapElement instanceof Page page
139                            && !clearedPages.contains(page))
140                        {
141                            CacheHelper.invalidateCache(page, getLogger());
142                            clearedPages.add(page);
143                        }
144                    }
145                }
146            }
147        }
148        catch (Exception e)
149        {
150            if (site != null)
151            {
152                getLogger().error("Unable ot invalidate cache for site '" + site.getName() + "' pages containing a service with explorer resources", e);
153            }
154            else
155            {
156                getLogger().error("Unable ot invalidate cache for all sites pages containing a service with explorer resources", e);                
157            }
158        }
159    }
160
161    /**
162     * Get the origin of the event.
163     * @param event the event.
164     * @return the origin site name.
165     */
166    protected String getSiteName(Event event)
167    {
168        String path = null;
169        
170        Map<String, Object> args = event.getArguments();
171        if (event.getId().equals(ObservationConstants.EVENT_RESOURCE_CREATED))
172        {
173            // Multiple resources may have been created, look for to parent path
174            path = (String) args.get(ObservationConstants.ARGS_PARENT_PATH);
175        }
176        else
177        {
178            path = (String) event.getArguments().get(ObservationConstants.ARGS_PATH);
179        }
180        
181        Matcher matcher1 = __RESOURCE_PATTERN.matcher(path);
182        Matcher matcher2 = __ROOT_SITE_RESOURCE_PATTERN.matcher(path);
183        
184        String siteName = null;
185        
186        if (matcher1.matches())
187        {
188            siteName = matcher1.group(1);
189        }
190        else if (matcher2.matches())
191        {
192            siteName = matcher2.group(1);
193        }
194        
195        return siteName;
196    }
197    
198}