Merge branch 'master' into jetty-9.4.x-Feature

This commit is contained in:
Greg Wilkins 2015-12-16 17:16:14 +11:00
commit 57ce87770a
65 changed files with 4444 additions and 467 deletions

View File

@ -31,6 +31,26 @@ jetty-9.3.6.v20151106 - 06 November 2015
+ 481437 Port ConnectHandler connect and context functionality from Jetty 8.
+ 481554 DispatcherType reset race
jetty-9.2.14.v20151106 - 06 November 2015
+ 428474 Expose batch mode in the Jetty WebSocket API
+ 471055 Restore legacy/experimental WebSocket extensions (deflate-frame)
+ 472082 isOpen returns true on CLOSING Connection
+ 474068 Update WebSocket Extension for permessage-deflate draft-22
+ 474319 Reintroduce blocking connect().
+ 474321 Allow synchronous address resolution.
+ 474453 Tiny buffers (under 7 bytes) fail to compress in permessage-deflate
+ 474454 Backport permessage-deflate from Jetty 9.3.x to 9.2.x
+ 474936 WebSocketSessions are not always cleaned out from openSessions
+ 476023 Incorrect trimming of WebSocket close reason
+ 476049 When using WebSocket Session.close() there should be no status code
or reason sent
+ 477385 Problem in MANIFEST.MF with version 9.2.10 / 9.2.13.
+ 477817 Fixed memory leak in QueuedThreadPool
+ 481006 SSL requests intermittently fail with EOFException when SSL
renegotiation is disallowed.
+ 481236 Make ShutdownMonitor java security manager friendly
+ 481437 Port ConnectHandler connect and context functionality from Jetty 8.
jetty-9.3.5.v20151012 - 12 October 2015
+ 479343 calls to MetaData#orderFragments() with relative ordering adds
duplicate jars

View File

@ -19,6 +19,8 @@
package org.eclipse.jetty.annotations;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.servlet.Servlet;
import javax.servlet.annotation.WebInitParam;
@ -28,6 +30,7 @@ import javax.servlet.http.HttpServlet;
import org.eclipse.jetty.servlet.Holder;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlet.ServletMapping;
import org.eclipse.jetty.util.ArrayUtil;
import org.eclipse.jetty.util.LazyList;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ -50,13 +53,13 @@ public class WebServletAnnotation extends DiscoveredAnnotation
{
super(context, className);
}
public WebServletAnnotation (WebAppContext context, String className, Resource resource)
{
super(context, className, resource);
}
/**
* @see DiscoveredAnnotation#apply()
*/
@ -104,10 +107,11 @@ public class WebServletAnnotation extends DiscoveredAnnotation
String servletName = (annotation.name().equals("")?clazz.getName():annotation.name());
MetaData metaData = _context.getMetaData();
ServletMapping mapping = null; //the new mapping
//Find out if a <servlet> already exists with this name
ServletHolder[] holders = _context.getServletHandler().getServlets();
boolean isNew = true;
ServletHolder holder = null;
if (holders != null)
{
@ -116,13 +120,13 @@ public class WebServletAnnotation extends DiscoveredAnnotation
if (h.getName() != null && servletName.equals(h.getName()))
{
holder = h;
isNew = false;
break;
}
}
}
if (isNew)
//handle creation/completion of a servlet
if (holder == null)
{
//No servlet of this name has already been defined, either by a descriptor
//or another annotation (which would be impossible).
@ -147,11 +151,11 @@ public class WebServletAnnotation extends DiscoveredAnnotation
}
_context.getServletHandler().addServlet(holder);
ServletMapping mapping = new ServletMapping();
mapping = new ServletMapping();
mapping.setServletName(holder.getName());
mapping.setPathSpecs( LazyList.toStringArray(urlPatternList));
_context.getServletHandler().addServletMapping(mapping);
metaData.setOrigin(servletName+".servlet.mappings",annotation,clazz);
}
else
{
@ -175,54 +179,102 @@ public class WebServletAnnotation extends DiscoveredAnnotation
}
}
//check the url-patterns
//ServletSpec 3.0 p81 If a servlet already has url mappings from a
//webxml or fragment descriptor the annotation is ignored. However, we want to be able to
//replace mappings that were given in webdefault.xml
boolean mappingsExist = false;
boolean anyNonDefaults = false;
ServletMapping[] allMappings = _context.getServletHandler().getServletMappings();
if (allMappings != null)
{
for (ServletMapping m:allMappings)
{
if (m.getServletName() != null && servletName.equals(m.getServletName()))
{
mappingsExist = true;
if (!m.isDefault())
{
anyNonDefaults = true;
break;
}
}
}
}
//webxml or fragment descriptor the annotation is ignored.
//However, we want to be able to replace mappings that were given in webdefault.xml
List<ServletMapping> existingMappings = getServletMappingsForServlet(servletName);
if (anyNonDefaults)
return; //if any mappings already set by a descriptor that is not webdefault.xml, we're done
boolean clash = false;
if (mappingsExist)
//if any mappings for this servlet already set by a descriptor that is not webdefault.xml forget
//about processing these url mappings
if (existingMappings.isEmpty() || !containsNonDefaultMappings(existingMappings))
{
for (String p:urlPatternList)
{
ServletMapping m = _context.getServletHandler().getServletMapping(p);
if (m != null && !m.isDefault())
{
//trying to override a servlet-mapping that was added not by webdefault.xml
clash = true;
break;
}
}
}
if (!mappingsExist || !clash)
{
ServletMapping m = new ServletMapping();
m.setServletName(servletName);
m.setPathSpecs(LazyList.toStringArray(urlPatternList));
_context.getServletHandler().addServletMapping(m);
mapping = new ServletMapping();
mapping.setServletName(servletName);
mapping.setPathSpecs(LazyList.toStringArray(urlPatternList));
}
}
//We also want to be able to replace mappings that were defined in webdefault.xml
//that were for a different servlet eg a mapping in webdefault.xml for / to the jetty
//default servlet should be able to be replaced by an annotation for / to a different
//servlet
if (mapping != null)
{
//url mapping was permitted by annotation processing rules
//take a copy of the existing servlet mappings that we can iterate over and remove from. This is
//because the ServletHandler interface does not support removal of individual mappings.
List<ServletMapping> allMappings = ArrayUtil.asMutableList(_context.getServletHandler().getServletMappings());
//for each of the urls in the annotation, check if a mapping to same/different servlet exists
// if mapping exists and is from a default descriptor, it can be replaced. NOTE: we do not
// guard against duplicate path mapping here: that is the job of the ServletHandler
for (String p:urlPatternList)
{
ServletMapping existingMapping = _context.getServletHandler().getServletMapping(p);
if (existingMapping != null && existingMapping.isDefault())
{
String[] updatedPaths = ArrayUtil.removeFromArray(existingMapping.getPathSpecs(), p);
//if we removed the last path from a servletmapping, delete the servletmapping
if (updatedPaths == null || updatedPaths.length == 0)
{
boolean success = allMappings.remove(existingMapping);
if (LOG.isDebugEnabled()) LOG.debug("Removed empty mapping {} from defaults descriptor success:{}",existingMapping, success);
}
else
{
existingMapping.setPathSpecs(updatedPaths);
if (LOG.isDebugEnabled()) LOG.debug("Removed path {} from mapping {} from defaults descriptor ", p,existingMapping);
}
}
_context.getMetaData().setOrigin(servletName+".servlet.mapping."+p, annotation, clazz);
}
allMappings.add(mapping);
_context.getServletHandler().setServletMappings(allMappings.toArray(new ServletMapping[allMappings.size()]));
}
}
/**
* @param name
* @return
*/
private List<ServletMapping> getServletMappingsForServlet (String name)
{
ServletMapping[] allMappings = _context.getServletHandler().getServletMappings();
if (allMappings == null)
return Collections.emptyList();
List<ServletMapping> mappings = new ArrayList<ServletMapping>();
for (ServletMapping m:allMappings)
{
if (m.getServletName() != null && name.equals(m.getServletName()))
{
mappings.add(m);
}
}
return mappings;
}
/**
* @param mappings
* @return
*/
private boolean containsNonDefaultMappings (List<ServletMapping> mappings)
{
if (mappings == null)
return false;
for (ServletMapping m:mappings)
{
if (!m.isDefault())
return true;
}
return false;
}
}

View File

@ -0,0 +1,31 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.annotations;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
@WebServlet(urlPatterns = { "/", "/bah/*" }, name="DServlet", initParams={@WebInitParam(name="x", value="y")}, loadOnStartup=1, asyncSupported=false)
public class ServletD extends HttpServlet
{
}

View File

@ -18,12 +18,6 @@
package org.eclipse.jetty.annotations;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -37,6 +31,13 @@ import org.eclipse.jetty.webapp.DiscoveredAnnotation;
import org.eclipse.jetty.webapp.WebAppContext;
import org.junit.Test;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* TestServletAnnotations
*/
@ -59,7 +60,7 @@ public class TestServletAnnotations
_list.add(a);
}
}
@Test
public void testServletAnnotation() throws Exception
{
@ -69,9 +70,9 @@ public class TestServletAnnotations
WebAppContext wac = new WebAppContext();
List<DiscoveredAnnotation> results = new ArrayList<DiscoveredAnnotation>();
TestWebServletAnnotationHandler handler = new TestWebServletAnnotationHandler(wac, results);
parser.parse(Collections.singleton(handler), classes, new ClassNameResolver ()
{
public boolean isExcluded(String name)
@ -85,7 +86,7 @@ public class TestServletAnnotations
}
});
assertEquals(1, results.size());
assertTrue(results.get(0) instanceof WebServletAnnotation);
@ -94,14 +95,14 @@ public class TestServletAnnotations
ServletHolder[] holders = wac.getServletHandler().getServlets();
assertNotNull(holders);
assertEquals(1, holders.length);
// Verify servlet annotations
ServletHolder cholder = holders[0];
assertThat("Servlet Name", cholder.getName(), is("CServlet"));
assertThat("InitParameter[x]", cholder.getInitParameter("x"), is("y"));
assertThat("Init Order", cholder.getInitOrder(), is(2));
assertThat("Async Supported", cholder.isAsyncSupported(), is(false));
// Verify mappings
ServletMapping[] mappings = wac.getServletHandler().getServletMappings();
assertNotNull(mappings);
@ -111,6 +112,195 @@ public class TestServletAnnotations
assertEquals(2, paths.length);
}
@Test
public void testWebServletAnnotationOverrideDefault () throws Exception
{
//if the existing servlet mapping TO A DIFFERENT SERVLET IS from a default descriptor we
//DO allow the annotation to replace the mapping.
WebAppContext wac = new WebAppContext();
ServletHolder defaultServlet = new ServletHolder();
defaultServlet.setClassName("org.eclipse.jetty.servlet.DefaultServlet");
defaultServlet.setName("default");
wac.getServletHandler().addServlet(defaultServlet);
ServletMapping m = new ServletMapping();
m.setPathSpec("/");
m.setServletName("default");
m.setDefault(true); //this mapping will be from a default descriptor
wac.getServletHandler().addServletMapping(m);
WebServletAnnotation annotation = new WebServletAnnotation(wac, "org.eclipse.jetty.annotations.ServletD", null);
annotation.apply();
//test that as the original servlet mapping had only 1 pathspec, then the whole
//servlet mapping should be deleted as that pathspec will be remapped to the DServlet
ServletMapping[] resultMappings = wac.getServletHandler().getServletMappings();
assertNotNull(resultMappings);
assertEquals(1, resultMappings.length);
assertEquals(2, resultMappings[0].getPathSpecs().length);
resultMappings[0].getServletName().equals("DServlet");
for (String s:resultMappings[0].getPathSpecs())
{
assertTrue (s.equals("/") || s.equals("/bah/*"));
}
}
@Test
public void testWebServletAnnotationReplaceDefault () throws Exception
{
//if the existing servlet mapping TO A DIFFERENT SERVLET IS from a default descriptor we
//DO allow the annotation to replace the mapping.
WebAppContext wac = new WebAppContext();
ServletHolder defaultServlet = new ServletHolder();
defaultServlet.setClassName("org.eclipse.jetty.servlet.DefaultServlet");
defaultServlet.setName("default");
wac.getServletHandler().addServlet(defaultServlet);
ServletMapping m = new ServletMapping();
m.setPathSpec("/");
m.setServletName("default");
m.setDefault(true); //this mapping will be from a default descriptor
wac.getServletHandler().addServletMapping(m);
ServletMapping m2 = new ServletMapping();
m2.setPathSpec("/other");
m2.setServletName("default");
m2.setDefault(true); //this mapping will be from a default descriptor
wac.getServletHandler().addServletMapping(m2);
WebServletAnnotation annotation = new WebServletAnnotation(wac, "org.eclipse.jetty.annotations.ServletD", null);
annotation.apply();
//test that only the mapping for "/" was removed from the mappings to the default servlet
ServletMapping[] resultMappings = wac.getServletHandler().getServletMappings();
assertNotNull(resultMappings);
assertEquals(2, resultMappings.length);
for (ServletMapping r:resultMappings)
{
if (r.getServletName().equals("default"))
{
assertEquals(1,r.getPathSpecs().length);
assertEquals("/other", r.getPathSpecs()[0]);
}
else if (r.getServletName().equals("DServlet"))
{
assertEquals(2,r.getPathSpecs().length);
for (String p:r.getPathSpecs())
{
if (!p.equals("/") && !p.equals("/bah/*"))
fail("Unexpected path");
}
}
else
fail("Unexpected servlet mapping");
}
}
@Test
public void testWebServletAnnotationNotOverride () throws Exception
{
//if the existing servlet mapping TO A DIFFERENT SERVLET IS NOT from a default descriptor we
//DO NOT allow the annotation to replace the mapping
WebAppContext wac = new WebAppContext();
ServletHolder servlet = new ServletHolder();
servlet.setClassName("org.eclipse.jetty.servlet.FooServlet");
servlet.setName("foo");
wac.getServletHandler().addServlet(servlet);
ServletMapping m = new ServletMapping();
m.setPathSpec("/");
m.setServletName("foo");
wac.getServletHandler().addServletMapping(m);
WebServletAnnotation annotation = new WebServletAnnotation(wac, "org.eclipse.jetty.annotations.ServletD", null);
annotation.apply();
ServletMapping[] resultMappings = wac.getServletHandler().getServletMappings();
assertEquals(2, resultMappings.length);
for (ServletMapping r:resultMappings)
{
if (r.getServletName().equals("DServlet"))
{
assertEquals(2, r.getPathSpecs().length);
}
else if (r.getServletName().equals("foo"))
{
assertEquals(1, r.getPathSpecs().length);
}
else
fail("Unexpected servlet name");
}
}
@Test
public void testWebServletAnnotationIgnore () throws Exception
{
//an existing servlet OF THE SAME NAME has even 1 non-default mapping we can't use
//any of the url mappings in the annotation
WebAppContext wac = new WebAppContext();
ServletHolder servlet = new ServletHolder();
servlet.setClassName("org.eclipse.jetty.servlet.OtherDServlet");
servlet.setName("DServlet");
wac.getServletHandler().addServlet(servlet);
ServletMapping m = new ServletMapping();
m.setPathSpec("/default");
m.setDefault(true);
m.setServletName("DServlet");
wac.getServletHandler().addServletMapping(m);
ServletMapping m2 = new ServletMapping();
m2.setPathSpec("/other");
m2.setServletName("DServlet");
wac.getServletHandler().addServletMapping(m2);
WebServletAnnotation annotation = new WebServletAnnotation(wac, "org.eclipse.jetty.annotations.ServletD", null);
annotation.apply();
ServletMapping[] resultMappings = wac.getServletHandler().getServletMappings();
assertEquals(2, resultMappings.length);
for (ServletMapping r:resultMappings)
{
assertEquals(1, r.getPathSpecs().length);
if (!r.getPathSpecs()[0].equals("/default") && !r.getPathSpecs()[0].equals("/other"))
fail("Unexpected path in mapping");
}
}
@Test
public void testWebServletAnnotationNoMappings () throws Exception
{
//an existing servlet OF THE SAME NAME has no mappings, therefore all mappings in the annotation
//should be accepted
WebAppContext wac = new WebAppContext();
ServletHolder servlet = new ServletHolder();
servlet.setName("foo");
wac.getServletHandler().addServlet(servlet);
WebServletAnnotation annotation = new WebServletAnnotation(wac, "org.eclipse.jetty.annotations.ServletD", null);
annotation.apply();
ServletMapping[] resultMappings = wac.getServletHandler().getServletMappings();
assertEquals(1, resultMappings.length);
assertEquals(2, resultMappings[0].getPathSpecs().length);
for (String s:resultMappings[0].getPathSpecs())
{
if (!s.equals("/") && !s.equals("/bah/*"))
fail("Unexpected path mapping");
}
}
@Test
public void testDeclareRoles ()
throws Exception
{

View File

@ -276,12 +276,18 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
private void process()
{
Connection connection = connectionPool.acquire();
if (connection != null)
process(connection);
while (true)
{
Connection connection = connectionPool.acquire();
if (connection == null)
break;
boolean proceed = process(connection);
if (!proceed)
break;
}
}
public void process(final Connection connection)
public boolean process(final Connection connection)
{
HttpClient client = getHttpClient();
final HttpExchange exchange = getHttpExchanges().poll();
@ -291,13 +297,13 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
{
if (!connectionPool.release(connection))
connection.close();
if (!client.isRunning())
{
if (LOG.isDebugEnabled())
LOG.debug("{} is stopping", client);
connection.close();
}
return false;
}
else
{
@ -316,6 +322,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
{
send(connection, exchange);
}
return getHttpExchanges().peek() != null;
}
}

View File

@ -324,27 +324,34 @@ public abstract class HttpReceiver
}
else
{
List<ByteBuffer> decodeds = new ArrayList<>(2);
while (buffer.hasRemaining())
try
{
ByteBuffer decoded = decoder.decode(buffer);
if (!decoded.hasRemaining())
continue;
decodeds.add(decoded);
if (LOG.isDebugEnabled())
LOG.debug("Response content decoded ({}) {}{}{}", decoder, response, System.lineSeparator(), BufferUtil.toDetailString(decoded));
}
List<ByteBuffer> decodeds = new ArrayList<>(2);
while (buffer.hasRemaining())
{
ByteBuffer decoded = decoder.decode(buffer);
if (!decoded.hasRemaining())
continue;
decodeds.add(decoded);
if (LOG.isDebugEnabled())
LOG.debug("Response content decoded ({}) {}{}{}", decoder, response, System.lineSeparator(), BufferUtil.toDetailString(decoded));
}
if (decodeds.isEmpty())
{
callback.succeeded();
if (decodeds.isEmpty())
{
callback.succeeded();
}
else
{
int size = decodeds.size();
CountingCallback counter = new CountingCallback(callback, size);
for (int i = 0; i < size; ++i)
notifier.notifyContent(listeners, response, decodeds.get(i), counter);
}
}
else
catch (Throwable x)
{
int size = decodeds.size();
CountingCallback counter = new CountingCallback(callback, size);
for (int i = 0; i < size; ++i)
notifier.notifyContent(listeners, response, decodeds.get(i), counter);
callback.failed(x);
}
}

