blob: 7d86dcad3c52dce1ff2a0449846c1bcc9f393f7a [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.servlets;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpSession;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.PushBuilder;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class PushSessionCacheFilter implements Filter
{
private static final String TARGET_ATTR = "PushCacheFilter.target";
private static final String TIMESTAMP_ATTR = "PushCacheFilter.timestamp";
private static final Logger LOG = Log.getLogger(PushSessionCacheFilter.class);
private final ConcurrentMap<String, Target> _cache = new ConcurrentHashMap<>();
private long _associateDelay = 5000L;
@Override
public void init(FilterConfig config) throws ServletException
{
if (config.getInitParameter("associateDelay") != null)
_associateDelay = Long.valueOf(config.getInitParameter("associateDelay"));
// Add a listener that is used to collect information about associated resource,
// etags and modified dates
config.getServletContext().addListener(new ServletRequestListener()
{
// Collect information when request is destroyed.
@Override
public void requestDestroyed(ServletRequestEvent sre)
{
Request request = Request.getBaseRequest(sre.getServletRequest());
Target target = (Target)request.getAttribute(TARGET_ATTR);
if (target == null)
return;
// Update conditional data
Response response = request.getResponse();
target._etag = response.getHttpFields().get(HttpHeader.ETAG);
target._lastModified = response.getHttpFields().get(HttpHeader.LAST_MODIFIED);
// Don't associate pushes
if (request.isPush())
{
if (LOG.isDebugEnabled())
LOG.debug("Pushed {} for {}", request.getResponse().getStatus(), request.getRequestURI());
return;
}
else if (LOG.isDebugEnabled())
{
LOG.debug("Served {} for {}", request.getResponse().getStatus(), request.getRequestURI());
}
// Does this request have a referer?
String referer = request.getHttpFields().get(HttpHeader.REFERER);
if (referer != null)
{
// Is the referer from this contexts?
HttpURI referer_uri = new HttpURI(referer);
if (request.getServerName().equals(referer_uri.getHost()))
{
Target referer_target = _cache.get(referer_uri.getPath());
if (referer_target != null)
{
HttpSession session = request.getSession();
ConcurrentHashMap<String, Long> timestamps = (ConcurrentHashMap<String, Long>)session.getAttribute(TIMESTAMP_ATTR);
Long last = timestamps.get(referer_target._path);
if (last != null && (System.currentTimeMillis() - last) < _associateDelay)
{
if (referer_target._associated.putIfAbsent(target._path, target) == null)
{
if (LOG.isDebugEnabled())
LOG.debug("ASSOCIATE {}->{}", referer_target._path, target._path);
}
}
}
}
}
}
@Override
public void requestInitialized(ServletRequestEvent sre)
{
}
});
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
// Get Jetty request as these APIs are not yet standard
Request baseRequest = Request.getBaseRequest(request);
String uri = baseRequest.getRequestURI();
if (LOG.isDebugEnabled())
LOG.debug("{} {} push={}", baseRequest.getMethod(), uri, baseRequest.isPush());
HttpSession session = baseRequest.getSession(true);
// find the target for this resource
Target target = _cache.get(uri);
if (target == null)
{
Target t = new Target(uri);
target = _cache.putIfAbsent(uri, t);
target = target == null ? t : target;
}
request.setAttribute(TARGET_ATTR, target);
// Set the timestamp for this resource in this session
ConcurrentHashMap<String, Long> timestamps = (ConcurrentHashMap<String, Long>)session.getAttribute(TIMESTAMP_ATTR);
if (timestamps == null)
{
timestamps = new ConcurrentHashMap<>();
session.setAttribute(TIMESTAMP_ATTR, timestamps);
}
timestamps.put(uri, System.currentTimeMillis());
// push any associated resources
if (baseRequest.isPushSupported() && !baseRequest.isPush() && !target._associated.isEmpty())
{
// Breadth-first push of associated resources.
Queue<Target> queue = new ArrayDeque<>();
queue.offer(target);
while (!queue.isEmpty())
{
Target parent = queue.poll();
PushBuilder builder = baseRequest.getPushBuilder();
builder.addHeader("X-Pusher", PushSessionCacheFilter.class.toString());
for (Target child : parent._associated.values())
{
queue.offer(child);
String path = child._path;
if (LOG.isDebugEnabled())
LOG.debug("PUSH {} <- {}", path, uri);
builder.path(path).etag(child._etag).lastModified(child._lastModified).push();
}
}
}
chain.doFilter(request, response);
}
@Override
public void destroy()
{
_cache.clear();
}
private static class Target
{
private final String _path;
private final ConcurrentMap<String, Target> _associated = new ConcurrentHashMap<>();
private volatile String _etag;
private volatile String _lastModified;
private Target(String path)
{
_path = path;
}
@Override
public String toString()
{
return String.format("Target{p=%s,e=%s,m=%s,a=%d}", _path, _etag, _lastModified, _associated.size());
}
}
}