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}