View File

@ -40,18 +40,18 @@ public class MultiplexConnectionPool extends AbstractConnectionPool
private static final Logger LOG = Log.getLogger(MultiplexConnectionPool.class);
private final ReentrantLock lock = new ReentrantLock();
private final int maxMultiplexed;
private final Deque<Holder> idleConnections;
private final Map<Connection, Holder> muxedConnections;
private final Map<Connection, Holder> busyConnections;
private int maxMultiplex;
public MultiplexConnectionPool(HttpDestination destination, int maxConnections, Callback requester, int maxMultiplexed)
public MultiplexConnectionPool(HttpDestination destination, int maxConnections, Callback requester, int maxMultiplex)
{
super(destination, maxConnections, requester);
this.maxMultiplexed = maxMultiplexed;
this.idleConnections = new ArrayDeque<>(maxConnections);
this.muxedConnections = new HashMap<>(maxConnections);
this.busyConnections = new HashMap<>(maxConnections);
this.maxMultiplex = maxMultiplex;
}
protected void lock()
@ -64,6 +64,32 @@ public class MultiplexConnectionPool extends AbstractConnectionPool
lock.unlock();
}
public int getMaxMultiplex()
{
lock();
try
{
return maxMultiplex;
}
finally
{
unlock();
}
}
public void setMaxMultiplex(int maxMultiplex)
{
lock();
try
{
this.maxMultiplex = maxMultiplex;
}
finally
{
unlock();
}
}
@Override
public boolean isActive(Connection connection)
{
@ -120,7 +146,7 @@ public class MultiplexConnectionPool extends AbstractConnectionPool
holder = muxedConnections.values().iterator().next();
}
if (holder.count < maxMultiplexed)
if (holder.count < maxMultiplex)
{
++holder.count;
break;

View File

@ -30,4 +30,19 @@ public abstract class MultiplexHttpDestination extends HttpDestination
return new MultiplexConnectionPool(this, client.getMaxConnectionsPerDestination(), this,
client.getMaxRequestsQueuedPerDestination());
}
public int getMaxRequestsPerConnection()
{
ConnectionPool connectionPool = getConnectionPool();
if (connectionPool instanceof MultiplexConnectionPool)
return ((MultiplexConnectionPool)connectionPool).getMaxMultiplex();
return 1;
}
public void setMaxRequestsPerConnection(int maxRequestsPerConnection)
{
ConnectionPool connectionPool = getConnectionPool();
if (connectionPool instanceof MultiplexConnectionPool)
((MultiplexConnectionPool)connectionPool).setMaxMultiplex(maxRequestsPerConnection);
}
}

View File

@ -218,16 +218,17 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
parser.setHeadResponse(HttpMethod.HEAD.is(method) || HttpMethod.CONNECT.is(method));
exchange.getResponse().version(version).status(status).reason(reason);
responseBegin(exchange);
return false;
return !responseBegin(exchange);
}
@Override
public void parsedHeader(HttpField field)
{
HttpExchange exchange = getHttpExchange();
if (exchange != null)
responseHeader(exchange, field);
if (exchange == null)
return;
responseHeader(exchange, field);
}
@Override
@ -237,8 +238,7 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
if (exchange == null)
return false;
responseHeaders(exchange);
return false;
return !responseHeaders(exchange);
}
@Override
@ -263,17 +263,20 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
failAndClose(x);
}
};
responseContent(exchange, buffer, callback);
return callback.tryComplete();
// Do not short circuit these calls.
boolean proceed = responseContent(exchange, buffer, callback);
boolean async = callback.tryComplete();
return !proceed || async;
}
@Override
public boolean messageComplete()
{
HttpExchange exchange = getHttpExchange();
if (exchange != null)
responseSuccess(exchange);
return false;
if (exchange == null)
return false;
return !responseSuccess(exchange);
}
@Override

View File

@ -22,6 +22,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPOutputStream;
@ -196,6 +197,33 @@ public class HttpClientGZIPTest extends AbstractHttpClientServerTest
Assert.assertArrayEquals(data, response.getContent());
}
@Test
public void testGZIPContentCorrupted() throws Exception
{
start(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
response.setHeader("Content-Encoding", "gzip");
// Not gzipped, will cause the client to blow up.
response.getOutputStream().print("0123456789");
}
});
final CountDownLatch latch = new CountDownLatch(1);
client.newRequest("localhost", connector.getLocalPort())
.scheme(scheme)
.send(result ->
{
if (result.isFailed())
latch.countDown();
});
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
}
private static void sleep(long ms) throws IOException
{
try

View File

@ -374,9 +374,10 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements Connec
close(x);
}
};
if (!channel.content(buffer, callback))
return true;
return callback.tryComplete();
// Do not short circuit these calls.
boolean proceed = channel.content(buffer, callback);
boolean async = callback.tryComplete();
return !proceed || async;
}
else
{

View File

@ -27,6 +27,7 @@ import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import org.eclipse.jetty.util.ArrayTernaryTrie;
import org.eclipse.jetty.util.RegexSet;
@ -592,9 +593,8 @@ public class PathMap<O> extends HashMap<String,O>
}
}
public static class PathSet extends AbstractSet<String>
public static class PathSet extends AbstractSet<String> implements Predicate<String>
{
public static final BiFunction<PathSet,String,Boolean> MATCHER=(s,e)->{return s.containsMatch(e);};
private final PathMap<Boolean> _map = new PathMap<>();
@Override
@ -627,12 +627,15 @@ public class PathMap<O> extends HashMap<String,O>
return _map.containsKey(o);
}
@Override
public boolean test(String s)
{
return _map.containsMatch(s);
}
public boolean containsMatch(String s)
{
return _map.containsMatch(s);
}
}
}

View File

@ -0,0 +1,101 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
@ManagedObject("Mapped Resource")
public class MappedResource<E> implements Comparable<MappedResource<E>>
{
private final PathSpec pathSpec;
private final E resource;
public MappedResource(PathSpec pathSpec, E resource)
{
this.pathSpec = pathSpec;
this.resource = resource;
}
/**
* Comparison is based solely on the pathSpec
*/
@Override
public int compareTo(MappedResource<E> other)
{
return this.pathSpec.compareTo(other.pathSpec);
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
MappedResource<?> other = (MappedResource<?>)obj;
if (pathSpec == null)
{
if (other.pathSpec != null)
{
return false;
}
}
else if (!pathSpec.equals(other.pathSpec))
{
return false;
}
return true;
}
@ManagedAttribute(value = "path spec", readonly = true)
public PathSpec getPathSpec()
{
return pathSpec;
}
@ManagedAttribute(value = "resource", readonly = true)
public E getResource()
{
return resource;
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = (prime * result) + ((pathSpec == null) ? 0 : pathSpec.hashCode());
return result;
}
@Override
public String toString()
{
return String.format("MappedResource[pathSpec=%s,resource=%s]",pathSpec,resource);
}
}

View File

@ -0,0 +1,159 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* Path Mappings of PathSpec to Resource.
* <p>
* Sorted into search order upon entry into the Set
*
* @param <E> the type of mapping endpoint
*/
@ManagedObject("Path Mappings")
public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
{
private static final Logger LOG = Log.getLogger(PathMappings.class);
private List<MappedResource<E>> mappings = new ArrayList<MappedResource<E>>();
private MappedResource<E> defaultResource = null;
private MappedResource<E> rootResource = null;
@Override
public String dump()
{
return ContainerLifeCycle.dump(this);
}
@Override
public void dump(Appendable out, String indent) throws IOException
{
ContainerLifeCycle.dump(out,indent,mappings);
}
@ManagedAttribute(value = "mappings", readonly = true)
public List<MappedResource<E>> getMappings()
{
return mappings;
}
public void reset()
{
mappings.clear();
}
/**
* Return a list of MappedResource matches for the specified path.
*
* @param path the path to return matches on
* @return the list of mapped resource the path matches on
*/
public List<MappedResource<E>> getMatches(String path)
{
boolean matchRoot = "/".equals(path);
List<MappedResource<E>> ret = new ArrayList<>();
int len = mappings.size();
for (int i = 0; i < len; i++)
{
MappedResource<E> mr = mappings.get(i);
switch (mr.getPathSpec().group)
{
case ROOT:
if (matchRoot)
ret.add(mr);
break;
case DEFAULT:
if (matchRoot || mr.getPathSpec().matches(path))
ret.add(mr);
break;
default:
if (mr.getPathSpec().matches(path))
ret.add(mr);
break;
}
}
return ret;
}
public MappedResource<E> getMatch(String path)
{
if (path.equals("/") && rootResource != null)
{
return rootResource;
}
int len = mappings.size();
for (int i = 0; i < len; i++)
{
MappedResource<E> mr = mappings.get(i);
if (mr.getPathSpec().matches(path))
{
return mr;
}
}
return defaultResource;
}
@Override
public Iterator<MappedResource<E>> iterator()
{
return mappings.iterator();
}
@SuppressWarnings("incomplete-switch")
public void put(PathSpec pathSpec, E resource)
{
MappedResource<E> entry = new MappedResource<>(pathSpec,resource);
switch (pathSpec.group)
{
case DEFAULT:
defaultResource = entry;
break;
case ROOT:
rootResource = entry;
break;
}
// TODO: add warning when replacing an existing pathspec?
mappings.add(entry);
if (LOG.isDebugEnabled())
LOG.debug("Added {} to {}",entry,this);
Collections.sort(mappings);
}
@Override
public String toString()
{
return String.format("%s[size=%d]",this.getClass().getSimpleName(),mappings.size());
}
}

View File

@ -0,0 +1,167 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
/**
* The base PathSpec, what all other path specs are based on
*/
public abstract class PathSpec implements Comparable<PathSpec>
{
protected String pathSpec;
protected PathSpecGroup group;
protected int pathDepth;
protected int specLength;
@Override
public int compareTo(PathSpec other)
{
// Grouping (increasing)
int diff = this.group.ordinal() - other.group.ordinal();
if (diff != 0)
{
return diff;
}
// Spec Length (decreasing)
diff = other.specLength - this.specLength;
if (diff != 0)
{
return diff;
}
// Path Spec Name (alphabetical)
return this.pathSpec.compareTo(other.pathSpec);
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
PathSpec other = (PathSpec)obj;
if (pathSpec == null)
{
if (other.pathSpec != null)
{
return false;
}
}
else if (!pathSpec.equals(other.pathSpec))
{
return false;
}
return true;
}
public PathSpecGroup getGroup()
{
return group;
}
/**
* Get the number of path elements that this path spec declares.
* <p>
* This is used to determine longest match logic.
*
* @return the depth of the path segments that this spec declares
*/
public int getPathDepth()
{
return pathDepth;
}
/**
* Return the portion of the path that is after the path spec.
*
* @param path
* the path to match against
* @return the path info portion of the string
*/
public abstract String getPathInfo(String path);
/**
* Return the portion of the path that matches a path spec.
*
* @param path
* the path to match against
* @return the match, or null if no match at all
*/
public abstract String getPathMatch(String path);
/**
* The as-provided path spec.
*
* @return the as-provided path spec
*/
public String getDeclaration()
{
return pathSpec;
}
/**
* Get the relative path.
*
* @param base
* the base the path is relative to
* @param path
* the additional path
* @return the base plus path with pathSpec portion removed
*/
public abstract String getRelativePath(String base, String path);
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = (prime * result) + ((pathSpec == null)?0:pathSpec.hashCode());
return result;
}
/**
* Test to see if the provided path matches this path spec
*
* @param path
* the path to test
* @return true if the path matches this path spec, false otherwise
*/
public abstract boolean matches(String path);
@Override
public String toString()
{
StringBuilder str = new StringBuilder();
str.append(this.getClass().getSimpleName()).append("[\"");
str.append(pathSpec);
str.append("\",pathDepth=").append(pathDepth);
str.append(",group=").append(group);
str.append("]");
return str.toString();
}
}

View File

@ -0,0 +1,101 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
/**
* Types of path spec groups.
* <p>
* This is used to facilitate proper pathspec search order.
* <p>
* Search Order:
* <ol>
* <li>{@link PathSpecGroup#ordinal()} [increasing]</li>
* <li>{@link PathSpec#specLength} [decreasing]</li>
* <li>{@link PathSpec#pathSpec} [natural sort order]</li>
* </ol>
*/
public enum PathSpecGroup
{
// NOTE: Order of enums determines order of Groups.
/**
* For exactly defined path specs, no glob.
*/
EXACT,
/**
* For path specs that have a hardcoded prefix and suffix with wildcard glob in the middle.
*
* <pre>
* "^/downloads/[^/]*.zip$" - regex spec
* "/a/{var}/c" - uri-template spec
* </pre>
*
* Note: there is no known servlet spec variant of this kind of path spec
*/
MIDDLE_GLOB,
/**
* For path specs that have a hardcoded prefix and a trailing wildcard glob.
* <p>
*
* <pre>
* "/downloads/*" - servlet spec
* "/api/*" - servlet spec
* "^/rest/.*$" - regex spec
* "/bookings/{guest-id}" - uri-template spec
* "/rewards/{vip-level}" - uri-template spec
* </pre>
*/
PREFIX_GLOB,
/**
* For path specs that have a wildcard glob with a hardcoded suffix
*
* <pre>
* "*.do" - servlet spec
* "*.css" - servlet spec
* "^.*\.zip$" - regex spec
* </pre>
*
* Note: there is no known uri-template spec variant of this kind of path spec
*/
SUFFIX_GLOB,
/**
* The root spec for accessing the Root behavior.
*
* <pre>
* "" - servlet spec (Root Servlet)
* null - servlet spec (Root Servlet)
* </pre>
*
* Note: there is no known uri-template spec variant of this kind of path spec
*/
ROOT,
/**
* The default spec for accessing the Default path behavior.
*
* <pre>
* "/" - servlet spec (Default Servlet)
* "/" - uri-template spec (Root Context)
* "^/$" - regex spec (Root Context)
* </pre>
*
* Per Servlet Spec, pathInfo is always null for these specs.
* If nothing above matches, then default will match.
*/
DEFAULT,
}

View File

@ -0,0 +1,216 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
/**
* A Set of PathSpec strings.
* <p>
* Used by {@link org.eclipse.jetty.util.IncludeExclude} logic
*/
public class PathSpecSet implements Set<String>, Predicate<String>
{
private final Set<PathSpec> specs = new TreeSet<>();
@Override
public boolean test(String s)
{
for (PathSpec spec : specs)
{
if (spec.matches(s))
{
return true;
}
}
return false;
}
@Override
public boolean isEmpty()
{
return specs.isEmpty();
}
@Override
public Iterator<String> iterator()
{
return new Iterator<String>()
{
private Iterator<PathSpec> iter = specs.iterator();
@Override
public boolean hasNext()
{
return iter.hasNext();
}
@Override
public String next()
{
PathSpec spec = iter.next();
if (spec == null)
{
return null;
}
return spec.getDeclaration();
}
};
}
@Override
public int size()
{
return specs.size();
}
@Override
public boolean contains(Object o)
{
if (o instanceof PathSpec)
{
return specs.contains(o);
}
if (o instanceof String)
{
return specs.contains(toPathSpec((String)o));
}
return false;
}
private PathSpec asPathSpec(Object o)
{
if (o == null)
{
return null;
}
if (o instanceof PathSpec)
{
return (PathSpec)o;
}
if (o instanceof String)
{
return toPathSpec((String)o);
}
return toPathSpec(o.toString());
}
private PathSpec toPathSpec(String rawSpec)
{
if ((rawSpec == null) || (rawSpec.length() < 1))
{
throw new RuntimeException("Path Spec String must start with '^', '/', or '*.': got [" + rawSpec + "]");
}
if (rawSpec.charAt(0) == '^')
{
return new RegexPathSpec(rawSpec);
}
else
{
return new ServletPathSpec(rawSpec);
}
}
@Override
public Object[] toArray()
{
return toArray(new String[specs.size()]);
}
@Override
public <T> T[] toArray(T[] a)
{
int i = 0;
for (PathSpec spec : specs)
{
a[i++] = (T)spec.getDeclaration();
}
return a;
}
@Override
public boolean add(String e)
{
return specs.add(toPathSpec(e));
}
@Override
public boolean remove(Object o)
{
return specs.remove(asPathSpec(o));
}
@Override
public boolean containsAll(Collection<?> coll)
{
for (Object o : coll)
{
if (!specs.contains(asPathSpec(o)))
return false;
}
return true;
}
@Override
public boolean addAll(Collection<? extends String> coll)
{
boolean ret = false;
for (String s : coll)
{
ret |= add(s);
}
return ret;
}
@Override
public boolean retainAll(Collection<?> coll)
{
List<PathSpec> collSpecs = new ArrayList<>();
for (Object o : coll)
{
collSpecs.add(asPathSpec(o));
}
return specs.retainAll(collSpecs);
}
@Override
public boolean removeAll(Collection<?> coll)
{
List<PathSpec> collSpecs = new ArrayList<>();
for (Object o : coll)
{
collSpecs.add(asPathSpec(o));
}
return specs.removeAll(collSpecs);
}
@Override
public void clear()
{
specs.clear();
}
}

View File

@ -0,0 +1,176 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexPathSpec extends PathSpec
{
protected Pattern pattern;
protected RegexPathSpec()
{
super();
}
public RegexPathSpec(String regex)
{
super.pathSpec = regex;
boolean inGrouping = false;
this.pathDepth = 0;
this.specLength = pathSpec.length();
// build up a simple signature we can use to identify the grouping
StringBuilder signature = new StringBuilder();
for (char c : pathSpec.toCharArray())
{
switch (c)
{
case '[':
inGrouping = true;
break;
case ']':
inGrouping = false;
signature.append('g'); // glob
break;
case '*':
signature.append('g'); // glob
break;
case '/':
if (!inGrouping)
{
this.pathDepth++;
}
break;
default:
if (!inGrouping)
{
if (Character.isLetterOrDigit(c))
{
signature.append('l'); // literal (exact)
}
}
break;
}
}
this.pattern = Pattern.compile(pathSpec);
// Figure out the grouping based on the signature
String sig = signature.toString();
if (Pattern.matches("^l*$",sig))
{
this.group = PathSpecGroup.EXACT;
}
else if (Pattern.matches("^l*g+",sig))
{
this.group = PathSpecGroup.PREFIX_GLOB;
}
else if (Pattern.matches("^g+l+$",sig))
{
this.group = PathSpecGroup.SUFFIX_GLOB;
}
else
{
this.group = PathSpecGroup.MIDDLE_GLOB;
}
}
public Matcher getMatcher(String path)
{
return this.pattern.matcher(path);
}
@Override
public String getPathInfo(String path)
{
// Path Info only valid for PREFIX_GLOB types
if (group == PathSpecGroup.PREFIX_GLOB)
{
Matcher matcher = getMatcher(path);
if (matcher.matches())
{
if (matcher.groupCount() >= 1)
{
String pathInfo = matcher.group(1);
if ("".equals(pathInfo))
{
return "/";
}
else
{
return pathInfo;
}
}
}
}
return null;
}
@Override
public String getPathMatch(String path)
{
Matcher matcher = getMatcher(path);
if (matcher.matches())
{
if (matcher.groupCount() >= 1)
{
int idx = matcher.start(1);
if (idx > 0)
{
if (path.charAt(idx - 1) == '/')
{
idx--;
}
return path.substring(0,idx);
}
}
return path;
}
return null;
}
public Pattern getPattern()
{
return this.pattern;
}
@Override
public String getRelativePath(String base, String path)
{
// TODO Auto-generated method stub
return null;
}
@Override
public boolean matches(final String path)
{
int idx = path.indexOf('?');
if (idx >= 0)
{
// match only non-query part
return getMatcher(path.substring(0,idx)).matches();
}
else
{
// match entire path
return getMatcher(path).matches();
}
}
}

