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}