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.ServletContext;
039import javax.servlet.ServletException;
040import javax.servlet.ServletRequest;
041import javax.servlet.ServletResponse;
042import javax.servlet.http.HttpServletRequest;
043import javax.servlet.http.HttpServletResponse;
044import javax.servlet.http.HttpSession;
045
046import org.apache.commons.logging.Log;
047import org.apache.commons.logging.LogFactory;
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    // FIXME: typo in constant name.
069    protected static final int LOCK_TIMOUT_S = 120;
070
071    public static final DateFormat HTTP_EXPIRES_DATE_FORMAT = httpExpiresDateFormat();
072
073    protected static RequestControllerManager rcm;
074
075    private static final Log log = LogFactory.getLog(NuxeoRequestControllerFilter.class);
076
077    @Override
078    public void init(FilterConfig filterConfig) throws ServletException {
079        doInitIfNeeded();
080    }
081
082    private static void doInitIfNeeded() {
083        if (rcm == null) {
084            if (Framework.getRuntime() != null) {
085                rcm = Framework.getLocalService(RequestControllerManager.class);
086
087                if (rcm == null) {
088                    log.error("Unable to get RequestControllerManager service");
089                    // throw new ServletException(
090                    // "RequestControllerManager can not be found");
091                }
092                log.debug("Staring NuxeoRequestController filter");
093            } else {
094                log.debug("Postpone filter init since Runtime is not yet available");
095            }
096        }
097    }
098
099    public static String doFormatLogMessage(HttpServletRequest request, String info) {
100        String remoteHost = RemoteHostGuessExtractor.getRemoteHost(request);
101        Principal principal = request.getUserPrincipal();
102        String principalName = principal != null ? principal.getName() : "none";
103        String uri = request.getRequestURI();
104        HttpSession session = request.getSession(false);
105        String sessionId = session != null ? session.getId() : "none";
106        String threadName = Thread.currentThread().getName();
107        return "remote=" + remoteHost + ",principal=" + principalName + ",uri=" + uri + ",session=" + sessionId
108                + ",thread=" + threadName + ",info=" + info;
109    }
110
111    @Override
112    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
113            ServletException {
114
115        HttpServletRequest httpRequest = (HttpServletRequest) request;
116        HttpServletResponse httpResponse = (HttpServletResponse) response;
117
118        if (log.isDebugEnabled()) {
119            log.debug(doFormatLogMessage(httpRequest, "Entering NuxeoRequestController filter"));
120        }
121
122        ServletContext servletContext = httpRequest.getServletContext();
123        ServletHelper.setServletContext(servletContext);
124
125        doInitIfNeeded();
126
127        RequestFilterConfig config = rcm.getConfigForRequest(httpRequest);
128
129        boolean useSync = config.needSynchronization();
130        boolean useTx = config.needTransaction();
131
132        // Add cache header if needed
133        if (httpRequest.getMethod().equals("GET")) {
134            boolean isCached = config.isCached();
135            if (isCached) {
136                addCacheHeader(httpResponse, config.isPrivate(), config.getCacheTime());
137            }
138        }
139
140        if (!useSync && !useTx) {
141            if (log.isDebugEnabled()) {
142                log.debug(doFormatLogMessage(httpRequest, "Existing NuxeoRequestController filter: nothing to be done"));
143            }
144
145            try {
146                chain.doFilter(request, response);
147            } catch (ServletException e) {
148                if (DownloadHelper.isClientAbortError(e)) {
149                    DownloadHelper.logClientAbort(e);
150                } else {
151                    throw e;
152                }
153            }
154            return;
155        }
156
157        if (log.isDebugEnabled()) {
158            log.debug(doFormatLogMessage(httpRequest, "Handling request with tx=" + useTx + " and sync=" + useSync));
159        }
160
161        boolean sessionSynched = false;
162        if (useSync) {
163            sessionSynched = simpleSyncOnSession(httpRequest);
164        }
165        boolean txStarted = false;
166        try {
167            if (useTx) {
168                txStarted = ServletHelper.startTransaction(httpRequest);
169                if (txStarted) {
170                    if (config.needTransactionBuffered()) {
171                        response = new BufferingHttpServletResponse(httpResponse);
172                    }
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                    throw e;
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_TIMOUT_S, TimeUnit.SECONDS);
252        } catch (InterruptedException e) {
253            log.error(doFormatLogMessage(request, "Unable to acquire lock for Session sync"), e);
254            return false;
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    private static DateFormat httpExpiresDateFormat() {
302        // formatted http Expires: Thu, 01 Dec 1994 16:00:00 GMT
303        DateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
304        df.setTimeZone(TimeZone.getTimeZone("GMT"));
305        return df;
306    }
307
308    /**
309     * Set cache parameters to httpResponse.
310     */
311    public static void addCacheHeader(HttpServletResponse httpResponse, Boolean isPrivate, String cacheTime) {
312        if (isPrivate) {
313            httpResponse.setHeader("Cache-Control", "private, max-age=" + cacheTime);
314        } else {
315            httpResponse.setHeader("Cache-Control", "public, max-age=" + cacheTime);
316        }
317        // Generating expires using current date and adding cache time.
318        // we are using the format Expires: Thu, 01 Dec 1994 16:00:00 GMT
319        Date date = new Date();
320        long newDate = date.getTime() + new Long(cacheTime) * 1000;
321        date.setTime(newDate);
322
323        httpResponse.setHeader("Expires", HTTP_EXPIRES_DATE_FORMAT.format(date));
324    }
325
326    @Override
327    public void destroy() {
328        rcm = null;
329    }
330
331}