View File

@ -0,0 +1,261 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
import org.eclipse.jetty.util.URIUtil;
public class ServletPathSpec extends PathSpec
{
public ServletPathSpec(String servletPathSpec)
{
super();
assertValidServletPathSpec(servletPathSpec);
// The Root Path Spec
if ((servletPathSpec == null) || (servletPathSpec.length() == 0))
{
super.pathSpec = "";
super.pathDepth = -1; // force this to be at the end of the sort order
this.specLength = 1;
this.group = PathSpecGroup.ROOT;
return;
}
// The Default Path Spec
if("/".equals(servletPathSpec))
{
super.pathSpec = "/";
super.pathDepth = -1; // force this to be at the end of the sort order
this.specLength = 1;
this.group = PathSpecGroup.DEFAULT;
return;
}
this.specLength = servletPathSpec.length();
super.pathDepth = 0;
char lastChar = servletPathSpec.charAt(specLength - 1);
// prefix based
if ((servletPathSpec.charAt(0) == '/') && (specLength > 1) && (lastChar == '*'))
{
this.group = PathSpecGroup.PREFIX_GLOB;
}
// suffix based
else if (servletPathSpec.charAt(0) == '*')
{
this.group = PathSpecGroup.SUFFIX_GLOB;
}
else
{
this.group = PathSpecGroup.EXACT;
}
for (int i = 0; i < specLength; i++)
{
int cp = servletPathSpec.codePointAt(i);
if (cp < 128)
{
char c = (char)cp;
switch (c)
{
case '/':
super.pathDepth++;
break;
}
}
}
super.pathSpec = servletPathSpec;
}
private void assertValidServletPathSpec(String servletPathSpec)
{
if ((servletPathSpec == null) || servletPathSpec.equals(""))
{
return; // empty path spec
}
int len = servletPathSpec.length();
// path spec must either start with '/' or '*.'
if (servletPathSpec.charAt(0) == '/')
{
// Prefix Based
if (len == 1)
{
return; // simple '/' path spec
}
int idx = servletPathSpec.indexOf('*');
if (idx < 0)
{
return; // no hit on glob '*'
}
// only allowed to have '*' at the end of the path spec
if (idx != (len - 1))
{
throw new IllegalArgumentException("Servlet Spec 12.2 violation: glob '*' can only exist at end of prefix based matches: bad spec \""+ servletPathSpec +"\"");
}
}
else if (servletPathSpec.startsWith("*."))
{
// Suffix Based
int idx = servletPathSpec.indexOf('/');
// cannot have path separator
if (idx >= 0)
{
throw new IllegalArgumentException("Servlet Spec 12.2 violation: suffix based path spec cannot have path separators: bad spec \""+ servletPathSpec +"\"");
}
idx = servletPathSpec.indexOf('*',2);
// only allowed to have 1 glob '*', at the start of the path spec
if (idx >= 1)
{
throw new IllegalArgumentException("Servlet Spec 12.2 violation: suffix based path spec cannot have multiple glob '*': bad spec \""+ servletPathSpec +"\"");
}
}
else
{
throw new IllegalArgumentException("Servlet Spec 12.2 violation: path spec must start with \"/\" or \"*.\": bad spec \""+ servletPathSpec +"\"");
}
}
@Override
public String getPathInfo(String path)
{
// Path Info only valid for PREFIX_GLOB types
if (group == PathSpecGroup.PREFIX_GLOB)
{
if (path.length() == (specLength - 2))
{
return null;
}
return path.substring(specLength - 2);
}
return null;
}
@Override
public String getPathMatch(String path)
{
switch (group)
{
case EXACT:
if (pathSpec.equals(path))
{
return path;
}
else
{
return null;
}
case PREFIX_GLOB:
if (isWildcardMatch(path))
{
return path.substring(0,specLength - 2);
}
else
{
return null;
}
case SUFFIX_GLOB:
if (path.regionMatches(path.length() - (specLength - 1),pathSpec,1,specLength - 1))
{
return path;
}
else
{
return null;
}
case DEFAULT:
return path;
default:
return null;
}
}
@Override
public String getRelativePath(String base, String path)
{
String info = getPathInfo(path);
if (info == null)
{
info = path;
}
if (info.startsWith("./"))
{
info = info.substring(2);
}
if (base.endsWith(URIUtil.SLASH))
{
if (info.startsWith(URIUtil.SLASH))
{
path = base + info.substring(1);
}
else
{
path = base + info;
}
}
else if (info.startsWith(URIUtil.SLASH))
{
path = base + info;
}
else
{
path = base + URIUtil.SLASH + info;
}
return path;
}
private boolean isWildcardMatch(String path)
{
// For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
int cpl = specLength - 2;
if ((group == PathSpecGroup.PREFIX_GLOB) && (path.regionMatches(0,pathSpec,0,cpl)))
{
if ((path.length() == cpl) || ('/' == path.charAt(cpl)))
{
return true;
}
}
return false;
}
@Override
public boolean matches(String path)
{
switch (group)
{
case EXACT:
return pathSpec.equals(path);
case PREFIX_GLOB:
return isWildcardMatch(path);
case SUFFIX_GLOB:
return path.regionMatches((path.length() - specLength) + 1,pathSpec,1,specLength - 1);
case ROOT:
// Only "/" matches
return ("/".equals(path));
case DEFAULT:
// If we reached this point, then everything matches
return true;
default:
return false;
}
}
}

View File

@ -0,0 +1,341 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* PathSpec for URI Template based declarations
*
* @see <a href="https://tools.ietf.org/html/rfc6570">URI Templates (Level 1)</a>
*/
public class UriTemplatePathSpec extends RegexPathSpec
{
private static final Logger LOG = Log.getLogger(UriTemplatePathSpec.class);
private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{(.*)\\}");
/** Reserved Symbols in URI Template variable */
private static final String VARIABLE_RESERVED = ":/?#[]@" + // gen-delims
"!$&'()*+,;="; // sub-delims
/** Allowed Symbols in a URI Template variable */
private static final String VARIABLE_SYMBOLS="-._";
private static final Set<String> FORBIDDEN_SEGMENTS;
static
{
FORBIDDEN_SEGMENTS = new HashSet<>();
FORBIDDEN_SEGMENTS.add("/./");
FORBIDDEN_SEGMENTS.add("/../");
FORBIDDEN_SEGMENTS.add("//");
}
private String variables[];
public UriTemplatePathSpec(String rawSpec)
{
super();
Objects.requireNonNull(rawSpec,"Path Param Spec cannot be null");
if ("".equals(rawSpec) || "/".equals(rawSpec))
{
super.pathSpec = "/";
super.pattern = Pattern.compile("^/$");
super.pathDepth = 1;
this.specLength = 1;
this.variables = new String[0];
this.group = PathSpecGroup.EXACT;
return;
}
if (rawSpec.charAt(0) != '/')
{
// path specs must start with '/'
StringBuilder err = new StringBuilder();
err.append("Syntax Error: path spec \"");
err.append(rawSpec);
err.append("\" must start with '/'");
throw new IllegalArgumentException(err.toString());
}
for (String forbidden : FORBIDDEN_SEGMENTS)
{
if (rawSpec.contains(forbidden))
{
StringBuilder err = new StringBuilder();
err.append("Syntax Error: segment ");
err.append(forbidden);
err.append(" is forbidden in path spec: ");
err.append(rawSpec);
throw new IllegalArgumentException(err.toString());
}
}
this.pathSpec = rawSpec;
StringBuilder regex = new StringBuilder();
regex.append('^');
List<String> varNames = new ArrayList<>();
// split up into path segments (ignoring the first slash that will always be empty)
String segments[] = rawSpec.substring(1).split("/");
char segmentSignature[] = new char[segments.length];
this.pathDepth = segments.length;
for (int i = 0; i < segments.length; i++)
{
String segment = segments[i];
Matcher mat = VARIABLE_PATTERN.matcher(segment);
if (mat.matches())
{
// entire path segment is a variable.
String variable = mat.group(1);
if (varNames.contains(variable))
{
// duplicate variable names
StringBuilder err = new StringBuilder();
err.append("Syntax Error: variable ");
err.append(variable);
err.append(" is duplicated in path spec: ");
err.append(rawSpec);
throw new IllegalArgumentException(err.toString());
}
assertIsValidVariableLiteral(variable);
segmentSignature[i] = 'v'; // variable
// valid variable name
varNames.add(variable);
// build regex
regex.append("/([^/]+)");
}
else if (mat.find(0))
{
// variable exists as partial segment
StringBuilder err = new StringBuilder();
err.append("Syntax Error: variable ");
err.append(mat.group());
err.append(" must exist as entire path segment: ");
err.append(rawSpec);
throw new IllegalArgumentException(err.toString());
}
else if ((segment.indexOf('{') >= 0) || (segment.indexOf('}') >= 0))
{
// variable is split with a path separator
StringBuilder err = new StringBuilder();
err.append("Syntax Error: invalid path segment /");
err.append(segment);
err.append("/ variable declaration incomplete: ");
err.append(rawSpec);
throw new IllegalArgumentException(err.toString());
}
else if (segment.indexOf('*') >= 0)
{
// glob segment
StringBuilder err = new StringBuilder();
err.append("Syntax Error: path segment /");
err.append(segment);
err.append("/ contains a wildcard symbol (not supported by this uri-template implementation): ");
err.append(rawSpec);
throw new IllegalArgumentException(err.toString());
}
else
{
// valid path segment
segmentSignature[i] = 'e'; // exact
// build regex
regex.append('/');
// escape regex special characters
for (char c : segment.toCharArray())
{
if ((c == '.') || (c == '[') || (c == ']') || (c == '\\'))
{
regex.append('\\');
}
regex.append(c);
}
}
}
// Handle trailing slash (which is not picked up during split)
if(rawSpec.charAt(rawSpec.length()-1) == '/')
{
regex.append('/');
}
regex.append('$');
this.pattern = Pattern.compile(regex.toString());
int varcount = varNames.size();
this.variables = varNames.toArray(new String[varcount]);
// Convert signature to group
String sig = String.valueOf(segmentSignature);
if (Pattern.matches("^e*$",sig))
{
this.group = PathSpecGroup.EXACT;
}
else if (Pattern.matches("^e*v+",sig))
{
this.group = PathSpecGroup.PREFIX_GLOB;
}
else if (Pattern.matches("^v+e+",sig))
{
this.group = PathSpecGroup.SUFFIX_GLOB;
}
else
{
this.group = PathSpecGroup.MIDDLE_GLOB;
}
}
/**
* Validate variable literal name, per RFC6570, Section 2.1 Literals
* @param variable
* @param pathParamSpec
*/
private void assertIsValidVariableLiteral(String variable)
{
int len = variable.length();
int i = 0;
int codepoint;
boolean valid = (len > 0); // must not be zero length
while (valid && i < len)
{
codepoint = variable.codePointAt(i);
i += Character.charCount(codepoint);
// basic letters, digits, or symbols
if (isValidBasicLiteralCodepoint(codepoint))
{
continue;
}
// The ucschar and iprivate pieces
if (Character.isSupplementaryCodePoint(codepoint))
{
continue;
}
// pct-encoded
if (codepoint == '%')
{
if (i + 2 > len)
{
// invalid percent encoding, missing extra 2 chars
valid = false;
continue;
}
codepoint = TypeUtil.convertHexDigit(variable.codePointAt(i++)) << 4;
codepoint |= TypeUtil.convertHexDigit(variable.codePointAt(i++));
// validate basic literal
if (isValidBasicLiteralCodepoint(codepoint))
{
continue;
}
}
valid = false;
}
if (!valid)
{
// invalid variable name
StringBuilder err = new StringBuilder();
err.append("Syntax Error: variable {");
err.append(variable);
err.append("} an invalid variable name: ");
err.append(pathSpec);
throw new IllegalArgumentException(err.toString());
}
}
private boolean isValidBasicLiteralCodepoint(int codepoint)
{
// basic letters or digits
if((codepoint >= 'a' && codepoint <= 'z') ||
(codepoint >= 'A' && codepoint <= 'Z') ||
(codepoint >= '0' && codepoint <= '9'))
{
return true;
}
// basic allowed symbols
if(VARIABLE_SYMBOLS.indexOf(codepoint) >= 0)
{
return true; // valid simple value
}
// basic reserved symbols
if(VARIABLE_RESERVED.indexOf(codepoint) >= 0)
{
LOG.warn("Detected URI Template reserved symbol [{}] in path spec \"{}\"",(char)codepoint,pathSpec);
return false; // valid simple value
}
return false;
}
public Map<String, String> getPathParams(String path)
{
Matcher matcher = getMatcher(path);
if (matcher.matches())
{
if (group == PathSpecGroup.EXACT)
{
return Collections.emptyMap();
}
Map<String, String> ret = new HashMap<>();
int groupCount = matcher.groupCount();
for (int i = 1; i <= groupCount; i++)
{
ret.put(this.variables[i - 1],matcher.group(i));
}
return ret;
}
return null;
}
public int getVariableCount()
{
return variables.length;
}
public String[] getVariables()
{
return this.variables;
}
}

View File

