Added alternate push API and example
This commit is contained in:
parent
7b41e78f74
commit
06b1efc182
|
@ -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");
|
||||
|
|
|
@ -217,6 +217,7 @@ public class Dispatcher implements RequestDispatcher
|
|||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void push(ServletRequest request)
|
||||
{
|
||||
Request baseRequest = Request.getBaseRequest(request);
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue