001/* 002 * (C) Copyright 2006-2009 Nuxeo SAS (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Nuxeo - initial API and implementation 016 * 017 * $Id$ 018 */ 019 020package org.nuxeo.ecm.platform.web.common.requestcontroller.filter; 021 022import java.io.IOException; 023import java.security.Principal; 024import java.text.DateFormat; 025import java.text.SimpleDateFormat; 026import java.util.Date; 027import java.util.Locale; 028import java.util.TimeZone; 029import java.util.concurrent.TimeUnit; 030import java.util.concurrent.locks.Lock; 031import java.util.concurrent.locks.ReentrantLock; 032 033import javax.servlet.Filter; 034import javax.servlet.FilterChain; 035import javax.servlet.FilterConfig; 036import javax.servlet.ServletException; 037import javax.servlet.ServletRequest; 038import javax.servlet.ServletResponse; 039import javax.servlet.http.HttpServletRequest; 040import javax.servlet.http.HttpServletResponse; 041import javax.servlet.http.HttpSession; 042 043import org.apache.commons.logging.Log; 044import org.apache.commons.logging.LogFactory; 045import org.nuxeo.ecm.core.io.download.DownloadHelper; 046import org.nuxeo.ecm.platform.web.common.ServletHelper; 047import org.nuxeo.ecm.platform.web.common.requestcontroller.service.RequestControllerManager; 048import org.nuxeo.ecm.platform.web.common.requestcontroller.service.RequestFilterConfig; 049import org.nuxeo.runtime.api.Framework; 050import org.nuxeo.runtime.transaction.TransactionHelper; 051import org.nuxeo.runtime.transaction.TransactionRuntimeException; 052 053/** 054 * Filter to handle Transactions and Requests synchronization. This filter is useful when accessing web resources that 055 * are not protected by Seam Filter. This is the case for specific Servlets, WebEngine, XML-RPC connector ... 056 * 057 * @author tiry 058 */ 059public class NuxeoRequestControllerFilter implements Filter { 060 061 protected static final String SESSION_LOCK_KEY = "NuxeoSessionLockKey"; 062 063 protected static final String SYNCED_REQUEST_FLAG = "NuxeoSessionAlreadySync"; 064 065 // FIXME: typo in constant name. 066 protected static final int LOCK_TIMOUT_S = 120; 067 068 public static final DateFormat HTTP_EXPIRES_DATE_FORMAT = httpExpiresDateFormat(); 069 070 protected static RequestControllerManager rcm; 071 072 private static final Log log = LogFactory.getLog(NuxeoRequestControllerFilter.class); 073 074 @Override 075 public void init(FilterConfig filterConfig) throws ServletException { 076 doInitIfNeeded(); 077 } 078 079 private static void doInitIfNeeded() { 080 if (rcm == null) { 081 if (Framework.getRuntime() != null) { 082 rcm = Framework.getLocalService(RequestControllerManager.class); 083 084 if (rcm == null) { 085 log.error("Unable to get RequestControllerManager service"); 086 // throw new ServletException( 087 // "RequestControllerManager can not be found"); 088 } 089 log.debug("Staring NuxeoRequestController filter"); 090 } else { 091 log.debug("Postpone filter init since Runtime is not yet available"); 092 } 093 } 094 } 095 096 public static String doFormatLogMessage(HttpServletRequest request, String info) { 097 String remoteHost = RemoteHostGuessExtractor.getRemoteHost(request); 098 Principal principal = request.getUserPrincipal(); 099 String principalName = principal != null ? principal.getName() : "none"; 100 String uri = request.getRequestURI(); 101 HttpSession session = request.getSession(false); 102 String sessionId = session != null ? session.getId() : "none"; 103 String threadName = Thread.currentThread().getName(); 104 return "remote=" + remoteHost + ",principal=" + principalName + ",uri=" + uri + ",session=" + sessionId 105 + ",thread=" + threadName + ",info=" + info; 106 } 107 108 @Override 109 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, 110 ServletException { 111 112 HttpServletRequest httpRequest = (HttpServletRequest) request; 113 HttpServletResponse httpResponse = (HttpServletResponse) response; 114 115 if (log.isDebugEnabled()) { 116 log.debug(doFormatLogMessage(httpRequest, "Entering NuxeoRequestController filter")); 117 } 118 119 doInitIfNeeded(); 120 121 RequestFilterConfig config = rcm.getConfigForRequest(httpRequest); 122 123 boolean useSync = config.needSynchronization(); 124 boolean useTx = config.needTransaction(); 125 126 // Add cache header if needed 127 if (httpRequest.getMethod().equals("GET")) { 128 boolean isCached = config.isCached(); 129 if (isCached) { 130 addCacheHeader(httpResponse, config.isPrivate(), config.getCacheTime()); 131 } 132 } 133 134 if (!useSync && !useTx) { 135 if (log.isDebugEnabled()) { 136 log.debug(doFormatLogMessage(httpRequest, "Existing NuxeoRequestController filter: nothing to be done")); 137 } 138 139 try { 140 chain.doFilter(request, response); 141 } catch (ServletException e) { 142 if (DownloadHelper.isClientAbortError(e)) { 143 DownloadHelper.logClientAbort(e); 144 } else { 145 throw e; 146 } 147 } 148 return; 149 } 150 151 if (log.isDebugEnabled()) { 152 log.debug(doFormatLogMessage(httpRequest, "Handling request with tx=" + useTx + " and sync=" + useSync)); 153 } 154 155 boolean sessionSynched = false; 156 if (useSync) { 157 sessionSynched = simpleSyncOnSession(httpRequest); 158 } 159 boolean txStarted = false; 160 try { 161 if (useTx) { 162 txStarted = ServletHelper.startTransaction(httpRequest); 163 if (txStarted) { 164 if (config.needTransactionBuffered()) { 165 response = new BufferingHttpServletResponse(httpResponse); 166 } 167 } 168 } 169 chain.doFilter(request, response); 170 } catch (RuntimeException | IOException | ServletException e) { 171 if (txStarted) { 172 if (log.isDebugEnabled()) { 173 log.debug(doFormatLogMessage(httpRequest, "Marking transaction for RollBack")); 174 } 175 TransactionHelper.setTransactionRollbackOnly(); 176 } 177 if (DownloadHelper.isClientAbortError(e)) { 178 DownloadHelper.logClientAbort(e); 179 } else { 180 log.error(doFormatLogMessage(httpRequest, "Unhandled error was caught by the Filter"), e); 181 throw new ServletException(e); 182 } 183 } finally { 184 if (txStarted) { 185 try { 186 TransactionHelper.commitOrRollbackTransaction(); 187 } catch (TransactionRuntimeException e) { 188 // commit failed, report this to the client before stopping buffering 189 ((HttpServletResponse) response).sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 190 e.getMessage()); 191 throw e; 192 } finally { 193 if (config.needTransactionBuffered()) { 194 ((BufferingHttpServletResponse) response).stopBuffering(); 195 } 196 } 197 } 198 if (sessionSynched) { 199 simpleReleaseSyncOnSession(httpRequest); 200 } 201 if (log.isDebugEnabled()) { 202 log.debug(doFormatLogMessage(httpRequest, "Exiting NuxeoRequestController filter")); 203 } 204 } 205 } 206 207 /** 208 * Synchronizes the HttpSession. 209 * <p> 210 * Uses a {@link Lock} object in the HttpSession and locks it. If HttpSession is not created, exits without locking 211 * anything. 212 */ 213 public static boolean simpleSyncOnSession(HttpServletRequest request) { 214 HttpSession session = request.getSession(false); 215 if (session == null) { 216 if (log.isDebugEnabled()) { 217 log.debug(doFormatLogMessage(request, "HttpSession does not exist, this request won't be synched")); 218 } 219 return false; 220 } 221 222 if (log.isDebugEnabled()) { 223 log.debug(doFormatLogMessage(request, "Trying to sync on session ")); 224 } 225 226 if (request.getAttribute(SYNCED_REQUEST_FLAG) != null) { 227 if (log.isWarnEnabled()) { 228 log.warn(doFormatLogMessage(request, 229 "Request has already be synced, filter is reentrant, exiting without locking")); 230 } 231 return false; 232 } 233 234 Lock lock = (Lock) session.getAttribute(SESSION_LOCK_KEY); 235 if (lock == null) { 236 lock = new ReentrantLock(); 237 session.setAttribute(SESSION_LOCK_KEY, lock); 238 } 239 240 boolean locked = false; 241 try { 242 locked = lock.tryLock(LOCK_TIMOUT_S, TimeUnit.SECONDS); 243 } catch (InterruptedException e) { 244 log.error(doFormatLogMessage(request, "Unable to acquire lock for Session sync"), e); 245 return false; 246 } 247 248 if (locked) { 249 request.setAttribute(SYNCED_REQUEST_FLAG, true); 250 if (log.isDebugEnabled()) { 251 log.debug(doFormatLogMessage(request, "Request synced on session")); 252 } 253 } else { 254 if (log.isDebugEnabled()) { 255 log.debug(doFormatLogMessage(request, "Sync timeout")); 256 } 257 } 258 259 return locked; 260 } 261 262 /** 263 * Releases the {@link Lock} if present in the HttpSession. 264 */ 265 public static boolean simpleReleaseSyncOnSession(HttpServletRequest request) { 266 HttpSession session = request.getSession(false); 267 if (session == null) { 268 if (log.isDebugEnabled()) { 269 log.debug(doFormatLogMessage(request, 270 "No more HttpSession: can not unlock !, HttpSession must have been invalidated")); 271 } 272 return false; 273 } 274 log.debug("Trying to unlock on session " + session.getId() + " on Thread " + Thread.currentThread().getId()); 275 276 Lock lock = (Lock) session.getAttribute(SESSION_LOCK_KEY); 277 if (lock == null) { 278 log.error("Unable to find session lock, HttpSession may have been invalidated"); 279 return false; 280 } else { 281 lock.unlock(); 282 if (request.getAttribute(SYNCED_REQUEST_FLAG) != null) { 283 request.removeAttribute(SYNCED_REQUEST_FLAG); 284 } 285 if (log.isDebugEnabled()) { 286 log.debug("session unlocked on Thread "); 287 } 288 return true; 289 } 290 } 291 292 private static DateFormat httpExpiresDateFormat() { 293 // formatted http Expires: Thu, 01 Dec 1994 16:00:00 GMT 294 DateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); 295 df.setTimeZone(TimeZone.getTimeZone("GMT")); 296 return df; 297 } 298 299 /** 300 * Set cache parameters to httpResponse. 301 */ 302 public static void addCacheHeader(HttpServletResponse httpResponse, Boolean isPrivate, String cacheTime) { 303 if (isPrivate) { 304 httpResponse.addHeader("Cache-Control", "private, max-age=" + cacheTime); 305 } else { 306 httpResponse.addHeader("Cache-Control", "public, max-age=" + cacheTime); 307 } 308 // Generating expires using current date and adding cache time. 309 // we are using the format Expires: Thu, 01 Dec 1994 16:00:00 GMT 310 Date date = new Date(); 311 long newDate = date.getTime() + new Long(cacheTime) * 1000; 312 date.setTime(newDate); 313 314 httpResponse.setHeader("Expires", HTTP_EXPIRES_DATE_FORMAT.format(date)); 315 } 316 317 @Override 318 public void destroy() { 319 rcm = null; 320 } 321 322}