@ -0,0 +1,281 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Assert;
import org.junit.Test;
public class PathMappingsTest
{
private void assertMatch(PathMappings<String> pathmap, String path, String expectedValue)
{
String msg = String.format(".getMatch(\"%s\")",path);
MappedResource<String> match = pathmap.getMatch(path);
Assert.assertThat(msg,match,notNullValue());
String actualMatch = match.getResource();
Assert.assertEquals(msg,expectedValue,actualMatch);
}
public void dumpMappings(PathMappings<String> p)
{
for (MappedResource<String> res : p)
{
System.out.printf(" %s%n",res);
}
}
/**
* Test the match order rules with a mixed Servlet and regex path specs
* <p>
* <ul>
* <li>Exact match</li>
* <li>Longest prefix match</li>
* <li>Longest suffix match</li>
* </ul>
*/
@Test
public void testMixedMatchOrder()
{
PathMappings<String> p = new PathMappings<>();
p.put(new ServletPathSpec("/"),"default");
p.put(new ServletPathSpec("/animal/bird/*"),"birds");
p.put(new ServletPathSpec("/animal/fish/*"),"fishes");
p.put(new ServletPathSpec("/animal/*"),"animals");
p.put(new RegexPathSpec("^/animal/.*/chat$"),"animalChat");
p.put(new RegexPathSpec("^/animal/.*/cam$"),"animalCam");
p.put(new RegexPathSpec("^/entrance/cam$"),"entranceCam");
// dumpMappings(p);
assertMatch(p,"/animal/bird/eagle","birds");
assertMatch(p,"/animal/fish/bass/sea","fishes");
assertMatch(p,"/animal/peccary/javalina/evolution","animals");
assertMatch(p,"/","default");
assertMatch(p,"/animal/bird/eagle/chat","animalChat");
assertMatch(p,"/animal/bird/penguin/chat","animalChat");
assertMatch(p,"/animal/fish/trout/cam","animalCam");
assertMatch(p,"/entrance/cam","entranceCam");
}
/**
* Test the match order rules imposed by the Servlet API (default vs any)
*/
@Test
public void testServletMatchDefault()
{
PathMappings<String> p = new PathMappings<>();
p.put(new ServletPathSpec("/"),"default");
p.put(new ServletPathSpec("/*"),"any");
assertMatch(p,"/abs/path","any");
assertMatch(p,"/abs/path/xxx","any");
assertMatch(p,"/animal/bird/eagle/bald","any");
assertMatch(p,"/","any");
}
/**
* Test the match order rules with a mixed Servlet and URI Template path specs
* <p>
* <ul>
* <li>Exact match</li>
* <li>Longest prefix match</li>
* <li>Longest suffix match</li>
* </ul>
*/
@Test
public void testMixedMatchUriOrder()
{
PathMappings<String> p = new PathMappings<>();
p.put(new ServletPathSpec("/"),"default");
p.put(new ServletPathSpec("/animal/bird/*"),"birds");
p.put(new ServletPathSpec("/animal/fish/*"),"fishes");
p.put(new ServletPathSpec("/animal/*"),"animals");
p.put(new UriTemplatePathSpec("/animal/{type}/{name}/chat"),"animalChat");
p.put(new UriTemplatePathSpec("/animal/{type}/{name}/cam"),"animalCam");
p.put(new UriTemplatePathSpec("/entrance/cam"),"entranceCam");
// dumpMappings(p);
assertMatch(p,"/animal/bird/eagle","birds");
assertMatch(p,"/animal/fish/bass/sea","fishes");
assertMatch(p,"/animal/peccary/javalina/evolution","animals");
assertMatch(p,"/","default");
assertMatch(p,"/animal/bird/eagle/chat","animalChat");
assertMatch(p,"/animal/bird/penguin/chat","animalChat");
assertMatch(p,"/animal/fish/trout/cam","animalCam");
assertMatch(p,"/entrance/cam","entranceCam");
}
/**
* Test the match order rules for URI Template based specs
* <p>
* <ul>
* <li>Exact match</li>
* <li>Longest prefix match</li>
* <li>Longest suffix match</li>
* </ul>
*/
@Test
public void testUriTemplateMatchOrder()
{
PathMappings<String> p = new PathMappings<>();
p.put(new UriTemplatePathSpec("/a/{var}/c"),"endpointA");
p.put(new UriTemplatePathSpec("/a/b/c"),"endpointB");
p.put(new UriTemplatePathSpec("/a/{var1}/{var2}"),"endpointC");
p.put(new UriTemplatePathSpec("/{var1}/d"),"endpointD");
p.put(new UriTemplatePathSpec("/b/{var2}"),"endpointE");
// dumpMappings(p);
assertMatch(p,"/a/b/c","endpointB");
assertMatch(p,"/a/d/c","endpointA");
assertMatch(p,"/a/x/y","endpointC");
assertMatch(p,"/b/d","endpointE");
}
@Test
public void testPathMap() throws Exception
{
PathMappings<String> p = new PathMappings<>();
p.put(new ServletPathSpec("/abs/path"), "1");
p.put(new ServletPathSpec("/abs/path/longer"), "2");
p.put(new ServletPathSpec("/animal/bird/*"), "3");
p.put(new ServletPathSpec("/animal/fish/*"), "4");
p.put(new ServletPathSpec("/animal/*"), "5");
p.put(new ServletPathSpec("*.tar.gz"), "6");
p.put(new ServletPathSpec("*.gz"), "7");
p.put(new ServletPathSpec("/"), "8");
// p.put(new ServletPathSpec("/XXX:/YYY"), "9"); // special syntax from Jetty 3.1.x
p.put(new ServletPathSpec(""), "10");
p.put(new ServletPathSpec("/\u20ACuro/*"), "11");
assertEquals("pathMatch exact", "/Foo/bar", new ServletPathSpec("/Foo/bar").getPathMatch("/Foo/bar"));
assertEquals("pathMatch prefix", "/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo/bar"));
assertEquals("pathMatch prefix", "/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo/"));
assertEquals("pathMatch prefix", "/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo"));
assertEquals("pathMatch suffix", "/Foo/bar.ext", new ServletPathSpec("*.ext").getPathMatch("/Foo/bar.ext"));
assertEquals("pathMatch default", "/Foo/bar.ext", new ServletPathSpec("/").getPathMatch("/Foo/bar.ext"));
assertEquals("pathInfo exact", null, new ServletPathSpec("/Foo/bar").getPathInfo("/Foo/bar"));
assertEquals("pathInfo prefix", "/bar", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/bar"));
assertEquals("pathInfo prefix", "/*", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/*"));
assertEquals("pathInfo prefix", "/", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/"));
assertEquals("pathInfo prefix", null, new ServletPathSpec("/Foo/*").getPathInfo("/Foo"));
assertEquals("pathInfo suffix", null, new ServletPathSpec("*.ext").getPathInfo("/Foo/bar.ext"));
assertEquals("pathInfo default", null, new ServletPathSpec("/").getPathInfo("/Foo/bar.ext"));
p.put(new ServletPathSpec("/*"), "0");
// assertEquals("Get absolute path", "1", p.get("/abs/path"));
assertEquals("Match absolute path", "/abs/path", p.getMatch("/abs/path").getPathSpec().pathSpec);
assertEquals("Match absolute path", "1", p.getMatch("/abs/path").getResource());
assertEquals("Mismatch absolute path", "0", p.getMatch("/abs/path/xxx").getResource());
assertEquals("Mismatch absolute path", "0", p.getMatch("/abs/pith").getResource());
assertEquals("Match longer absolute path", "2", p.getMatch("/abs/path/longer").getResource());
assertEquals("Not exact absolute path", "0", p.getMatch("/abs/path/").getResource());
assertEquals("Not exact absolute path", "0", p.getMatch("/abs/path/xxx").getResource());
assertEquals("Match longest prefix", "3", p.getMatch("/animal/bird/eagle/bald").getResource());
assertEquals("Match longest prefix", "4", p.getMatch("/animal/fish/shark/grey").getResource());
assertEquals("Match longest prefix", "5", p.getMatch("/animal/insect/bug").getResource());
assertEquals("mismatch exact prefix", "5", p.getMatch("/animal").getResource());
assertEquals("mismatch exact prefix", "5", p.getMatch("/animal/").getResource());
assertEquals("Match longest suffix", "0", p.getMatch("/suffix/path.tar.gz").getResource());
assertEquals("Match longest suffix", "0", p.getMatch("/suffix/path.gz").getResource());
assertEquals("prefix rather than suffix", "5", p.getMatch("/animal/path.gz").getResource());
assertEquals("default", "0", p.getMatch("/Other/path").getResource());
assertEquals("pathMatch /*", "", new ServletPathSpec("/*").getPathMatch("/xxx/zzz"));
assertEquals("pathInfo /*", "/xxx/zzz", new ServletPathSpec("/*").getPathInfo("/xxx/zzz"));
assertTrue("match /", new ServletPathSpec("/").matches("/anything"));
assertTrue("match /*", new ServletPathSpec("/*").matches("/anything"));
assertTrue("match /foo", new ServletPathSpec("/foo").matches("/foo"));
assertTrue("!match /foo", !new ServletPathSpec("/foo").matches("/bar"));
assertTrue("match /foo/*", new ServletPathSpec("/foo/*").matches("/foo"));
assertTrue("match /foo/*", new ServletPathSpec("/foo/*").matches("/foo/"));
assertTrue("match /foo/*", new ServletPathSpec("/foo/*").matches("/foo/anything"));
assertTrue("!match /foo/*", !new ServletPathSpec("/foo/*").matches("/bar"));
assertTrue("!match /foo/*", !new ServletPathSpec("/foo/*").matches("/bar/"));
assertTrue("!match /foo/*", !new ServletPathSpec("/foo/*").matches("/bar/anything"));
assertTrue("match *.foo", new ServletPathSpec("*.foo").matches("anything.foo"));
assertTrue("!match *.foo", !new ServletPathSpec("*.foo").matches("anything.bar"));
assertEquals("match / with ''", "10", p.getMatch("/").getResource());
assertTrue("match \"\"", new ServletPathSpec("").matches("/"));
}
/**
* See JIRA issue: JETTY-88.
* @throws Exception failed test
*/
@Test
public void testPathMappingsOnlyMatchOnDirectoryNames() throws Exception
{
ServletPathSpec spec = new ServletPathSpec("/xyz/*");
PathSpecAssert.assertMatch(spec, "/xyz");
PathSpecAssert.assertMatch(spec, "/xyz/");
PathSpecAssert.assertMatch(spec, "/xyz/123");
PathSpecAssert.assertMatch(spec, "/xyz/123/");
PathSpecAssert.assertMatch(spec, "/xyz/123.txt");
PathSpecAssert.assertNotMatch(spec, "/xyz123");
PathSpecAssert.assertNotMatch(spec, "/xyz123;jessionid=99");
PathSpecAssert.assertNotMatch(spec, "/xyz123/");
PathSpecAssert.assertNotMatch(spec, "/xyz123/456");
PathSpecAssert.assertNotMatch(spec, "/xyz.123");
PathSpecAssert.assertNotMatch(spec, "/xyz;123"); // as if the ; was encoded and part of the path
PathSpecAssert.assertNotMatch(spec, "/xyz?123"); // as if the ? was encoded and part of the path
}
@Test
public void testPrecidenceVsOrdering() throws Exception
{
PathMappings<String> p = new PathMappings<>();
p.put(new ServletPathSpec("/dump/gzip/*"),"prefix");
p.put(new ServletPathSpec("*.txt"),"suffix");
assertEquals(null,p.getMatch("/foo/bar"));
assertEquals("prefix",p.getMatch("/dump/gzip/something").getResource());
assertEquals("suffix",p.getMatch("/foo/something.txt").getResource());
assertEquals("prefix",p.getMatch("/dump/gzip/something.txt").getResource());
p = new PathMappings<>();
p.put(new ServletPathSpec("*.txt"),"suffix");
p.put(new ServletPathSpec("/dump/gzip/*"),"prefix");
assertEquals(null,p.getMatch("/foo/bar"));
assertEquals("prefix",p.getMatch("/dump/gzip/something").getResource());
assertEquals("suffix",p.getMatch("/foo/something.txt").getResource());
assertEquals("prefix",p.getMatch("/dump/gzip/something.txt").getResource());
}
}

View File

@ -0,0 +1,37 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
public class PathSpecAssert
{
public static void assertMatch(PathSpec spec, String path)
{
boolean match = spec.matches(path);
assertThat(spec.getClass().getSimpleName() + " '" + spec + "' should match path '" + path + "'", match, is(true));
}
public static void assertNotMatch(PathSpec spec, String path)
{
boolean match = spec.matches(path);
assertThat(spec.getClass().getSimpleName() + " '" + spec + "' should not match path '" + path + "'", match, is(false));
}
}

View File

@ -0,0 +1,135 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import org.junit.Test;
public class RegexPathSpecTest
{
public static void assertMatches(PathSpec spec, String path)
{
String msg = String.format("Spec(\"%s\").matches(\"%s\")",spec.getDeclaration(),path);
assertThat(msg,spec.matches(path),is(true));
}
public static void assertNotMatches(PathSpec spec, String path)
{
String msg = String.format("!Spec(\"%s\").matches(\"%s\")",spec.getDeclaration(),path);
assertThat(msg,spec.matches(path),is(false));
}
@Test
public void testExactSpec()
{
RegexPathSpec spec = new RegexPathSpec("^/a$");
assertEquals("Spec.pathSpec","^/a$",spec.getDeclaration());
assertEquals("Spec.pattern","^/a$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",1,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.EXACT,spec.group);
assertMatches(spec,"/a");
assertNotMatches(spec,"/aa");
assertNotMatches(spec,"/a/");
}
@Test
public void testMiddleSpec()
{
RegexPathSpec spec = new RegexPathSpec("^/rest/([^/]*)/list$");
assertEquals("Spec.pathSpec","^/rest/([^/]*)/list$",spec.getDeclaration());
assertEquals("Spec.pattern","^/rest/([^/]*)/list$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",3,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.MIDDLE_GLOB,spec.group);
assertMatches(spec,"/rest/api/list");
assertMatches(spec,"/rest/1.0/list");
assertMatches(spec,"/rest/2.0/list");
assertMatches(spec,"/rest/accounts/list");
assertNotMatches(spec,"/a");
assertNotMatches(spec,"/aa");
assertNotMatches(spec,"/aa/bb");
assertNotMatches(spec,"/rest/admin/delete");
assertNotMatches(spec,"/rest/list");
}
@Test
public void testMiddleSpecNoGrouping()
{
RegexPathSpec spec = new RegexPathSpec("^/rest/[^/]+/list$");
assertEquals("Spec.pathSpec","^/rest/[^/]+/list$",spec.getDeclaration());
assertEquals("Spec.pattern","^/rest/[^/]+/list$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",3,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.MIDDLE_GLOB,spec.group);
assertMatches(spec,"/rest/api/list");
assertMatches(spec,"/rest/1.0/list");
assertMatches(spec,"/rest/2.0/list");
assertMatches(spec,"/rest/accounts/list");
assertNotMatches(spec,"/a");
assertNotMatches(spec,"/aa");
assertNotMatches(spec,"/aa/bb");
assertNotMatches(spec,"/rest/admin/delete");
assertNotMatches(spec,"/rest/list");
}
@Test
public void testPrefixSpec()
{
RegexPathSpec spec = new RegexPathSpec("^/a/(.*)$");
assertEquals("Spec.pathSpec","^/a/(.*)$",spec.getDeclaration());
assertEquals("Spec.pattern","^/a/(.*)$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",2,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.PREFIX_GLOB,spec.group);
assertMatches(spec,"/a/");
assertMatches(spec,"/a/b");
assertMatches(spec,"/a/b/c/d/e");
assertNotMatches(spec,"/a");
assertNotMatches(spec,"/aa");
assertNotMatches(spec,"/aa/bb");
}
@Test
public void testSuffixSpec()
{
RegexPathSpec spec = new RegexPathSpec("^(.*).do$");
assertEquals("Spec.pathSpec","^(.*).do$",spec.getDeclaration());
assertEquals("Spec.pattern","^(.*).do$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",0,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.SUFFIX_GLOB,spec.group);
assertMatches(spec,"/a.do");
assertMatches(spec,"/a/b/c.do");
assertMatches(spec,"/abcde.do");
assertMatches(spec,"/abc/efg.do");
assertNotMatches(spec,"/a");
assertNotMatches(spec,"/aa");
assertNotMatches(spec,"/aa/bb");
assertNotMatches(spec,"/aa/bb.do/more");
}
}

View File

@ -0,0 +1,101 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
/**
* Tests of {@link PathMappings#getMatches(String)}
*/
@RunWith(Parameterized.class)
public class ServletPathSpecMatchListTest
{
@Parameters(name="{0} = {1}")
public static List<String[]> testCases()
{
String data[][] = new String[][] {
// From old PathMapTest
{ "All matches", "/animal/bird/path.tar.gz", "[/animal/bird/*=birds, /animal/*=animals, *.tar.gz=tarball, *.gz=gzipped, /=default]"},
{ "Dir matches", "/animal/fish/", "[/animal/fish/*=fishes, /animal/*=animals, /=default]"},
{ "Dir matches", "/animal/fish", "[/animal/fish/*=fishes, /animal/*=animals, /=default]"},
{ "Root matches", "/", "[=root, /=default]"},
{ "Dir matches", "", "[/=default]"}
};
return Arrays.asList(data);
}
private static PathMappings<String> mappings;
static {
mappings = new PathMappings<>();
// From old PathMapTest
mappings.put(new ServletPathSpec("/abs/path"),"abspath"); // 1
mappings.put(new ServletPathSpec("/abs/path/longer"),"longpath"); // 2
mappings.put(new ServletPathSpec("/animal/bird/*"),"birds"); // 3
mappings.put(new ServletPathSpec("/animal/fish/*"),"fishes"); // 4
mappings.put(new ServletPathSpec("/animal/*"),"animals"); // 5
mappings.put(new ServletPathSpec("*.tar.gz"),"tarball"); // 6
mappings.put(new ServletPathSpec("*.gz"),"gzipped"); // 7
mappings.put(new ServletPathSpec("/"),"default"); // 8
// 9 was the old Jetty ":" spec delimited case (no longer valid)
mappings.put(new ServletPathSpec(""),"root"); // 10
mappings.put(new ServletPathSpec("/\u20ACuro/*"),"money"); // 11
}
@Parameter(0)
public String message;
@Parameter(1)
public String inputPath;
@Parameter(2)
public String expectedListing;
@Test
public void testGetMatches()
{
List<MappedResource<String>> matches = mappings.getMatches(inputPath);
StringBuilder actual = new StringBuilder();
actual.append('[');
boolean delim = false;
for (MappedResource<String> res : matches)
{
if (delim)
actual.append(", ");
actual.append(res.getPathSpec().pathSpec).append('=').append(res.getResource());
delim = true;
}
actual.append(']');
assertThat(message + " on [" + inputPath + "]",actual.toString(),is(expectedListing));
}
}

View File

@ -0,0 +1,103 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
/**
* Tests of {@link PathMappings#getMatch(String)}, with a focus on correct mapping selection order
*/
@RunWith(Parameterized.class)
public class ServletPathSpecOrderTest
{
@Parameters(name="{0} = {1}")
public static List<String[]> testCases()
{
String data[][] = new String[][] {
// From old PathMapTest
{"/abs/path","abspath"},
{"/abs/path/xxx","default"},
{"/abs/pith","default"},
{"/abs/path/longer","longpath"},
{"/abs/path/","default"},
{"/abs/path/foo","default"},
{"/animal/bird/eagle/bald","birds"},
{"/animal/fish/shark/hammerhead","fishes"},
{"/animal/insect/ladybug","animals"},
{"/animal","animals"},
{"/animal/","animals"},
{"/animal/other","animals"},
{"/animal/*","animals"},
{"/downloads/distribution.tar.gz","tarball"},
{"/downloads/script.gz","gzipped"},
{"/animal/arhive.gz","animals"},
{"/Other/path","default"},
{"/\u20ACuro/path","money"},
{"/","root"},
// Extra tests
{"/downloads/readme.txt","default"},
{"/downloads/logs.tgz","default"},
{"/main.css","default"}
};
return Arrays.asList(data);
}
private static PathMappings<String> mappings;
static {
mappings = new PathMappings<>();
// From old PathMapTest
mappings.put(new ServletPathSpec("/abs/path"),"abspath"); // 1
mappings.put(new ServletPathSpec("/abs/path/longer"),"longpath"); // 2
mappings.put(new ServletPathSpec("/animal/bird/*"),"birds"); // 3
mappings.put(new ServletPathSpec("/animal/fish/*"),"fishes"); // 4
mappings.put(new ServletPathSpec("/animal/*"),"animals"); // 5
mappings.put(new ServletPathSpec("*.tar.gz"),"tarball"); // 6
mappings.put(new ServletPathSpec("*.gz"),"gzipped"); // 7
mappings.put(new ServletPathSpec("/"),"default"); // 8
// 9 was the old Jetty ":" spec delimited case (no longer valid)
mappings.put(new ServletPathSpec(""),"root"); // 10
mappings.put(new ServletPathSpec("/\u20ACuro/*"),"money"); // 11
}
@Parameter(0)
public String inputPath;
@Parameter(1)
public String expectedResource;
@Test
public void testMatch()
{
assertThat("Match on ["+ inputPath+ "]", mappings.getMatch(inputPath).getResource(), is(expectedResource));
}
}

View File

@ -0,0 +1,188 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import org.junit.Test;
public class ServletPathSpecTest
{
private void assertBadServletPathSpec(String pathSpec)
{
try
{
new ServletPathSpec(pathSpec);
fail("Expected IllegalArgumentException for a bad servlet pathspec on: " + pathSpec);
}
catch (IllegalArgumentException e)
{
// expected path
System.out.println(e);
}
}
private void assertMatches(ServletPathSpec spec, String path)
{
String msg = String.format("Spec(\"%s\").matches(\"%s\")",spec.getDeclaration(),path);
assertThat(msg,spec.matches(path),is(true));
}
private void assertNotMatches(ServletPathSpec spec, String path)
{
String msg = String.format("!Spec(\"%s\").matches(\"%s\")",spec.getDeclaration(),path);
assertThat(msg,spec.matches(path),is(false));
}
@Test
public void testBadServletPathSpecA()
{
assertBadServletPathSpec("foo");
}
@Test
public void testBadServletPathSpecB()
{
assertBadServletPathSpec("/foo/*.do");
}
@Test
public void testBadServletPathSpecC()
{
assertBadServletPathSpec("foo/*.do");
}
@Test
public void testBadServletPathSpecD()
{
assertBadServletPathSpec("foo/*.*do");
}
@Test
public void testBadServletPathSpecE()
{
assertBadServletPathSpec("*do");
}
@Test
public void testDefaultPathSpec()
{
ServletPathSpec spec = new ServletPathSpec("/");
assertEquals("Spec.pathSpec","/",spec.getDeclaration());
assertEquals("Spec.pathDepth",-1,spec.getPathDepth());
}
@Test
public void testExactPathSpec()
{
ServletPathSpec spec = new ServletPathSpec("/abs/path");
assertEquals("Spec.pathSpec","/abs/path",spec.getDeclaration());
assertEquals("Spec.pathDepth",2,spec.getPathDepth());
assertMatches(spec,"/abs/path");
assertNotMatches(spec,"/abs/path/");
assertNotMatches(spec,"/abs/path/more");
assertNotMatches(spec,"/foo");
assertNotMatches(spec,"/foo/abs/path");
assertNotMatches(spec,"/foo/abs/path/");
}
@Test
public void testGetPathInfo()
{
assertEquals("pathInfo exact",null,new ServletPathSpec("/Foo/bar").getPathInfo("/Foo/bar"));
assertEquals("pathInfo prefix","/bar",new ServletPathSpec("/Foo/*").getPathInfo("/Foo/bar"));
assertEquals("pathInfo prefix","/*",new ServletPathSpec("/Foo/*").getPathInfo("/Foo/*"));
assertEquals("pathInfo prefix","/",new ServletPathSpec("/Foo/*").getPathInfo("/Foo/"));
assertEquals("pathInfo prefix",null,new ServletPathSpec("/Foo/*").getPathInfo("/Foo"));
assertEquals("pathInfo suffix",null,new ServletPathSpec("*.ext").getPathInfo("/Foo/bar.ext"));
assertEquals("pathInfo default",null,new ServletPathSpec("/").getPathInfo("/Foo/bar.ext"));
assertEquals("pathInfo default","/xxx/zzz",new ServletPathSpec("/*").getPathInfo("/xxx/zzz"));
}
@Test
public void testNullPathSpec()
{
ServletPathSpec spec = new ServletPathSpec(null);
assertEquals("Spec.pathSpec","",spec.getDeclaration());
assertEquals("Spec.pathDepth",-1,spec.getPathDepth());
}
@Test
public void testRootPathSpec()
{
ServletPathSpec spec = new ServletPathSpec("");
assertEquals("Spec.pathSpec","",spec.getDeclaration());
assertEquals("Spec.pathDepth",-1,spec.getPathDepth());
}
@Test
public void testPathMatch()
{
assertEquals("pathMatch exact","/Foo/bar",new ServletPathSpec("/Foo/bar").getPathMatch("/Foo/bar"));
assertEquals("pathMatch prefix","/Foo",new ServletPathSpec("/Foo/*").getPathMatch("/Foo/bar"));
assertEquals("pathMatch prefix","/Foo",new ServletPathSpec("/Foo/*").getPathMatch("/Foo/"));
assertEquals("pathMatch prefix","/Foo",new ServletPathSpec("/Foo/*").getPathMatch("/Foo"));
assertEquals("pathMatch suffix","/Foo/bar.ext",new ServletPathSpec("*.ext").getPathMatch("/Foo/bar.ext"));
assertEquals("pathMatch default","/Foo/bar.ext",new ServletPathSpec("/").getPathMatch("/Foo/bar.ext"));
assertEquals("pathMatch default","",new ServletPathSpec("/*").getPathMatch("/xxx/zzz"));
}
@Test
public void testPrefixPathSpec()
{
ServletPathSpec spec = new ServletPathSpec("/downloads/*");
assertEquals("Spec.pathSpec","/downloads/*",spec.getDeclaration());
assertEquals("Spec.pathDepth",2,spec.getPathDepth());
assertMatches(spec,"/downloads/logo.jpg");
assertMatches(spec,"/downloads/distribution.tar.gz");
assertMatches(spec,"/downloads/distribution.tgz");
assertMatches(spec,"/downloads/distribution.zip");
assertMatches(spec,"/downloads");
assertEquals("Spec.pathInfo","/",spec.getPathInfo("/downloads/"));
assertEquals("Spec.pathInfo","/distribution.zip",spec.getPathInfo("/downloads/distribution.zip"));
assertEquals("Spec.pathInfo","/dist/9.0/distribution.tar.gz",spec.getPathInfo("/downloads/dist/9.0/distribution.tar.gz"));
}
@Test
public void testSuffixPathSpec()
{
ServletPathSpec spec = new ServletPathSpec("*.gz");
assertEquals("Spec.pathSpec","*.gz",spec.getDeclaration());
assertEquals("Spec.pathDepth",0,spec.getPathDepth());
assertMatches(spec,"/downloads/distribution.tar.gz");
assertMatches(spec,"/downloads/jetty.log.gz");
assertNotMatches(spec,"/downloads/distribution.zip");
assertNotMatches(spec,"/downloads/distribution.tgz");
assertNotMatches(spec,"/abs/path");
assertEquals("Spec.pathInfo",null,spec.getPathInfo("/downloads/distribution.tar.gz"));
}
}

View File

@ -0,0 +1,87 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
/**
* Tests for bad path specs on ServerEndpoint Path Param / URI Template
*/
@RunWith(Parameterized.class)
public class UriTemplatePathSpecBadSpecsTest
{
private static void bad(List<String[]> data, String str)
{
data.add(new String[]
{ str });
}
@Parameters
public static Collection<String[]> data()
{
List<String[]> data = new ArrayList<>();
bad(data,"/a/b{var}"); // bad syntax - variable does not encompass whole path segment
bad(data,"a/{var}"); // bad syntax - no start slash
bad(data,"/a/{var/b}"); // path segment separator in variable name
bad(data,"/{var}/*"); // bad syntax - no globs allowed
bad(data,"/{var}.do"); // bad syntax - variable does not encompass whole path segment
bad(data,"/a/{var*}"); // use of glob character not allowed in variable name
bad(data,"/a/{}"); // bad syntax - no variable name
// MIGHT BE ALLOWED bad(data,"/a/{---}"); // no alpha in variable name
bad(data,"{var}"); // bad syntax - no start slash
bad(data,"/a/{my special variable}"); // bad syntax - space in variable name
bad(data,"/a/{var}/{var}"); // variable name duplicate
// MIGHT BE ALLOWED bad(data,"/a/{var}/{Var}/{vAR}"); // variable name duplicated (diff case)
bad(data,"/a/../../../{var}"); // path navigation not allowed
bad(data,"/a/./{var}"); // path navigation not allowed
bad(data,"/a//{var}"); // bad syntax - double path slash (no path segment)
return data;
}
private String pathSpec;
public UriTemplatePathSpecBadSpecsTest(String pathSpec)
{
this.pathSpec = pathSpec;
}
@Test
public void testBadPathSpec()
{
try
{
new UriTemplatePathSpec(this.pathSpec);
fail("Expected IllegalArgumentException for a bad PathParam pathspec on: " + pathSpec);
}
catch (IllegalArgumentException e)
{
// expected path
System.out.println(e.getMessage());
}
}
}

View File

@ -0,0 +1,284 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http.pathmap;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import java.util.Map;
import org.junit.Test;
/**
* Tests for URI Template Path Specs
*/
public class UriTemplatePathSpecTest
{
private void assertDetectedVars(UriTemplatePathSpec spec, String... expectedVars)
{
String prefix = String.format("Spec(\"%s\")",spec.getDeclaration());
assertEquals(prefix + ".variableCount",expectedVars.length,spec.getVariableCount());
assertEquals(prefix + ".variable.length",expectedVars.length,spec.getVariables().length);
for (int i = 0; i < expectedVars.length; i++)
{
assertEquals(String.format("%s.variable[%d]",prefix,i),expectedVars[i],spec.getVariables()[i]);
}
}
private void assertMatches(PathSpec spec, String path)
{
String msg = String.format("Spec(\"%s\").matches(\"%s\")",spec.getDeclaration(),path);
assertThat(msg,spec.matches(path),is(true));
}
private void assertNotMatches(PathSpec spec, String path)
{
String msg = String.format("!Spec(\"%s\").matches(\"%s\")",spec.getDeclaration(),path);
assertThat(msg,spec.matches(path),is(false));
}
@Test
public void testDefaultPathSpec()
{
UriTemplatePathSpec spec = new UriTemplatePathSpec("/");
assertEquals("Spec.pathSpec","/",spec.getDeclaration());
assertEquals("Spec.pattern","^/$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",1,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.EXACT,spec.getGroup());
assertEquals("Spec.variableCount",0,spec.getVariableCount());
assertEquals("Spec.variable.length",0,spec.getVariables().length);
}
@Test
public void testExactOnePathSpec()
{
UriTemplatePathSpec spec = new UriTemplatePathSpec("/a");
assertEquals("Spec.pathSpec","/a",spec.getDeclaration());
assertEquals("Spec.pattern","^/a$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",1,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.EXACT,spec.getGroup());
assertMatches(spec,"/a");
assertMatches(spec,"/a?type=other");
assertNotMatches(spec,"/a/b");
assertNotMatches(spec,"/a/");
assertEquals("Spec.variableCount",0,spec.getVariableCount());
assertEquals("Spec.variable.length",0,spec.getVariables().length);
}
@Test
public void testExactPathSpec_TestWebapp()
{
UriTemplatePathSpec spec = new UriTemplatePathSpec("/deep.thought/");
assertEquals("Spec.pathSpec","/deep.thought/",spec.getDeclaration());
assertEquals("Spec.pattern","^/deep\\.thought/$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",1,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.EXACT,spec.getGroup());
assertMatches(spec,"/deep.thought/");
assertNotMatches(spec,"/deep.thought");
assertEquals("Spec.variableCount",0,spec.getVariableCount());
assertEquals("Spec.variable.length",0,spec.getVariables().length);
}
@Test
public void testExactTwoPathSpec()
{
UriTemplatePathSpec spec = new UriTemplatePathSpec("/a/b");
assertEquals("Spec.pathSpec","/a/b",spec.getDeclaration());
assertEquals("Spec.pattern","^/a/b$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",2,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.EXACT,spec.getGroup());
assertEquals("Spec.variableCount",0,spec.getVariableCount());
assertEquals("Spec.variable.length",0,spec.getVariables().length);
assertMatches(spec,"/a/b");
assertNotMatches(spec,"/a/b/");
assertNotMatches(spec,"/a/");
assertNotMatches(spec,"/a/bb");
}
@Test
public void testMiddleVarPathSpec()
{
UriTemplatePathSpec spec = new UriTemplatePathSpec("/a/{var}/c");
assertEquals("Spec.pathSpec","/a/{var}/c",spec.getDeclaration());
assertEquals("Spec.pattern","^/a/([^/]+)/c$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",3,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.MIDDLE_GLOB,spec.getGroup());
assertDetectedVars(spec,"var");
assertMatches(spec,"/a/b/c");
assertMatches(spec,"/a/zz/c");
assertMatches(spec,"/a/hello+world/c");
assertNotMatches(spec,"/a/bc");
assertNotMatches(spec,"/a/b/");
assertNotMatches(spec,"/a/b");
Map<String, String> mapped = spec.getPathParams("/a/b/c");
assertThat("Spec.pathParams",mapped,notNullValue());
assertThat("Spec.pathParams.size",mapped.size(),is(1));
assertEquals("Spec.pathParams[var]","b",mapped.get("var"));
}
@Test
public void testOneVarPathSpec()
{
UriTemplatePathSpec spec = new UriTemplatePathSpec("/a/{foo}");
assertEquals("Spec.pathSpec","/a/{foo}",spec.getDeclaration());
assertEquals("Spec.pattern","^/a/([^/]+)$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",2,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.PREFIX_GLOB,spec.getGroup());
assertDetectedVars(spec,"foo");
assertMatches(spec,"/a/b");
assertNotMatches(spec,"/a/");
assertNotMatches(spec,"/a");
Map<String, String> mapped = spec.getPathParams("/a/b");
assertThat("Spec.pathParams",mapped,notNullValue());
assertThat("Spec.pathParams.size",mapped.size(),is(1));
assertEquals("Spec.pathParams[foo]","b",mapped.get("foo"));
}
@Test
public void testOneVarSuffixPathSpec()
{
UriTemplatePathSpec spec = new UriTemplatePathSpec("/{var}/b/c");
assertEquals("Spec.pathSpec","/{var}/b/c",spec.getDeclaration());
assertEquals("Spec.pattern","^/([^/]+)/b/c$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",3,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.SUFFIX_GLOB,spec.getGroup());
assertDetectedVars(spec,"var");
assertMatches(spec,"/a/b/c");
assertMatches(spec,"/az/b/c");
assertMatches(spec,"/hello+world/b/c");
assertNotMatches(spec,"/a/bc");
assertNotMatches(spec,"/a/b/");
assertNotMatches(spec,"/a/b");
Map<String, String> mapped = spec.getPathParams("/a/b/c");
assertThat("Spec.pathParams",mapped,notNullValue());
assertThat("Spec.pathParams.size",mapped.size(),is(1));
assertEquals("Spec.pathParams[var]","a",mapped.get("var"));
}
@Test
public void testTwoVarComplexInnerPathSpec()
{
UriTemplatePathSpec spec = new UriTemplatePathSpec("/a/{var1}/c/{var2}/e");
assertEquals("Spec.pathSpec","/a/{var1}/c/{var2}/e",spec.getDeclaration());
assertEquals("Spec.pattern","^/a/([^/]+)/c/([^/]+)/e$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",5,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.MIDDLE_GLOB,spec.getGroup());
assertDetectedVars(spec,"var1","var2");
assertMatches(spec,"/a/b/c/d/e");
assertNotMatches(spec,"/a/bc/d/e");
assertNotMatches(spec,"/a/b/d/e");
assertNotMatches(spec,"/a/b//d/e");
Map<String, String> mapped = spec.getPathParams("/a/b/c/d/e");
assertThat("Spec.pathParams",mapped,notNullValue());
assertThat("Spec.pathParams.size",mapped.size(),is(2));
assertEquals("Spec.pathParams[var1]","b",mapped.get("var1"));
assertEquals("Spec.pathParams[var2]","d",mapped.get("var2"));
}
@Test
public void testTwoVarComplexOuterPathSpec()
{
UriTemplatePathSpec spec = new UriTemplatePathSpec("/{var1}/b/{var2}/{var3}");
assertEquals("Spec.pathSpec","/{var1}/b/{var2}/{var3}",spec.getDeclaration());
assertEquals("Spec.pattern","^/([^/]+)/b/([^/]+)/([^/]+)$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",4,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.MIDDLE_GLOB,spec.getGroup());
assertDetectedVars(spec,"var1","var2","var3");
assertMatches(spec,"/a/b/c/d");
assertNotMatches(spec,"/a/bc/d/e");
assertNotMatches(spec,"/a/c/d/e");
assertNotMatches(spec,"/a//d/e");
Map<String, String> mapped = spec.getPathParams("/a/b/c/d");
assertThat("Spec.pathParams",mapped,notNullValue());
assertThat("Spec.pathParams.size",mapped.size(),is(3));
assertEquals("Spec.pathParams[var1]","a",mapped.get("var1"));
assertEquals("Spec.pathParams[var2]","c",mapped.get("var2"));
assertEquals("Spec.pathParams[var3]","d",mapped.get("var3"));
}
@Test
public void testTwoVarPrefixPathSpec()
{
UriTemplatePathSpec spec = new UriTemplatePathSpec("/a/{var1}/{var2}");
assertEquals("Spec.pathSpec","/a/{var1}/{var2}",spec.getDeclaration());
assertEquals("Spec.pattern","^/a/([^/]+)/([^/]+)$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",3,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.PREFIX_GLOB,spec.getGroup());
assertDetectedVars(spec,"var1","var2");
assertMatches(spec,"/a/b/c");
assertNotMatches(spec,"/a/bc");
assertNotMatches(spec,"/a/b/");
assertNotMatches(spec,"/a/b");
Map<String, String> mapped = spec.getPathParams("/a/b/c");
assertThat("Spec.pathParams",mapped,notNullValue());
assertThat("Spec.pathParams.size",mapped.size(),is(2));
assertEquals("Spec.pathParams[var1]","b",mapped.get("var1"));
assertEquals("Spec.pathParams[var2]","c",mapped.get("var2"));
}
@Test
public void testVarOnlyPathSpec()
{
UriTemplatePathSpec spec = new UriTemplatePathSpec("/{var1}");
assertEquals("Spec.pathSpec","/{var1}",spec.getDeclaration());
assertEquals("Spec.pattern","^/([^/]+)$",spec.getPattern().pattern());
assertEquals("Spec.pathDepth",1,spec.getPathDepth());
assertEquals("Spec.group",PathSpecGroup.PREFIX_GLOB,spec.getGroup());
assertDetectedVars(spec,"var1");
assertMatches(spec,"/a");
assertNotMatches(spec,"/");
assertNotMatches(spec,"/a/b");
assertNotMatches(spec,"/a/b/c");
Map<String, String> mapped = spec.getPathParams("/a");
assertThat("Spec.pathParams",mapped,notNullValue());
assertThat("Spec.pathParams.size",mapped.size(),is(1));
assertEquals("Spec.pathParams[var1]","a",mapped.get("var1"));
}
}

View File

@ -39,6 +39,7 @@ import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
@ -585,6 +586,48 @@ public class HTTP2Test extends AbstractTest
Assert.assertTrue(completeLatch.await(5, TimeUnit.SECONDS));
}
@Test
public void testCleanGoAwayDoesNotTriggerFailureNotification() throws Exception
{
start(new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
MetaData.Response metaData = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
HeadersFrame response = new HeadersFrame(stream.getId(), metaData, null, true);
stream.headers(response, Callback.NOOP);
// Close cleanly.
stream.getSession().close(ErrorCode.NO_ERROR.code, null, Callback.NOOP);
return null;
}
});
CountDownLatch closeLatch = new CountDownLatch(1);
CountDownLatch failureLatch = new CountDownLatch(1);
Session session = newClient(new Session.Listener.Adapter()
{
@Override
public void onClose(Session session, GoAwayFrame frame)
{
closeLatch.countDown();
}
@Override
public void onFailure(Session session, Throwable failure)
{
failureLatch.countDown();
}
});
MetaData.Request metaData = newRequest("GET", new HttpFields());
HeadersFrame request = new HeadersFrame(metaData, null, true);
session.newStream(request, new Promise.Adapter<>(), new Stream.Listener.Adapter());
// Make sure onClose() is called.
Assert.assertTrue(closeLatch.await(5, TimeUnit.SECONDS));
Assert.assertFalse(failureLatch.await(1, TimeUnit.SECONDS));
}
private static void sleep(long time)
{
try

View File

@ -230,7 +230,7 @@ public class HTTP2Flusher extends IteratingCallback
if (actives.isEmpty())
{
if (isClosed())
abort(new ClosedChannelException());
fail(new ClosedChannelException(), true);
if (LOG.isDebugEnabled())
LOG.debug("Flushed {}", session);
@ -289,9 +289,6 @@ public class HTTP2Flusher extends IteratingCallback
@Override
protected void onCompleteFailure(Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("Failed", x);
lease.recycle();
// Transfer active items to avoid reentrancy.
@ -306,10 +303,10 @@ public class HTTP2Flusher extends IteratingCallback
entry.failed(x);
}
abort(x);
fail(x, isClosed());
}
private void abort(Throwable x)
private void fail(Throwable x, boolean closed)
{
Queue<Entry> queued;
synchronized (this)
@ -319,12 +316,13 @@ public class HTTP2Flusher extends IteratingCallback
}
if (LOG.isDebugEnabled())
LOG.debug("Aborting, queued={}", queued.size());
LOG.debug("{}, queued={}", closed ? "Closing" : "Failing", queued.size());
for (Entry entry : queued)
closed(entry, x);
entry.failed(x);
session.abort(x);
if (!closed)
session.abort(x);
}
private void closed(Entry entry, Throwable failure)

