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