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.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.ServletContext; 037import javax.servlet.ServletException; 038import javax.servlet.ServletRequest; 039import javax.servlet.ServletResponse; 040import javax.servlet.http.HttpServletRequest; 041import javax.servlet.http.HttpServletResponse; 042import javax.servlet.http.HttpSession; 043 044import org.apache.commons.lang3.time.FastDateFormat; 045import org.apache.commons.logging.Log; 046import org.apache.commons.logging.LogFactory; 047import org.nuxeo.ecm.core.api.NuxeoException; 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 protected static final int LOCK_TIMEOUT_S = 120; 069 070 // formatted http Expires: Thu, 01 Dec 1994 16:00:00 GMT 071 public static final FastDateFormat HTTP_EXPIRES_DATE_FORMAT = FastDateFormat.getInstance( 072 "EEE, dd MMM yyyy HH:mm:ss z", TimeZone.getTimeZone("GMT"), Locale.US); 073 074 protected static RequestControllerManager rcm; 075 076 private static final Log log = LogFactory.getLog(NuxeoRequestControllerFilter.class); 077 078 @Override 079 public void init(FilterConfig filterConfig) throws ServletException { 080 doInitIfNeeded(); 081 } 082 083 private static void doInitIfNeeded() { 084 if (rcm == null) { 085 if (Framework.getRuntime() != null) { 086 rcm = Framework.getService(RequestControllerManager.class); 087 088 if (rcm == null) { 089 log.error("Unable to get RequestControllerManager service"); 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 ServletContext servletContext = httpRequest.getServletContext(); 122 ServletHelper.setServletContext(servletContext); 123 124 doInitIfNeeded(); 125 126 RequestFilterConfig config = rcm.getConfigForRequest(httpRequest); 127 128 boolean useSync = config.needSynchronization(); 129 boolean useTx = config.needTransaction(); 130 131 // Add cache header if needed 132 if (httpRequest.getMethod().equals("GET")) { 133 boolean isCached = config.isCached(); 134 if (isCached) { 135 addCacheHeader(httpResponse, config.isPrivate(), config.getCacheTime()); 136 } 137 } 138 139 if (!useSync && !useTx) { 140 if (log.isDebugEnabled()) { 141 log.debug(doFormatLogMessage(httpRequest, "Existing NuxeoRequestController filter: nothing to be done")); 142 } 143 144 try { 145 chain.doFilter(request, response); 146 } catch (ServletException e) { 147 if (DownloadHelper.isClientAbortError(e)) { 148 DownloadHelper.logClientAbort(e); 149 } else { 150 throw e; 151 } 152 } 153 return; 154 } 155 156 if (log.isDebugEnabled()) { 157 log.debug(doFormatLogMessage(httpRequest, "Handling request with tx=" + useTx + " and sync=" + useSync)); 158 } 159 160 boolean sessionSynched = false; 161 if (useSync) { 162 sessionSynched = simpleSyncOnSession(httpRequest); 163 } 164 boolean txStarted = false; 165 try { 166 if (useTx && !TransactionHelper.isTransactionActiveOrMarkedRollback()) { 167 txStarted = ServletHelper.startTransaction(httpRequest); 168 if (!txStarted) { 169 throw new ServletException("Failed to start transaction"); 170 } 171 if (config.needTransactionBuffered()) { 172 response = new BufferingHttpServletResponse(httpResponse); 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 log.error(e); // don't rethrow inside finally 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_TIMEOUT_S, TimeUnit.SECONDS); 252 } catch (InterruptedException e) { 253 Thread.currentThread().interrupt(); 254 throw new NuxeoException(e); 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 /** 302 * Set cache parameters to httpResponse. 303 */ 304 public static void addCacheHeader(HttpServletResponse httpResponse, Boolean isPrivate, String cacheTime) { 305 if (isPrivate) { 306 httpResponse.setHeader("Cache-Control", "private, max-age=" + cacheTime); 307 } else { 308 httpResponse.setHeader("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}