View File

@ -989,8 +989,16 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
@Override
public String toString()
{
return String.format("%s@%x{queueSize=%d,sendWindow=%s,recvWindow=%s,streams=%d,%s}", getClass().getSimpleName(),
hashCode(), flusher.getQueueSize(), sendWindow, recvWindow, streams.size(), closed);
return String.format("%s@%x{l:%s <-> r:%s,queueSize=%d,sendWindow=%s,recvWindow=%s,streams=%d,%s}",
getClass().getSimpleName(),
hashCode(),
getEndPoint().getLocalAddress(),
getEndPoint().getRemoteAddress(),
flusher.getQueueSize(),
sendWindow,
recvWindow,
streams.size(),
closed);
}
private class ControlEntry extends HTTP2Flusher.Entry

View File

@ -75,7 +75,12 @@ public class HeaderBlockParser
MetaData result = hpackDecoder.decode(toDecode);
buffer.limit(limit);
byteBufferPool.release(blockBuffer);
if (blockBuffer != null)
{
byteBufferPool.release(blockBuffer);
blockBuffer = null;
}
return result;
}

View File

@ -130,18 +130,23 @@ public class DataGenerateParseTest
}
}, 4096, 8192);
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
ByteBuffer data = ByteBuffer.wrap(largeContent);
generator.generateData(lease, 13, data.slice(), true, data.remaining());
for (ByteBuffer buffer : lease.getByteBuffers())
// Iterate a few times to be sure generator and parser are properly reset.
for (int i = 0; i < 2; ++i)
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
ByteBuffer data = ByteBuffer.wrap(largeContent);
generator.generateData(lease, 13, data.slice(), true, data.remaining());
Assert.assertEquals(largeContent.length, frames.size());
frames.clear();
for (ByteBuffer buffer : lease.getByteBuffers())
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
Assert.assertEquals(largeContent.length, frames.size());
}
}
}

