Added alternate push API and example

This commit is contained in:
Greg Wilkins 2014-12-05 17:47:25 +01:00
parent 7b41e78f74
commit 06b1efc182
5 changed files with 457 additions and 2 deletions

View File

@ -44,7 +44,7 @@ import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlets.PushCacheFilter;
import org.eclipse.jetty.servlets.PushSessionCacheFilter;
import org.eclipse.jetty.util.ssl.SslContextFactory;
@ -59,7 +59,7 @@ public class Http2Server
ServletContextHandler context = new ServletContextHandler(server, "/",ServletContextHandler.SESSIONS);
context.setResourceBase("/tmp");
context.addFilter(PushCacheFilter.class,"/*",EnumSet.of(DispatcherType.REQUEST))
context.addFilter(PushSessionCacheFilter.class,"/*",EnumSet.of(DispatcherType.REQUEST))
.setInitParameter("ports","443,6443,8443");
context.addServlet(new ServletHolder(servlet), "/test/*");
context.addServlet(DefaultServlet.class, "/").setInitParameter("maxCacheSize","81920");

View File

@ -217,6 +217,7 @@ public class Dispatcher implements RequestDispatcher
}
}
@Deprecated
public void push(ServletRequest request)
{
Request baseRequest = Request.getBaseRequest(request);

View File

@ -0,0 +1,157 @@
//
// ========================================================================
// Copyright (c) 1995-2014 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.server;
import java.util.Collection;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.util.URIUtil;
/* ------------------------------------------------------------ */
/**
*
*/
public class PushBuilder
{
private final Request _request;
private final HttpFields _fields;
private String _method;
private String _queryString;
private String _sessionId;
private boolean _conditional;
public PushBuilder(Request request, HttpFields fields, String method, String queryString, String sessionId, boolean conditional)
{
super();
_request = request;
_fields = fields;
_method = method;
_queryString = queryString;
_sessionId = sessionId;
_conditional = conditional;
}
public String getMethod()
{
return _method;
}
public void setMethod(String method)
{
_method = method;
}
public String getQueryString()
{
return _queryString;
}
public void setQueryString(String queryString)
{
_queryString = queryString;
}
public String getSessionId()
{
return _sessionId;
}
public void setSessionId(String sessionId)
{
_sessionId = sessionId;
}
public boolean isConditional()
{
return _conditional;
}
public void setConditional(boolean conditional)
{
_conditional = conditional;
}
public Collection<String> getHeaderNames()
{
return _fields.getFieldNamesCollection();
}
public String getHeader(String name)
{
return _fields.get(name);
}
public void setHeader(String name,String value)
{
_fields.put(name,value);
}
public void addHeader(String name,String value)
{
_fields.add(name,value);
}
/* ------------------------------------------------------------ */
/** Push a resource.
* Push a resource based on the current state of the PushBuilder. If {@link #isConditional()}
* is true and an etag or lastModified value is provided, then an appropriate conditional header
* will be generated. If an etag and lastModified value are provided only an If-None-Match header
* will be generated. If the builder has a session ID, then the pushed request
* will include the session ID either as a Cookie or as a URI parameter as appropriate.The builders
* query string is merged with any passed query string.
* @param uriInContext The URI within the current context of the resource to push.
* @param etag The etag for the resource or null if not available
* @param lastModified The last modified date of the resource or null if not available
* @throws IllegalArgumentException if the method set expects a request
* body (eg POST)
*/
public void push(String uriInContext,String etag,String lastModified)
{
if (HttpMethod.POST.is(_method) || HttpMethod.PUT.is(_method))
throw new IllegalStateException("Bad Method "+_method);
String query=_queryString;
int q=uriInContext.indexOf('?');
if (q>=0)
{
query=uriInContext.substring(q+1)+'&'+query;
uriInContext=uriInContext.substring(0,q);
}
String path = URIUtil.addPaths(_request.getContextPath(),uriInContext);
String param=null;
if (_sessionId!=null && _request.isRequestedSessionIdFromURL())
param="jsessionid="+_sessionId;
if (_conditional)
{
if (etag!=null)
_fields.add(HttpHeader.IF_NONE_MATCH,etag);
else if (lastModified!=null)
_fields.add(HttpHeader.IF_MODIFIED_SINCE,lastModified);
}
HttpURI uri = HttpURI.createHttpURI(_request.getScheme(),_request.getServerName(),_request.getServerPort(),path,param,query,null);
MetaData.Request push = new MetaData.Request(_method,uri,_request.getHttpVersion(),_fields);
_request.getHttpChannel().getHttpTransport().push(push);
}
}

View File

@ -213,6 +213,110 @@ public class Request implements HttpServletRequest
{
return _input;
}
/* ------------------------------------------------------------ */
/** Get a PushBuilder associated with this request initialized as follows:<ul>
* <li>The method is initialized to "GET"</li>
* <li>The headers from this request are copied to the Builder, except for:<ul>
* <li>Conditional headers (eg. If-Modified-Since)
* <li>Range headers
* <li>Expect headers
* <li>Authorization headers
* <li>Referrer headers
* </ul></li>
* <li>If the request was Authenticated, an Authorization header will
* be set with a container generated token that will result in equivalent
* Authorization</li>
* <li>The query string from {@link #getQueryString()}
* <li>The {@link #getRequestedSessionId()} value, unless at the time
* of the call {@link #getSession(boolean)}
* has previously been called to create a new {@link HttpSession}, in
* which case the new session ID will be used as the PushBuilders
* requested session ID.</li>
* <li>The source of the requested session id will be the same as for
* this request</li>
* <li>The builders Referer header will be set to {@link #getRequestURL()}
* plus any {@link #getQueryString()} </li>
* <li>If {@link HttpServletResponse#addCookie(Cookie)} has been called
* on the associated response, then a corresponding Cookie header will be added
* to the PushBuilder, unless the {@link Cookie#getMaxAge()} is <=0, in which
* case the Cookie will be removed from the builder.</li>
* <li>If this request has has the conditional headers If-Modified-Since or
* If-None-Match then the {@link PushBuilder#isConditional()} header is set
* to true.
* </ul>
*
* <p>Each call to getPushBuilder() will return a new instance
* of a PushBuilder based off this Request. Any mutations to the
* returned PushBuilder are not reflected on future returns.
* @return A new PushBuilder or null if push is not supported
*/
public PushBuilder getPushBuilder()
{
HttpFields fields = new HttpFields(getHttpFields().size()+5);
boolean conditional=false;
UserIdentity user_identity=null;
Authentication authentication=null;
for (HttpField field : getHttpFields())
{
HttpHeader header = field.getHeader();
if (header==null)
fields.add(field);
else
{
switch(header)
{
case IF_MATCH:
case IF_RANGE:
case IF_UNMODIFIED_SINCE:
case RANGE:
case EXPECT:
case REFERER:
case COOKIE:
continue;
case AUTHORIZATION:
user_identity=getUserIdentity();
authentication=_authentication;
continue;
case IF_NONE_MATCH:
case IF_MODIFIED_SINCE:
conditional=true;
continue;
default:
fields.add(field);
}
}
}
String id=null;
try
{
HttpSession session = getSession();
if (session!=null)
{
session.getLastAccessedTime(); // checks if session is valid
id=session.getId();
}
else
id=getRequestedSessionId();
}
catch(IllegalStateException e)
{
id=getRequestedSessionId();
}
PushBuilder builder = new PushBuilder(this,fields,getMethod(),getQueryString(),id,conditional);
builder.addHeader("referer",getRequestURL().toString());
// TODO process any set cookies
// TODO process any user_identity
return builder;
}
/* ------------------------------------------------------------ */
public void addEventListener(final EventListener listener)

View File

@ -0,0 +1,193 @@
//
// ========================================================================
// Copyright (c) 1995-2014 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.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.HttpFields;
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.URIUtil;
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 Logger LOG = Log.getLogger(PushSessionCacheFilter.class);
private final ConcurrentMap<String, Target> _cache = new ConcurrentHashMap<>();
private long _associateDelay=5000L;
/* ------------------------------------------------------------ */
/**
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
@Override
public void init(FilterConfig config) throws ServletException
{
if (config.getInitParameter("associateDelay")!=null)
_associateDelay=Long.valueOf(config.getInitParameter("associateDelay"));
config.getServletContext().addListener(new ServletRequestListener()
{
@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);
// Does this request have a referer?
String referer = request.getHttpFields().get(HttpHeader.REFERER);
if (referer!=null)
{
// Is the referer from this contexts?
HttpURI uri = new HttpURI(referer);
String path = uri.getPath();
if (request.getServerName().equals(uri.getHost()) && path.startsWith(request.getContextPath()))
{
String path_in_ctx = path.substring(request.getContextPath().length());
Target referer_target = _cache.get(path_in_ctx);
if (referer_target!=null)
{
String sessionId = request.getSession(true).getId();
Long last = referer_target._timestamp.get(sessionId);
if (last!=null && (System.currentTimeMillis()-last)<_associateDelay && !referer_target._associated.containsKey(path))
{
if (referer_target._associated.putIfAbsent(path,target)==null)
LOG.info("ASSOCIATE {}->{}",path_in_ctx,target._path);
}
}
}
}
}
@Override
public void requestInitialized(ServletRequestEvent sre)
{
}
});
}
/* ------------------------------------------------------------ */
/**
* @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
Request baseRequest = Request.getBaseRequest(request);
// Iterating over fields is more efficient than multiple gets
HttpFields fields = baseRequest.getHttpFields();
String referer=fields.get(HttpHeader.REFERER);
if (LOG.isDebugEnabled())
LOG.debug("{} {} referer={}%n",baseRequest.getMethod(),baseRequest.getRequestURI(),referer);
HttpSession session = baseRequest.getSession(true);
String sessionId = session.getId();
String path = URIUtil.addPaths(baseRequest.getServletPath(),baseRequest.getPathInfo());
// find the target for this resource
Target target = _cache.get(path);
if (target == null)
{
Target t=new Target(path);
target = _cache.putIfAbsent(path,t);
target = target==null?t:target;
}
target._timestamp.put(sessionId,System.currentTimeMillis());
request.setAttribute(TARGET_ATTR,target);
// push any associated resources
if (target._associated.size()>0)
{
PushBuilder builder = baseRequest.getPushBuilder();
if (!session.isNew())
builder.setConditional(true);
if (builder!=null)
{
for (Target associated : target._associated.values())
{
LOG.info("PUSH {}->{}",path,associated._path);
builder.push(associated._path,associated._etag,associated._lastModified);
}
}
}
chain.doFilter(request,response);
}
/* ------------------------------------------------------------ */
/**
* @see javax.servlet.Filter#destroy()
*/
@Override
public void destroy()
{
}
public static class Target
{
final String _path;
final ConcurrentMap<String,Target> _associated = new ConcurrentHashMap<>();
final ConcurrentMap<String,Long> _timestamp = new ConcurrentHashMap<>();
volatile String _etag;
volatile String _lastModified;
public Target(String path)
{
_path=path;
}
}
}