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.site; 017 018import java.io.IOException; 019import java.util.Map; 020import java.util.concurrent.ConcurrentHashMap; 021import java.util.concurrent.ConcurrentMap; 022import java.util.concurrent.locks.ReentrantLock; 023 024import org.apache.avalon.framework.activity.Initializable; 025import org.apache.avalon.framework.component.Component; 026import org.apache.avalon.framework.context.Context; 027import org.apache.avalon.framework.context.ContextException; 028import org.apache.avalon.framework.context.Contextualizable; 029import org.apache.avalon.framework.logger.AbstractLogEnabled; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.avalon.framework.thread.ThreadSafe; 034import org.apache.cocoon.components.ContextHelper; 035import org.apache.cocoon.environment.ObjectModelHelper; 036import org.apache.cocoon.environment.Request; 037import org.apache.excalibur.xml.sax.SAXParser; 038import org.apache.http.Header; 039import org.apache.http.HttpResponse; 040import org.apache.http.client.methods.HttpUriRequest; 041import org.apache.http.impl.client.CloseableHttpClient; 042 043import org.ametys.plugins.site.headers.RequestHeaderExtensionPoint; 044 045/** 046 * Component that regulates access to the page cache. 047 */ 048public class CacheAccessManager extends AbstractLogEnabled implements Component, Initializable, Serviceable, Contextualizable, ThreadSafe 049{ 050 051 /** The avalon role. */ 052 public static final String ROLE = CacheAccessManager.class.getName(); 053 054 /** 055 * A Map keeping record of whether the pages are cacheable or not. 056 * The key is the page path, the value indicates if the page is cacheable. 057 */ 058 protected ConcurrentMap<String, Boolean> _pageCacheable; 059 060 /** 061 * Map of page cache locks. 062 * The lock is acquired on the first call to the page, when the cache is empty, 063 * and released when the page is written into the cache. 064 */ 065 protected ConcurrentMap<String, ReentrantLock> _pageLocks; 066 067 /** The cocoon context. */ 068 protected Context _context; 069 070 /** A SAX parser. */ 071 protected SAXParser _parser; 072 073 /** The extension point for adding request headers in BO request */ 074 protected RequestHeaderExtensionPoint _requestHeaderEP; 075 076 @Override 077 public void initialize() throws Exception 078 { 079 _pageCacheable = new ConcurrentHashMap<>(); 080 _pageLocks = new ConcurrentHashMap<>(); 081 } 082 083 @Override 084 public void contextualize(Context context) throws ContextException 085 { 086 _context = context; 087 } 088 089 @Override 090 public void service(ServiceManager serviceManager) throws ServiceException 091 { 092 _parser = (SAXParser) serviceManager.lookup(SAXParser.ROLE); 093 _requestHeaderEP = (RequestHeaderExtensionPoint) serviceManager.lookup(RequestHeaderExtensionPoint.ROLE); 094 } 095 096 /** 097 * Tests if the page is cacheable. 098 * If more than one thread call this method concurrently, only one tests the 099 * cacheability and (possibly) generates the corresponding resource. 100 * The concurrent threads are blocked until the page is known not to be cacheable, 101 * or known to be cacheable and put into the cache (in {@link GeneratePageAction}). 102 * @param pagePath the page path. 103 * @return true if the page is cacheable, false otherwise. 104 * @throws IOException if an error occurs sending the cacheability request to the back-office. 105 * @see GeneratePageAction 106 */ 107 public boolean isCacheable(String pagePath) throws IOException 108 { 109 boolean cacheable = false; 110 111 // If we don't know if the page is cacheable, or if the page is known to be cacheable, block. 112 if (!_pageCacheable.containsKey(pagePath) || _pageCacheable.get(pagePath)) 113 { 114 if (getLogger().isDebugEnabled()) 115 { 116 getLogger().debug("Page cacheability unknown or known to be cacheable, testing : " + pagePath); 117 } 118 119 // Retrieve the shared lock. 120 ReentrantLock lock = new ReentrantLock(); 121 122 ReentrantLock oldLock = _pageLocks.putIfAbsent(pagePath, lock); 123 if (oldLock != null) 124 { 125 lock = oldLock; 126 } 127 128 if (getLogger().isDebugEnabled()) 129 { 130 getLogger().debug("Acquiring lock for page " + pagePath + (lock.isLocked() ? " (already locked)" : " (unlocked)") + ", queue length: " + lock.getQueueLength() + ", hold count: " + lock.getHoldCount()); 131 } 132 133 // Try to acquire the lock. If the thread is the first to acquire the lock, 134 // it will go on. If not, it will be blocked until the owner unlocks. 135 // It MUST be unlocked (call to unlock) by the same thread. 136 lock.lock(); 137 138 // At this point, the cacheable status could already be computed by another thread. 139 if (_pageCacheable.containsKey(pagePath)) 140 { 141 cacheable = _pageCacheable.get(pagePath); 142 143 if (getLogger().isDebugEnabled()) 144 { 145 getLogger().debug("The page " + pagePath + " was computed to be " + (cacheable ? "" : "not ") + "cacheable by another thread, unlocking..."); 146 } 147 } 148 else 149 { 150 if (getLogger().isDebugEnabled()) 151 { 152 getLogger().debug("Asking the CMS if the page is cacheable: " + pagePath); 153 } 154 155 cacheable = _isCacheable(pagePath); 156 157 _pageCacheable.putIfAbsent(pagePath, cacheable); 158 159 if (getLogger().isDebugEnabled()) 160 { 161 getLogger().debug("The CMS answered that the page " + pagePath + " is " + (cacheable ? "" : "not ") + "cacheable."); 162 } 163 } 164 } 165 166 return cacheable; 167 } 168 169 /** 170 * Release the lock associated with a page. 171 * @param pagePath the page path. 172 */ 173 public void unlock(String pagePath) 174 { 175 if (_pageLocks.containsKey(pagePath)) 176 { 177 if (getLogger().isDebugEnabled()) 178 { 179 getLogger().debug("The page " + pagePath + " is being unlocked."); 180 } 181 182 ReentrantLock lock = _pageLocks.get(pagePath); 183 184 // This method is called from several locations, where it's hard to know if the resource has actually been locked or not. 185 // So simply ask the lock itself 186 if (lock.isHeldByCurrentThread()) 187 { 188 if (getLogger().isDebugEnabled()) 189 { 190 getLogger().debug("Releasing lock for page " + pagePath + ", queue length: " + lock.getQueueLength() + ", hold count: " + lock.getHoldCount()); 191 } 192 193 lock.unlock(); 194 195 if (getLogger().isDebugEnabled()) 196 { 197 getLogger().debug("The page " + pagePath + " has been successfully unlocked."); 198 } 199 } 200 } 201 } 202 203 /** 204 * Resets all the local caches and locks. 205 */ 206 public void reset() 207 { 208 _pageCacheable = new ConcurrentHashMap<>(); 209 } 210 211 /** 212 * Remove a specified page path from the local cache and lock. 213 * @param pagePath the page path. 214 */ 215 public void resetPage(String pagePath) 216 { 217 _pageCacheable.remove(pagePath); 218 } 219 220 /** 221 * Get the cacheable status of a page from the back-office. 222 * @param pagePath the page path. 223 * @return true if the page is cacheable, false otherwise. 224 * @throws IOException in case of a problem or the connection was aborted 225 */ 226 protected boolean _isCacheable(String pagePath) throws IOException 227 { 228 Map objectModel = ContextHelper.getObjectModel(_context); 229 230 CloseableHttpClient httpClient = BackOfficeRequestHelper.getHttpClient(); 231 HttpUriRequest cmsRequest = BackOfficeRequestHelper.getRequest(objectModel, pagePath, _requestHeaderEP); 232 233 HttpResponse cmsResponse = httpClient.execute(cmsRequest); 234 Request request = ObjectModelHelper.getRequest(objectModel); 235 request.setAttribute("cms-request", cmsRequest); 236 request.setAttribute("cms-response", cmsResponse); 237 request.setAttribute("http-client", httpClient); 238 239 Header header = cmsResponse.getFirstHeader("X-Ametys-Cacheable"); 240 return header != null && "true".equals(header.getValue()); 241 } 242}