View File

@ -96,21 +96,26 @@ public class GoAwayGenerateParseTest
byte[] payload = new byte[16];
new Random().nextBytes(payload);
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generateGoAway(lease, lastStreamId, error, payload);
for (ByteBuffer buffer : lease.getByteBuffers())
// Iterate a few times to be sure generator and parser are properly reset.
for (int i = 0; i < 2; ++i)
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generateGoAway(lease, lastStreamId, error, payload);
Assert.assertEquals(1, frames.size());
GoAwayFrame frame = frames.get(0);
Assert.assertEquals(lastStreamId, frame.getLastStreamId());
Assert.assertEquals(error, frame.getError());
Assert.assertArrayEquals(payload, frame.getPayload());
frames.clear();
for (ByteBuffer buffer : lease.getByteBuffers())
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
Assert.assertEquals(1, frames.size());
GoAwayFrame frame = frames.get(0);
Assert.assertEquals(lastStreamId, frame.getLastStreamId());
Assert.assertEquals(error, frame.getError());
Assert.assertArrayEquals(payload, frame.getPayload());
}
}
}

View File

@ -114,41 +114,47 @@ public class HeadersGenerateParseTest
}
}, 4096, 8192);
int streamId = 13;
HttpFields fields = new HttpFields();
fields.put("Accept", "text/html");
fields.put("User-Agent", "Jetty");
MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP, new HostPortHttpField("localhost:8080"), "/path", HttpVersion.HTTP_2, fields);
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
PriorityFrame priorityFrame = new PriorityFrame(streamId, 3 * streamId, 200, true);
generator.generateHeaders(lease, streamId, metaData, priorityFrame, true);
for (ByteBuffer buffer : lease.getByteBuffers())
// Iterate a few times to be sure generator and parser are properly reset.
for (int i = 0; i < 2; ++i)
{
while (buffer.hasRemaining())
int streamId = 13;
HttpFields fields = new HttpFields();
fields.put("Accept", "text/html");
fields.put("User-Agent", "Jetty");
MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP, new HostPortHttpField("localhost:8080"), "/path", HttpVersion.HTTP_2, fields);
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
PriorityFrame priorityFrame = new PriorityFrame(streamId, 3 * streamId, 200, true);
generator.generateHeaders(lease, streamId, metaData, priorityFrame, true);
frames.clear();
for (ByteBuffer buffer : lease.getByteBuffers())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
buffer = buffer.slice();
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
}
Assert.assertEquals(1, frames.size());
HeadersFrame frame = frames.get(0);
Assert.assertEquals(streamId, frame.getStreamId());
Assert.assertTrue(frame.isEndStream());
MetaData.Request request = (MetaData.Request)frame.getMetaData();
Assert.assertEquals(metaData.getMethod(), request.getMethod());
Assert.assertEquals(metaData.getURI(), request.getURI());
for (int j = 0; j < fields.size(); ++j)
{
HttpField field = fields.getField(j);
Assert.assertTrue(request.getFields().contains(field));
Assert.assertEquals(1, frames.size());
HeadersFrame frame = frames.get(0);
Assert.assertEquals(streamId, frame.getStreamId());
Assert.assertTrue(frame.isEndStream());
MetaData.Request request = (MetaData.Request)frame.getMetaData();
Assert.assertEquals(metaData.getMethod(), request.getMethod());
Assert.assertEquals(metaData.getURI(), request.getURI());
for (int j = 0; j < fields.size(); ++j)
{
HttpField field = fields.getField(j);
Assert.assertTrue(request.getFields().contains(field));
}
PriorityFrame priority = frame.getPriority();
Assert.assertNotNull(priority);
Assert.assertEquals(priorityFrame.getStreamId(), priority.getStreamId());
Assert.assertEquals(priorityFrame.getParentStreamId(), priority.getParentStreamId());
Assert.assertEquals(priorityFrame.getWeight(), priority.getWeight());
Assert.assertEquals(priorityFrame.isExclusive(), priority.isExclusive());
}
PriorityFrame priority = frame.getPriority();
Assert.assertNotNull(priority);
Assert.assertEquals(priorityFrame.getStreamId(), priority.getStreamId());
Assert.assertEquals(priorityFrame.getParentStreamId(), priority.getParentStreamId());
Assert.assertEquals(priorityFrame.getWeight(), priority.getWeight());
Assert.assertEquals(priorityFrame.isExclusive(), priority.isExclusive());
}
}

View File

@ -93,21 +93,26 @@ public class PingGenerateParseTest
byte[] payload = new byte[8];
new Random().nextBytes(payload);
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generatePing(lease, payload, true);
for (ByteBuffer buffer : lease.getByteBuffers())
// Iterate a few times to be sure generator and parser are properly reset.
for (int i = 0; i < 2; ++i)
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generatePing(lease, payload, true);
Assert.assertEquals(1, frames.size());
PingFrame frame = frames.get(0);
Assert.assertArrayEquals(payload, frame.getPayload());
Assert.assertTrue(frame.isReply());
frames.clear();
for (ByteBuffer buffer : lease.getByteBuffers())
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
Assert.assertEquals(1, frames.size());
PingFrame frame = frames.get(0);
Assert.assertArrayEquals(payload, frame.getPayload());
Assert.assertTrue(frame.isReply());
}
}
@Test

View File

@ -98,22 +98,27 @@ public class PriorityGenerateParseTest
int weight = 3;
boolean exclusive = true;
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generatePriority(lease, streamId, parentStreamId, weight, exclusive);
for (ByteBuffer buffer : lease.getByteBuffers())
// Iterate a few times to be sure generator and parser are properly reset.
for (int i = 0; i < 2; ++i)
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generatePriority(lease, streamId, parentStreamId, weight, exclusive);
Assert.assertEquals(1, frames.size());
PriorityFrame frame = frames.get(0);
Assert.assertEquals(streamId, frame.getStreamId());
Assert.assertEquals(parentStreamId, frame.getParentStreamId());
Assert.assertEquals(weight, frame.getWeight());
Assert.assertEquals(exclusive, frame.isExclusive());
frames.clear();
for (ByteBuffer buffer : lease.getByteBuffers())
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
Assert.assertEquals(1, frames.size());
PriorityFrame frame = frames.get(0);
Assert.assertEquals(streamId, frame.getStreamId());
Assert.assertEquals(parentStreamId, frame.getParentStreamId());
Assert.assertEquals(weight, frame.getWeight());
Assert.assertEquals(exclusive, frame.isExclusive());
}
}
}

View File

@ -115,28 +115,33 @@ public class PushPromiseGenerateParseTest
fields.put("User-Agent", "Jetty");
MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP, new HostPortHttpField("localhost:8080"), "/path", HttpVersion.HTTP_2, fields);
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generatePushPromise(lease, streamId, promisedStreamId, metaData);
for (ByteBuffer buffer : lease.getByteBuffers())
// Iterate a few times to be sure generator and parser are properly reset.
for (int i = 0; i < 2; ++i)
{
while (buffer.hasRemaining())
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generatePushPromise(lease, streamId, promisedStreamId, metaData);
frames.clear();
for (ByteBuffer buffer : lease.getByteBuffers())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
}
Assert.assertEquals(1, frames.size());
PushPromiseFrame frame = frames.get(0);
Assert.assertEquals(streamId, frame.getStreamId());
Assert.assertEquals(promisedStreamId, frame.getPromisedStreamId());
MetaData.Request request = (MetaData.Request)frame.getMetaData();
Assert.assertEquals(metaData.getMethod(), request.getMethod());
Assert.assertEquals(metaData.getURI(), request.getURI());
for (int j = 0; j < fields.size(); ++j)
{
HttpField field = fields.getField(j);
Assert.assertTrue(request.getFields().contains(field));
Assert.assertEquals(1, frames.size());
PushPromiseFrame frame = frames.get(0);
Assert.assertEquals(streamId, frame.getStreamId());
Assert.assertEquals(promisedStreamId, frame.getPromisedStreamId());
MetaData.Request request = (MetaData.Request)frame.getMetaData();
Assert.assertEquals(metaData.getMethod(), request.getMethod());
Assert.assertEquals(metaData.getURI(), request.getURI());
for (int j = 0; j < fields.size(); ++j)
{
HttpField field = fields.getField(j);
Assert.assertTrue(request.getFields().contains(field));
}
}
}
}

View File

@ -92,20 +92,25 @@ public class ResetGenerateParseTest
int streamId = 13;
int error = 17;
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generateReset(lease, streamId, error);
for (ByteBuffer buffer : lease.getByteBuffers())
// Iterate a few times to be sure generator and parser are properly reset.
for (int i = 0; i < 2; ++i)
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generateReset(lease, streamId, error);
Assert.assertEquals(1, frames.size());
ResetFrame frame = frames.get(0);
Assert.assertEquals(streamId, frame.getStreamId());
Assert.assertEquals(error, frame.getError());
frames.clear();
for (ByteBuffer buffer : lease.getByteBuffers())
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
Assert.assertEquals(1, frames.size());
ResetFrame frame = frames.get(0);
Assert.assertEquals(streamId, frame.getStreamId());
Assert.assertEquals(error, frame.getError());
}
}
}

View File

@ -155,22 +155,27 @@ public class SettingsGenerateParseTest
Integer value = 17;
settings1.put(key, value);
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generateSettings(lease, settings1, true);
for (ByteBuffer buffer : lease.getByteBuffers())
// Iterate a few times to be sure generator and parser are properly reset.
for (int i = 0; i < 2; ++i)
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generateSettings(lease, settings1, true);
Assert.assertEquals(1, frames.size());
SettingsFrame frame = frames.get(0);
Map<Integer, Integer> settings2 = frame.getSettings();
Assert.assertEquals(1, settings2.size());
Assert.assertEquals(value, settings2.get(key));
Assert.assertTrue(frame.isReply());
frames.clear();
for (ByteBuffer buffer : lease.getByteBuffers())
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
Assert.assertEquals(1, frames.size());
SettingsFrame frame = frames.get(0);
Map<Integer, Integer> settings2 = frame.getSettings();
Assert.assertEquals(1, settings2.size());
Assert.assertEquals(value, settings2.get(key));
Assert.assertTrue(frame.isReply());
}
}
}

View File

@ -92,20 +92,25 @@ public class WindowUpdateGenerateParseTest
int streamId = 13;
int windowUpdate = 17;
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generateWindowUpdate(lease, streamId, windowUpdate);
for (ByteBuffer buffer : lease.getByteBuffers())
// Iterate a few times to be sure generator and parser are properly reset.
for (int i = 0; i < 2; ++i)
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.generateWindowUpdate(lease, streamId, windowUpdate);
Assert.assertEquals(1, frames.size());
WindowUpdateFrame frame = frames.get(0);
Assert.assertEquals(streamId, frame.getStreamId());
Assert.assertEquals(windowUpdate, frame.getWindowDelta());
frames.clear();
for (ByteBuffer buffer : lease.getByteBuffers())
{
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
}
}
Assert.assertEquals(1, frames.size());
WindowUpdateFrame frame = frames.get(0);
Assert.assertEquals(streamId, frame.getStreamId());
Assert.assertEquals(windowUpdate, frame.getWindowDelta());
}
}
}

View File

@ -61,6 +61,23 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-server</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>

View File

@ -32,6 +32,8 @@ import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.client.HTTP2Client;
import org.eclipse.jetty.http2.client.HTTP2ClientConnectionFactory;
import org.eclipse.jetty.http2.frames.GoAwayFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.ssl.SslClientConnectionFactory;
@ -105,39 +107,17 @@ public class HttpClientTransportOverHTTP2 extends ContainerLifeCycle implements
{
client.setConnectTimeout(httpClient.getConnectTimeout());
final HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY);
HttpDestinationOverHTTP2 destination = (HttpDestinationOverHTTP2)context.get(HTTP_DESTINATION_CONTEXT_KEY);
@SuppressWarnings("unchecked")
final Promise<Connection> connection = (Promise<Connection>)context.get(HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
Promise<Connection> connectionPromise = (Promise<Connection>)context.get(HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
Session.Listener listener = new Session.Listener.Adapter()
{
@Override
public void onFailure(Session session, Throwable failure)
{
destination.abort(failure);
}
};
final Promise<Session> promise = new Promise<Session>()
{
@Override
public void succeeded(Session session)
{
connection.succeeded(newHttpConnection(destination, session));
}
@Override
public void failed(Throwable failure)
{
connection.failed(failure);
}
};
SessionListenerPromise listenerPromise = new SessionListenerPromise(destination, connectionPromise);
SslContextFactory sslContextFactory = null;
if (HttpScheme.HTTPS.is(destination.getScheme()))
sslContextFactory = httpClient.getSslContextFactory();
client.connect(sslContextFactory, address, listener, promise, context);
client.connect(sslContextFactory, address, listenerPromise, listenerPromise, context);
}
@Override
@ -154,4 +134,50 @@ public class HttpClientTransportOverHTTP2 extends ContainerLifeCycle implements
{
return new HttpConnectionOverHTTP2(destination, session);
}
private class SessionListenerPromise extends Session.Listener.Adapter implements Promise<Session>
{
private final HttpDestinationOverHTTP2 destination;
private final Promise<Connection> promise;
private HttpConnectionOverHTTP2 connection;
public SessionListenerPromise(HttpDestinationOverHTTP2 destination, Promise<Connection> promise)
{
this.destination = destination;
this.promise = promise;
}
@Override
public void succeeded(Session session)
{
connection = newHttpConnection(destination, session);
promise.succeeded(connection);
}
@Override
public void failed(Throwable failure)
{
promise.failed(failure);
}
@Override
public void onSettings(Session session, SettingsFrame frame)
{
Map<Integer, Integer> settings = frame.getSettings();
if (settings.containsKey(SettingsFrame.MAX_CONCURRENT_STREAMS))
destination.setMaxRequestsPerConnection(settings.get(SettingsFrame.MAX_CONCURRENT_STREAMS));
}
@Override
public void onClose(Session session, GoAwayFrame frame)
{
connection.close();
}
@Override
public void onFailure(Session session, Throwable failure)
{
connection.close(failure);
}
}
}

View File

@ -57,16 +57,22 @@ public class HttpConnectionOverHTTP2 extends HttpConnection
protected void release(HttpChannel channel)
{
channels.remove(channel);
getHttpDestination().release(this);
}
@Override
public void close()
{
close(new AsynchronousCloseException());
}
protected void close(Throwable failure)
{
// First close then abort, to be sure that the connection cannot be reused
// from an onFailure() handler or by blocking code waiting for completion.
getHttpDestination().close(this);
session.close(ErrorCode.NO_ERROR.code, null, Callback.NOOP);
abort(new AsynchronousCloseException());
abort(failure);
}
private void abort(Throwable failure)
@ -79,4 +85,13 @@ public class HttpConnectionOverHTTP2 extends HttpConnection
}
channels.clear();
}
@Override
public String toString()
{
return String.format("%s@%h[%s]",
getClass().getSimpleName(),
this,
session);
}
}

View File

@ -0,0 +1,70 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http2.client.http;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.http2.client.HTTP2Client;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.toolchain.test.TestTracker;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.After;
import org.junit.Rule;
public class AbstractTest
{
@Rule
public TestTracker tracker = new TestTracker();
protected Server server;
protected ServerConnector connector;
protected HttpClient client;
protected void start(int maxConcurrentStreams, Handler handler) throws Exception
{
QueuedThreadPool serverExecutor = new QueuedThreadPool();
serverExecutor.setName("server");
server = new Server(serverExecutor);
HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(new HttpConfiguration());
http2.setMaxConcurrentStreams(maxConcurrentStreams);
connector = new ServerConnector(server, 1, 1, http2);
server.addConnector(connector);
server.setHandler(handler);
server.start();
client = new HttpClient(new HttpClientTransportOverHTTP2(new HTTP2Client()), null);
QueuedThreadPool clientExecutor = new QueuedThreadPool();
clientExecutor.setName("client");
client.setExecutor(clientExecutor);
client.start();
}
@After
public void dispose() throws Exception
{
if (client != null)
client.stop();
if (server != null)
server.stop();
}
}

View File

