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