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