@ -0,0 +1,197 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.http2.client.http;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.junit.Assert;
import org.junit.Test;
public class MaxConcurrentStreamsTest extends AbstractTest
{
@Test
public void testOneConcurrentStream() throws Exception
{
long sleep = 1000;
start(1, new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
// Sleep a bit to allow the second request to be queued.
sleep(sleep);
}
});
client.setMaxConnectionsPerDestination(1);
// Prime the connection so that the maxConcurrentStream setting arrives to the client.
client.newRequest("localhost", connector.getLocalPort()).path("/prime").send();
CountDownLatch latch = new CountDownLatch(2);
// First request is sent immediately.
client.newRequest("localhost", connector.getLocalPort())
.path("/first")
.send(result ->
{
if (result.isSucceeded())
latch.countDown();
});
// Second request is queued.
client.newRequest("localhost", connector.getLocalPort())
.path("/second")
.send(result ->
{
if (result.isSucceeded())
latch.countDown();
});
// When the first request returns, the second must be sent.
Assert.assertTrue(latch.await(5 * sleep, TimeUnit.MILLISECONDS));
}
@Test
public void testTwoConcurrentStreamsThirdWaits() throws Exception
{
int maxStreams = 2;
long sleep = 1000;
start(maxStreams, new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
sleep(sleep);
}
});
client.setMaxConnectionsPerDestination(1);
// Prime the connection so that the maxConcurrentStream setting arrives to the client.
client.newRequest("localhost", connector.getLocalPort()).path("/prime").send();
// Send requests up to the max allowed.
for (int i = 0; i < maxStreams; ++i)
{
client.newRequest("localhost", connector.getLocalPort())
.path("/" + i)
.send(null);
}
// Send the request in excess.
CountDownLatch latch = new CountDownLatch(1);
String path = "/excess";
client.newRequest("localhost", connector.getLocalPort())
.path(path)
.send(result ->
{
if (result.getResponse().getStatus() == HttpStatus.OK_200)
latch.countDown();
});
// The last exchange should remain in the queue.
HttpDestinationOverHTTP2 destination = (HttpDestinationOverHTTP2)client.getDestination("http", "localhost", connector.getLocalPort());
Assert.assertEquals(1, destination.getHttpExchanges().size());
Assert.assertEquals(path, destination.getHttpExchanges().peek().getRequest().getPath());
Assert.assertTrue(latch.await(5 * sleep, TimeUnit.MILLISECONDS));
}
@Test
public void testAbortedWhileQueued() throws Exception
{
long sleep = 1000;
start(1, new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
sleep(sleep);
}
});
client.setMaxConnectionsPerDestination(1);
// Prime the connection so that the maxConcurrentStream setting arrives to the client.
client.newRequest("localhost", connector.getLocalPort()).path("/prime").send();
// Send a request that is aborted while queued.
client.newRequest("localhost", connector.getLocalPort())
.path("/aborted")
.onRequestQueued(request -> request.abort(new Exception()))
.send(null);
// Must be able to send another request.
ContentResponse response = client.newRequest("localhost", connector.getLocalPort()).path("/check").send();
Assert.assertEquals(HttpStatus.OK_200, response.getStatus());
}
@Test
public void testMultipleRequestsQueuedOnConnect() throws Exception
{
int maxConcurrent = 10;
long sleep = 500;
start(maxConcurrent, new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
sleep(sleep);
}
});
client.setMaxConnectionsPerDestination(1);
// The first request will open the connection, the others will be queued.
CountDownLatch latch = new CountDownLatch(maxConcurrent);
for (int i = 0; i < maxConcurrent; ++i)
{
client.newRequest("localhost", connector.getLocalPort())
.path("/" + i)
.send(result -> latch.countDown());
}
// The requests should be processed in parallel, not sequentially.
Assert.assertTrue(latch.await(maxConcurrent * sleep / 2, TimeUnit.MILLISECONDS));
}
private void sleep(long time)
{
try
{
Thread.sleep(time);
}
catch (InterruptedException x)
{
throw new RuntimeException(x);
}
}
}

View File

@ -36,7 +36,7 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.PathMap;
import org.eclipse.jetty.http.pathmap.PathSpecSet;
import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.HandlerWrapper;
@ -72,9 +72,9 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
// non-static, as other GzipHandler instances may have different configurations
private final ThreadLocal<Deflater> _deflater = new ThreadLocal<Deflater>();
private final IncludeExclude<String> _agentPatterns=new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER);
private final IncludeExclude<String> _agentPatterns=new IncludeExclude<>(RegexSet.class);
private final IncludeExclude<String> _methods = new IncludeExclude<>();
private final IncludeExclude<String> _paths = new IncludeExclude<>(PathMap.PathSet.class,PathMap.PathSet.MATCHER);
private final IncludeExclude<String> _paths = new IncludeExclude<String>(PathSpecSet.class);
private final IncludeExclude<String> _mimeTypes = new IncludeExclude<>();
private HttpField _vary;
@ -144,9 +144,27 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
/* ------------------------------------------------------------ */
/**
* Add path to excluded paths list.
* <p>
* There are 2 syntaxes supported, Servlet <code>url-pattern</code> based, and
* Regex based. This means that the initial characters on the path spec
* line are very strict, and determine the behavior of the path matching.
* <ul>
* <li>If the spec starts with <code>'^'</code> the spec is assumed to be
* a regex based path spec and will match with normal Java regex rules.</li>
* <li>If the spec starts with <code>'/'</code> then spec is assumed to be
* a Servlet url-pattern rules path spec for either an exact match
* or prefix based match.</li>
* <li>If the spec starts with <code>'*.'</code> then spec is assumed to be
* a Servlet url-pattern rules path spec for a suffix based match.</li>
* <li>All other syntaxes are unsupported</li>
* </ul>
* <p>
* Note: inclusion takes precedence over exclude.
*
* @param pathspecs Path specs (as per servlet spec) to exclude. If a
* ServletContext is available, the paths are relative to the context path,
* otherwise they are absolute.
* otherwise they are absolute.<br>
* For backward compatibility the pathspecs may be comma separated strings, but this
* will not be supported in future versions.
*/
@ -191,12 +209,27 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
/* ------------------------------------------------------------ */
/**
* Add path specs to include. Inclusion takes precedence over exclusion.
* Add path specs to include.
* <p>
* There are 2 syntaxes supported, Servlet <code>url-pattern</code> based, and
* Regex based. This means that the initial characters on the path spec
* line are very strict, and determine the behavior of the path matching.
* <ul>
* <li>If the spec starts with <code>'^'</code> the spec is assumed to be
* a regex based path spec and will match with normal Java regex rules.</li>
* <li>If the spec starts with <code>'/'</code> then spec is assumed to be
* a Servlet url-pattern rules path spec for either an exact match
* or prefix based match.</li>
* <li>If the spec starts with <code>'*.'</code> then spec is assumed to be
* a Servlet url-pattern rules path spec for a suffix based match.</li>
* <li>All other syntaxes are unsupported</li>
* </ul>
* <p>
* Note: inclusion takes precedence over exclude.
*
* @param pathspecs Path specs (as per servlet spec) to include. If a
* ServletContext is available, the paths are relative to the context path,
* otherwise they are absolute
* For backward compatibility the pathspecs may be comma separated strings, but this
* will not be supported in future versions.
*/
public void addIncludedPaths(String... pathspecs)
{
@ -334,9 +367,9 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
/* ------------------------------------------------------------ */
/**
* Get the minimum reponse size.
* Get the minimum response size.
*
* @return minimum reponse size
* @return minimum response size
*/
public int getMinGzipSize()
{

View File

@ -0,0 +1,42 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.handler.gzip;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import java.util.Arrays;
import org.junit.Test;
public class GzipHandlerTest
{
@Test
public void testAddGetPaths()
{
GzipHandler gzip = new GzipHandler();
gzip.addIncludedPaths("/foo");
gzip.addIncludedPaths("^/bar.*$");
String[] includedPaths = gzip.getIncludedPaths();
assertThat("Included Paths.size", includedPaths.length, is(2));
assertThat("Included Paths", Arrays.asList(includedPaths), contains("/foo","^/bar.*$"));
}
}

View File

@ -51,6 +51,7 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.PathMap.MappedEntry;
import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.http.ResourceHttpContent;
import org.eclipse.jetty.io.WriterOutputStream;
@ -672,9 +673,9 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory
if ((_welcomeServlets || _welcomeExactServlets) && welcome_servlet==null)
{
MappedEntry<?> entry=_servletHandler.getHolderEntry(welcome_in_context);
if (entry!=null && entry.getValue()!=_defaultHolder &&
(_welcomeServlets || (_welcomeExactServlets && entry.getKey().equals(welcome_in_context))))
MappedResource<ServletHolder> entry=_servletHandler.getHolderEntry(welcome_in_context);
if (entry!=null && entry.getResource()!=_defaultHolder &&
(_welcomeServlets || (_welcomeExactServlets && entry.getPathSpec().getDeclaration().equals(welcome_in_context))))
welcome_servlet=welcome_in_context;
}

View File

@ -32,6 +32,8 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.PathMap.MappedEntry;
import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.server.Dispatcher;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
@ -71,7 +73,7 @@ public class Invoker extends HttpServlet
private ContextHandler _contextHandler;
private ServletHandler _servletHandler;
private Map.Entry<String, ServletHolder> _invokerEntry;
private MappedResource<ServletHolder> _invokerEntry;
private Map<String, String> _parameters;
private boolean _nonContextServlets;
private boolean _verbose;
@ -170,12 +172,12 @@ public class Invoker extends HttpServlet
// Check for existing mapping (avoid threaded race).
String path=URIUtil.addPaths(servlet_path,servlet);
Map.Entry<String, ServletHolder> entry = _servletHandler.getHolderEntry(path);
MappedResource<ServletHolder> entry = _servletHandler.getHolderEntry(path);
if (entry!=null && !entry.equals(_invokerEntry))
{
// Use the holder
holder=(ServletHolder)entry.getValue();
holder=(ServletHolder)entry.getResource();
}
else
{

View File

@ -49,7 +49,10 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.PathMap;
import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.http.pathmap.PathMappings;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.http.pathmap.ServletPathSpec;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.server.Request;
@ -111,7 +114,8 @@ public class ServletHandler extends ScopedHandler
private MultiMap<FilterMapping> _filterNameMappings;
private final Map<String,ServletHolder> _servletNameMap=new HashMap<>();
private PathMap<ServletHolder> _servletPathMap;
// private PathMap<ServletHolder> _servletPathMap;
private PathMappings<ServletHolder> _servletPathMap;
private ListenerHolder[] _listeners=new ListenerHolder[0];
@ -358,7 +362,7 @@ public class ServletHandler extends ScopedHandler
* @param pathInContext Path within _context.
* @return PathMap Entries pathspec to ServletHolder
*/
public PathMap.MappedEntry<ServletHolder> getHolderEntry(String pathInContext)
public MappedResource<ServletHolder> getHolderEntry(String pathInContext)
{
if (_servletPathMap==null)
return null;
@ -436,14 +440,14 @@ public class ServletHandler extends ScopedHandler
if (target.startsWith("/"))
{
// Look for the servlet by path
PathMap.MappedEntry<ServletHolder> entry=getHolderEntry(target);
MappedResource<ServletHolder> entry=getHolderEntry(target);
if (entry!=null)
{
servlet_holder=entry.getValue();
PathSpec pathSpec = entry.getPathSpec();
servlet_holder=entry.getResource();
String servlet_path_spec= entry.getKey();
String servlet_path=entry.getMapped()!=null?entry.getMapped():PathMap.pathMatch(servlet_path_spec,target);
String path_info=PathMap.pathInfo(servlet_path_spec,target);
String servlet_path=pathSpec.getPathMatch(target);
String path_info=pathSpec.getPathInfo(target);
if (DispatcherType.INCLUDE.equals(type))
{
@ -1307,7 +1311,7 @@ public class ServletHandler extends ScopedHandler
}
else
{
PathMap<ServletHolder> pm = new PathMap<>();
PathMappings<ServletHolder> pm = new PathMappings<>();
Map<String,ServletMapping> servletPathMappings = new HashMap<>();
//create a map of paths to set of ServletMappings that define that mapping
@ -1370,7 +1374,7 @@ public class ServletHandler extends ScopedHandler
if (LOG.isDebugEnabled()) LOG.debug("Chose path={} mapped to servlet={} from default={}", pathSpec, finalMapping.getServletName(), finalMapping.isDefault());
servletPathMappings.put(pathSpec, finalMapping);
pm.put(pathSpec,_servletNameMap.get(finalMapping.getServletName()));
pm.put(new ServletPathSpec(pathSpec),_servletNameMap.get(finalMapping.getServletName()));
}
_servletPathMap=pm;

View File

@ -207,15 +207,29 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
{
if (sh==this)
return 0;
if (sh._initOrder<_initOrder)
return 1;
if (sh._initOrder>_initOrder)
return -1;
int c=(_className!=null && sh._className!=null)?_className.compareTo(sh._className):0;
// consider _className, need to position properly when one is configured but not the other
int c;
if (_className==null && sh._className==null)
c=0;
else if (_className==null)
c=-1;
else if (sh._className==null)
c=1;
else
c=_className.compareTo(sh._className);
// if _initOrder and _className are the same, consider the _name
if (c==0)
c=_name.compareTo(sh._name);
return c;
return c;
}
/* ------------------------------------------------------------ */

View File

@ -0,0 +1,55 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.servlet;
import org.junit.Assert;
import org.junit.Test;
import java.util.Arrays;
import java.util.Collections;
public class ServletHolderTest {
@Test
public void testTransitiveCompareTo() throws Exception
{
// example of jsp-file referenced in web.xml
final ServletHolder one = new ServletHolder();
one.setInitOrder(-1);
one.setName("Login");
one.setClassName(null);
// example of pre-compiled jsp
final ServletHolder two = new ServletHolder();
two.setInitOrder(-1);
two.setName("org.my.package.jsp.WEB_002dINF.pages.precompiled_002dpage_jsp");
two.setClassName("org.my.package.jsp.WEB_002dINF.pages.precompiled_002dpage_jsp");
// example of servlet referenced in web.xml
final ServletHolder three = new ServletHolder();
three.setInitOrder(-1);
three.setName("Download");
three.setClassName("org.my.package.web.DownloadServlet");
// verify compareTo transitivity
Assert.assertTrue(one.compareTo(two) < 0);
Assert.assertTrue(two.compareTo(three) < 0);
Assert.assertTrue(one.compareTo(three) < 0);
}
}

View File

@ -84,6 +84,10 @@ public abstract class CompletableCallback implements Callback
}
break;
}
case FAILED:
{
return;
}
default:
{
throw new IllegalStateException(current.toString());
@ -110,6 +114,10 @@ public abstract class CompletableCallback implements Callback
}
break;
}
case FAILED:
{
return;
}
default:
{
throw new IllegalStateException(current.toString());

View File

@ -20,7 +20,7 @@ package org.eclipse.jetty.util;
import java.util.HashSet;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Predicate;
/** Utility class to maintain a set of inclusions and exclusions.
@ -36,35 +36,77 @@ public class IncludeExclude<ITEM>
{
private final Set<ITEM> _includes;
private final Set<ITEM> _excludes;
private final BiFunction<Set<ITEM>,ITEM, Boolean> _matcher;
private final Predicate<ITEM> _includePredicate;
private final Predicate<ITEM> _excludePredicate;
private static class SetContainsPredicate<ITEM> implements Predicate<ITEM>
{
private final Set<ITEM> set;
public SetContainsPredicate(Set<ITEM> set)
{
this.set = set;
}
@Override
public boolean test(ITEM item)
{
return set.contains(item);
}
}
/**
* Default constructor over {@link HashSet}
*/
public IncludeExclude()
{
this(HashSet.class,null);
this(HashSet.class);
}
/**
* Construct an IncludeExclude
* @param setClass The type of {@link Set} to using internally
* @param matcher A function to test if a passed ITEM is matched by the passed SET, or null to use {@link Set#contains(Object)}
* @param predicate A predicate function to test if a passed ITEM is matched by the passed SET}
*/
public <SET extends Set<ITEM>> IncludeExclude(Class<SET> setClass, BiFunction<SET,ITEM, Boolean> matcher)
public <SET extends Set<ITEM>> IncludeExclude(Class<SET> setClass)
{
try
{
_includes = setClass.newInstance();
_excludes = setClass.newInstance();
_matcher = (BiFunction<Set<ITEM>,ITEM, Boolean>)matcher;
if(_includes instanceof Predicate) {
_includePredicate = (Predicate<ITEM>)_includes;
} else {
_includePredicate = new SetContainsPredicate<>(_includes);
}
if(_excludes instanceof Predicate) {
_excludePredicate = (Predicate<ITEM>)_excludes;
} else {
_excludePredicate = new SetContainsPredicate<>(_excludes);
}
}
catch (InstantiationException | IllegalAccessException e)
{
throw new RuntimeException(e);
}
}
/**
* Construct an IncludeExclude
*
* @param includeSet the Set of items that represent the included space
* @param includePredicate the Predicate for included item testing (null for simple {@link Set#contains(Object)} test)
* @param excludeSet the Set of items that represent the excluded space
* @param excludePredicate the Predicate for excluded item testing (null for simple {@link Set#contains(Object)} test)
*/
public <SET extends Set<ITEM>> IncludeExclude(Set<ITEM> includeSet, Predicate<ITEM> includePredicate, Set<ITEM> excludeSet, Predicate<ITEM> excludePredicate)
{
_includes = includeSet;
_includePredicate = includePredicate;
_excludes = excludeSet;
_excludePredicate = excludePredicate;
}
public void include(ITEM element)
{
_includes.add(element);
@ -89,17 +131,11 @@ public class IncludeExclude<ITEM>
public boolean matches(ITEM e)
{
if (_matcher==null)
{
if (_includes.size()>0 && !_includes.contains(e))
return false;
return !_excludes.contains(e);
}
if (_includes.size()>0 && !_matcher.apply(_includes,e))
if (!_includes.isEmpty() && !_includePredicate.test(e))
return false;
return !_matcher.apply(_excludes,e);
return !_excludePredicate.test(e);
}
public int size()
{
return _includes.size()+_excludes.size();
@ -124,6 +160,6 @@ public class IncludeExclude<ITEM>
@Override
public String toString()
{
return String.format("%s@%x{i=%s,e=%s,m=%s}",this.getClass().getSimpleName(),hashCode(),_includes,_excludes,_matcher);
return String.format("%s@%x{i=%s,ip=%s,e=%s,ep=%s}",this.getClass().getSimpleName(),hashCode(),_includes,_includePredicate,_excludes,_excludePredicate);
}
}

View File

@ -24,6 +24,7 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.regex.Pattern;
@ -32,9 +33,8 @@ import java.util.regex.Pattern;
* <p>
* Provides the efficient {@link #matches(String)} method to check for a match against all the combined Regex's
*/
public class RegexSet extends AbstractSet<String>
public class RegexSet extends AbstractSet<String> implements Predicate<String>
{
public static final BiFunction<RegexSet,String,Boolean> MATCHER=(rs,p)->{return rs.matches(p);};
private final Set<String> _patterns=new HashSet<String>();
private final Set<String> _unmodifiable=Collections.unmodifiableSet(_patterns);
private Pattern _pattern;
@ -98,6 +98,12 @@ public class RegexSet extends AbstractSet<String>
builder.append(")$");
_pattern = Pattern.compile(builder.toString());
}
@Override
public boolean test(String s)
{
return _pattern!=null && _pattern.matcher(s).matches();
}
public boolean matches(String s)
{

View File

@ -20,7 +20,9 @@ package org.eclipse.jetty.util;
import org.junit.Test;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
public class IncludeExcludeTest
{
@ -29,8 +31,8 @@ public class IncludeExcludeTest
{
IncludeExclude<String> ie = new IncludeExclude<>();
assertEquals(0,ie.size());
assertEquals(true,ie.matches("foo"));
assertThat("Empty IncludeExclude", ie.size(), is(0));
assertThat("Matches 'foo'",ie.matches("foo"),is(true));
}
@Test
@ -40,7 +42,7 @@ public class IncludeExcludeTest
ie.include("foo");
ie.include("bar");
assertEquals(2,ie.size());
assertThat("IncludeExclude.size", ie.size(), is(2));
assertEquals(false,ie.matches(""));
assertEquals(true,ie.matches("foo"));
assertEquals(true,ie.matches("bar"));
@ -86,7 +88,7 @@ public class IncludeExcludeTest
@Test
public void testEmptyRegex()
{
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER);
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class);
assertEquals(0,ie.size());
assertEquals(true,ie.matches("foo"));
@ -95,7 +97,7 @@ public class IncludeExcludeTest
@Test
public void testIncludeRegex()
{
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER);
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class);
ie.include("f..");
ie.include("b((ar)|(oo))");
@ -112,7 +114,7 @@ public class IncludeExcludeTest
@Test
public void testExcludeRegex()
{
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER);
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class);
ie.exclude("f..");
ie.exclude("b((ar)|(oo))");
@ -130,7 +132,7 @@ public class IncludeExcludeTest
@Test
public void testIncludeExcludeRegex()
{
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class,RegexSet.MATCHER);
IncludeExclude<String> ie = new IncludeExclude<>(RegexSet.class);
ie.include(".*[aeiou].*");
ie.include("[AEIOU].*");
ie.exclude("f..");
@ -146,8 +148,5 @@ public class IncludeExcludeTest
assertEquals(true,ie.matches("foobar"));
assertEquals(true,ie.matches("Ant"));
}
}

View File

@ -20,7 +20,9 @@ package org.eclipse.jetty.websocket.jsr356.server;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.websocket.Extension;
import javax.websocket.Extension.Parameter;
@ -45,6 +47,7 @@ public class JsrCreator implements WebSocketCreator
{
public static final String PROP_REMOTE_ADDRESS = "javax.websocket.endpoint.remoteAddress";
public static final String PROP_LOCAL_ADDRESS = "javax.websocket.endpoint.localAddress";
public static final String PROP_LOCALES = "javax.websocket.upgrade.locales";
private static final Logger LOG = Log.getLogger(JsrCreator.class);
private final WebSocketContainerScope containerScope;
private final ServerEndpointMetadata metadata;
@ -74,8 +77,10 @@ public class JsrCreator implements WebSocketCreator
// This is being implemented as an optional set of userProperties so that
// it is not JSR api breaking. A few users on #jetty and a few from cometd
// have asked for access to this information.
config.getUserProperties().put(PROP_LOCAL_ADDRESS,req.getLocalSocketAddress());
config.getUserProperties().put(PROP_REMOTE_ADDRESS,req.getRemoteSocketAddress());
Map<String, Object> userProperties = config.getUserProperties();
userProperties.put(PROP_LOCAL_ADDRESS,req.getLocalSocketAddress());
userProperties.put(PROP_REMOTE_ADDRESS,req.getRemoteSocketAddress());
userProperties.put(PROP_LOCALES,Collections.list(req.getLocales()));
// Get Configurator from config object (not guaranteed to be unique per endpoint upgrade)
ServerEndpointConfig.Configurator configurator = config.getConfigurator();

View File

@ -28,13 +28,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.websocket.ClientEndpoint;
import javax.websocket.CloseReason;
@ -63,7 +59,7 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.common.test.LeakTrackingBufferPoolRule;
import org.eclipse.jetty.websocket.common.util.Hex;
import org.eclipse.jetty.websocket.common.util.Sha1Sum;
import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
import org.junit.AfterClass;
import org.junit.Assert;
@ -176,33 +172,12 @@ public class StreamTest
Assert.assertThat("Path should exist: " + file,file.exists(),is(true));
Assert.assertThat("Path should not be a directory:" + file,file.isDirectory(),is(false));
String expectedSha1 = loadExpectedSha1Sum(sha1File);
String actualSha1 = calculateSha1Sum(file);
String expectedSha1 = Sha1Sum.loadSha1(sha1File);
String actualSha1 = Sha1Sum.calculate(file);
Assert.assertThat("SHA1Sum of content: " + file,expectedSha1,equalToIgnoringCase(actualSha1));
}
private String calculateSha1Sum(File file) throws IOException, NoSuchAlgorithmException
{
MessageDigest digest = MessageDigest.getInstance("SHA1");
try (FileInputStream fis = new FileInputStream(file);
NoOpOutputStream noop = new NoOpOutputStream();
DigestOutputStream digester = new DigestOutputStream(noop,digest))
{
IO.copy(fis,digester);
return Hex.asHex(digest.digest());
}
}
private String loadExpectedSha1Sum(File sha1File) throws IOException
{
String contents = IO.readToString(sha1File);
Pattern pat = Pattern.compile("^[0-9A-Fa-f]*");
Matcher mat = pat.matcher(contents);
Assert.assertTrue("Should have found HEX code in SHA1 file: " + sha1File,mat.find());
return mat.group();
}
@ClientEndpoint
public static class ClientSocket
{
@ -317,32 +292,4 @@ public class StreamTest
t.printStackTrace(System.err);
}
}
private static class NoOpOutputStream extends OutputStream
{
@Override
public void write(byte[] b) throws IOException
{
}
@Override
public void write(byte[] b, int off, int len) throws IOException
{
}
@Override
public void flush() throws IOException
{
}
@Override
public void close() throws IOException
{
}
@Override
public void write(int b) throws IOException
{
}
}
}

View File

@ -154,30 +154,33 @@ public abstract class CompressExtension extends AbstractExtension
return;
}
byte[] output = new byte[DECOMPRESS_BUF_SIZE];
if (inflater.needsInput() && !supplyInput(inflater,buf))
while(buf.hasRemaining() && inflater.needsInput())
{
LOG.debug("Needed input, but no buffer could supply input");
return;
}
int read = 0;
while ((read = inflater.inflate(output)) >= 0)
{
if (read == 0)
if (!supplyInput(inflater,buf))
{
LOG.debug("Decompress: read 0 {}",toDetail(inflater));
break;
LOG.debug("Needed input, but no buffer could supply input");
return;
}
else
int read = 0;
while ((read = inflater.inflate(output)) >= 0)
{
// do something with output
if (LOG.isDebugEnabled())
if (read == 0)
{
LOG.debug("Decompressed {} bytes: {}",read,toDetail(inflater));
LOG.debug("Decompress: read 0 {}",toDetail(inflater));
break;
}
else
{
// do something with output
if (LOG.isDebugEnabled())
{
LOG.debug("Decompressed {} bytes: {}",read,toDetail(inflater));
}
accumulator.copyChunk(output,0,read);
}
accumulator.copyChunk(output,0,read);
}
}

