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}