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