View File

@ -0,0 +1,110 @@
//
// ========================================================================
// Copyright (c) 1995-2015 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.websocket.common.util;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jetty.toolchain.test.IO;
import org.junit.Assert;
/**
* Calculate the sha1sum for various content
*/
public class Sha1Sum
{
private static class NoOpOutputStream extends OutputStream
{
@Override
public void write(byte[] b) throws IOException
{
}
@Override
public void write(byte[] b, int off, int len) throws IOException
{
}
@Override
public void flush() throws IOException
{
}
@Override
public void close() throws IOException
{
}
@Override
public void write(int b) throws IOException
{
}
}
public static String calculate(File file) throws NoSuchAlgorithmException, IOException
{
return calculate(file.toPath());
}
public static String calculate(Path path) throws NoSuchAlgorithmException, IOException
{
MessageDigest digest = MessageDigest.getInstance("SHA1");
try (InputStream in = Files.newInputStream(path,StandardOpenOption.READ);
NoOpOutputStream noop = new NoOpOutputStream();
DigestOutputStream digester = new DigestOutputStream(noop,digest))
{
IO.copy(in,digester);
return Hex.asHex(digest.digest());
}
}
public static String calculate(byte[] buf) throws NoSuchAlgorithmException
{
MessageDigest digest = MessageDigest.getInstance("SHA1");
digest.update(buf);
return Hex.asHex(digest.digest());
}
public static String calculate(byte[] buf, int offset, int len) throws NoSuchAlgorithmException
{
MessageDigest digest = MessageDigest.getInstance("SHA1");
digest.update(buf,offset,len);
return Hex.asHex(digest.digest());
}
public static String loadSha1(File sha1File) throws IOException
{
String contents = IO.readToString(sha1File);
Pattern pat = Pattern.compile("^[0-9A-Fa-f]*");
Matcher mat = pat.matcher(contents);
Assert.assertTrue("Should have found HEX code in SHA1 file: " + sha1File,mat.find());
return mat.group();
}
}

View File

@ -20,34 +20,90 @@ package org.eclipse.jetty.websocket.server;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.toolchain.test.EventQueue;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
import org.eclipse.jetty.websocket.common.frames.TextFrame;
import org.eclipse.jetty.websocket.common.test.BlockheadClient;
import org.eclipse.jetty.websocket.common.test.HttpResponse;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.common.test.LeakTrackingBufferPoolRule;
import org.eclipse.jetty.websocket.common.util.Sha1Sum;
import org.eclipse.jetty.websocket.server.helper.CaptureSocket;
import org.eclipse.jetty.websocket.server.helper.EchoServlet;
import org.junit.AfterClass;
import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class PerMessageDeflateExtensionTest
{
private static SimpleServletServer server;
@BeforeClass
public static void startServer() throws Exception
private static enum TestCaseMessageSize
{
server = new SimpleServletServer(new EchoServlet());
server.start();
TINY(10),
SMALL(1024),
MEDIUM(10*1024),
LARGE(100*1024),
HUGE(1024*1024);
private int size;
private TestCaseMessageSize(int size)
{
this.size = size;
}
}
@Parameters(name = "{0} ({3}) (Input Buffer Size: {4} bytes)")
public static List<Object[]> modes()
{
List<Object[]> modes = new ArrayList<>();
for(TestCaseMessageSize size: TestCaseMessageSize.values())
{
modes.add(new Object[] { "Normal HTTP/WS", false, "ws", size, -1 });
modes.add(new Object[] { "Encrypted HTTPS/WSS", true, "wss", size, -1 });
int altInputBufSize = 15*1024;
modes.add(new Object[] { "Normal HTTP/WS", false, "ws", size, altInputBufSize });
modes.add(new Object[] { "Encrypted HTTPS/WSS", true, "wss", size, altInputBufSize });
}
return modes;
}
@AfterClass
public static void stopServer()
@Rule
public LeakTrackingBufferPoolRule bufferPool = new LeakTrackingBufferPoolRule("Test");
private SimpleServletServer server;
private String scheme;
private int msgSize;
private int inputBufferSize;
public PerMessageDeflateExtensionTest(String mode, boolean sslMode, String scheme, TestCaseMessageSize msgSize, int bufferSize) throws Exception
{
server = new SimpleServletServer(new EchoServlet());
server.enableSsl(sslMode);
server.start();
this.scheme = scheme;
this.msgSize = msgSize.size;
this.inputBufferSize = bufferSize;
}
@After
public void stopServer()
{
server.stop();
}
@ -62,42 +118,84 @@ public class PerMessageDeflateExtensionTest
Assume.assumeTrue("Server has permessage-deflate registered",
server.getWebSocketServletFactory().getExtensionFactory().isAvailable("permessage-deflate"));
BlockheadClient client = new BlockheadClient(server.getServerUri());
client.clearExtensions();
client.addExtensions("permessage-deflate");
client.setProtocols("echo");
Assert.assertThat("server scheme",server.getServerUri().getScheme(),is(scheme));
int binBufferSize = (int) (msgSize * 1.5);
WebSocketPolicy serverPolicy = server.getWebSocketServletFactory().getPolicy();
// Ensure binBufferSize is sane (not smaller then other buffers)
binBufferSize = Math.max(binBufferSize,serverPolicy.getMaxBinaryMessageSize());
binBufferSize = Math.max(binBufferSize,serverPolicy.getMaxBinaryMessageBufferSize());
binBufferSize = Math.max(binBufferSize,this.inputBufferSize);
serverPolicy.setMaxBinaryMessageSize(binBufferSize);
serverPolicy.setMaxBinaryMessageBufferSize(binBufferSize);
WebSocketClient client = new WebSocketClient(server.getSslContextFactory(),null,bufferPool);
WebSocketPolicy clientPolicy = client.getPolicy();
clientPolicy.setMaxBinaryMessageSize(binBufferSize);
clientPolicy.setMaxBinaryMessageBufferSize(binBufferSize);
if (inputBufferSize > 0)
{
clientPolicy.setInputBufferSize(inputBufferSize);
}
try
{
client.start();
// Make sure the read times out if there are problems with the implementation
client.setTimeout(1,TimeUnit.SECONDS);
client.connect();
client.sendStandardRequest();
HttpResponse resp = client.expectUpgradeResponse();
client.setMaxIdleTimeout(TimeUnit.SECONDS.toMillis(1));
Assert.assertThat("Response",resp.getExtensionsHeader(),containsString("permessage-deflate"));
CaptureSocket clientSocket = new CaptureSocket();
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.addExtensions("permessage-deflate");
request.setSubProtocols("echo");
String msg = "Hello";
Future<Session> fut = client.connect(clientSocket,server.getServerUri(),request);
// Wait for connect
Session session = fut.get(3,TimeUnit.SECONDS);
assertThat("Response.extensions",getNegotiatedExtensionList(session),containsString("permessage-deflate"));
// Create message
byte msg[] = new byte[msgSize];
Random rand = new Random();
rand.setSeed(8080);
rand.nextBytes(msg);
// Calculate sha1
String sha1 = Sha1Sum.calculate(msg);
// Client sends first message
client.write(new TextFrame().setPayload(msg));
session.getRemote().sendBytes(ByteBuffer.wrap(msg));
EventQueue<WebSocketFrame> frames = client.readFrames(1,1000,TimeUnit.MILLISECONDS);
WebSocketFrame frame = frames.poll();
Assert.assertThat("TEXT.payload",frame.getPayloadAsUTF8(),is(msg.toString()));
// Client sends second message
client.clearCaptured();
msg = "There";
client.write(new TextFrame().setPayload(msg));
frames = client.readFrames(1,1,TimeUnit.SECONDS);
frame = frames.poll();
Assert.assertThat("TEXT.payload",frame.getPayloadAsUTF8(),is(msg.toString()));
clientSocket.messages.awaitEventCount(1,1,TimeUnit.SECONDS);
String echoMsg = clientSocket.messages.poll();
Assert.assertThat("Echo'd Message",echoMsg,is("binary[sha1="+sha1+"]"));
}
finally
{
client.close();
client.stop();
}
}
private String getNegotiatedExtensionList(Session session)
{
StringBuilder actual = new StringBuilder();
actual.append('[');
boolean delim = false;
for (ExtensionConfig ext : session.getUpgradeResponse().getExtensions())
{
if (delim)
actual.append(", ");
actual.append(ext.getName());
delim = true;
}
actual.append(']');
return actual.toString();
}
}

View File

@ -18,12 +18,14 @@
package org.eclipse.jetty.websocket.server.helper;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.toolchain.test.EventQueue;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.common.util.Sha1Sum;
public class CaptureSocket extends WebSocketAdapter
{
@ -58,4 +60,18 @@ public class CaptureSocket extends WebSocketAdapter
// System.out.printf("Received Message \"%s\" [size %d]%n", message, message.length());
messages.add(message);
}
@Override
public void onWebSocketBinary(byte[] payload, int offset, int len)
{
try
{
messages.add("binary[sha1="+Sha1Sum.calculate(payload,offset,len)+"]");
}
catch (NoSuchAlgorithmException e)
{
messages.add("ERROR: Unable to caclulate Binary SHA1: " + e.getMessage());
e.printStackTrace();
}
}
}

View File

@ -27,6 +27,8 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.junit.Assert;
@ -93,4 +95,28 @@ public class HttpClientIdleTimeoutTest extends AbstractTest
Assert.assertTrue(latch.await(2 * idleTimeout, TimeUnit.MILLISECONDS));
}
@Test
public void testServerIdleTimeout() throws Exception
{
start(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
}
});
connector.setIdleTimeout(idleTimeout);
ContentResponse response1 = client.newRequest(newURI()).send();
Assert.assertEquals(HttpStatus.OK_200, response1.getStatus());
// Let the server idle timeout.
Thread.sleep(2 * idleTimeout);
// Make sure we can make another request successfully.
ContentResponse response2 = client.newRequest(newURI()).send();
Assert.assertEquals(HttpStatus.OK_200, response2.getStatus());
}
}

View File

@ -28,6 +28,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.IntStream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@ -200,7 +201,20 @@ public class HttpClientLoadTest extends AbstractTest
assertThat("Connection Leaks", connectionLeaks.get(), Matchers.is(0L));
}
private void run(Random random, int iterations) throws InterruptedException
@Test
public void testConcurrent() throws Exception
{
start(new LoadHandler());
Random random = new Random();
int runs = 1;
int iterations = 256;
IntStream.range(0, 16).parallel().forEach(i ->
IntStream.range(0, runs).forEach(j ->
run(random, iterations)));
}
private void run(Random random, int iterations)
{
CountDownLatch latch = new CountDownLatch(iterations);
List<String> failures = new ArrayList<>();
@ -222,7 +236,7 @@ public class HttpClientLoadTest extends AbstractTest
test(random, latch, failures);
// test("http", "localhost", "GET", false, false, 64 * 1024, false, latch, failures);
}
Assert.assertTrue(latch.await(iterations, TimeUnit.SECONDS));
Assert.assertTrue(await(latch, iterations, TimeUnit.SECONDS));
long end = System.nanoTime();
task.cancel();
long elapsed = TimeUnit.NANOSECONDS.toMillis(end - begin);
@ -234,7 +248,7 @@ public class HttpClientLoadTest extends AbstractTest
Assert.assertTrue(failures.toString(), failures.isEmpty());
}
private void test(Random random, final CountDownLatch latch, final List<String> failures) throws InterruptedException
private void test(Random random, final CountDownLatch latch, final List<String> failures)
{
// Choose a random destination
String host = random.nextBoolean() ? "localhost" : "127.0.0.1";
@ -257,7 +271,7 @@ public class HttpClientLoadTest extends AbstractTest
test(ssl ? "https" : "http", host, method.asString(), clientClose, serverClose, contentLength, true, latch, failures);
}
private void test(String scheme, String host, String method, boolean clientClose, boolean serverClose, int contentLength, final boolean checkContentLength, final CountDownLatch latch, final List<String> failures) throws InterruptedException
private void test(String scheme, String host, String method, boolean clientClose, boolean serverClose, int contentLength, final boolean checkContentLength, final CountDownLatch latch, final List<String> failures)
{
Request request = client.newRequest(host, connector.getLocalPort())
.scheme(scheme)
@ -318,7 +332,19 @@ public class HttpClientLoadTest extends AbstractTest
latch.countDown();
}
});
requestLatch.await(5, TimeUnit.SECONDS);
await(requestLatch, 5, TimeUnit.SECONDS);
}
private boolean await(CountDownLatch latch, long time, TimeUnit unit)
{
try
{
return latch.await(time, unit);
}
catch (InterruptedException x)
{
throw new RuntimeException(x);
}
}
private class LoadHandler extends AbstractHandler