diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java index ae1f1938601..bd01c93dc0b 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.jmx.MBeanContainer; import org.eclipse.jetty.security.HashLoginService; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.LowResourceMonitor; import org.eclipse.jetty.server.NCSARequestLog; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -36,6 +37,7 @@ import org.eclipse.jetty.server.handler.RequestLogHandler; import org.eclipse.jetty.server.handler.StatisticsHandler; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.TimerScheduler; public class LikeJettyXml { @@ -53,6 +55,8 @@ public class LikeJettyXml server.setDumpAfterStart(false); server.setDumpBeforeStop(false); + server.addBean(new TimerScheduler()); + // Setup JMX MBeanContainer mbContainer=new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); server.addBean(mbContainer); @@ -120,6 +124,12 @@ public class LikeJettyXml requestLogHandler.setRequestLog(requestLog); server.setStopAtShutdown(true); + + LowResourceMonitor lowResourcesMonitor=new LowResourceMonitor(server); + lowResourcesMonitor.setLowResourcesIdleTimeout(1000); + lowResourcesMonitor.setMaxConnections(2); + lowResourcesMonitor.setPeriod(1200); + server.addBean(lowResourcesMonitor); server.start(); server.join(); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/ContinueProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/ContinueProtocolHandler.java index d1d67f54610..e7995c1958f 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/ContinueProtocolHandler.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/ContinueProtocolHandler.java @@ -76,17 +76,19 @@ public class ContinueProtocolHandler implements ProtocolHandler case 100: { // All good, continue + conversation.setResponseListener(null); exchange.resetResponse(true); - conversation.setResponseListeners(listeners); exchange.proceed(true); break; } default: { - // Server either does not support 100 Continue, or it does and wants to refuse the request content + // Server either does not support 100 Continue, + // or it does and wants to refuse the request content, + // or we got some other HTTP status code like a redirect. + conversation.setResponseListener(null); HttpContentResponse contentResponse = new HttpContentResponse(response, getContent(), getEncoding()); notifier.forwardSuccess(listeners, contentResponse); - conversation.setResponseListeners(listeners); exchange.proceed(false); break; } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index c96f0599343..6a61eb777d8 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -64,6 +64,7 @@ import org.eclipse.jetty.io.ssl.SslConnection; import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.Jetty; import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.SocketAddressResolver; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -122,6 +123,7 @@ public class HttpClient extends ContainerLifeCycle private volatile Executor executor; private volatile ByteBufferPool byteBufferPool; private volatile Scheduler scheduler; + private volatile SocketAddressResolver resolver; private volatile SelectorManager selectorManager; private volatile HttpField agentField = new HttpField(HttpHeader.USER_AGENT, "Jetty/" + Jetty.VERSION); private volatile boolean followRedirects = true; @@ -132,6 +134,7 @@ public class HttpClient extends ContainerLifeCycle private volatile int maxRedirects = 8; private volatile SocketAddress bindAddress; private volatile long connectTimeout = 15000; + private volatile long addressResolutionTimeout = 15000; private volatile long idleTimeout; private volatile boolean tcpNoDelay = true; private volatile boolean dispatchIO = true; @@ -198,6 +201,8 @@ public class HttpClient extends ContainerLifeCycle scheduler = new ScheduledExecutorScheduler(name + "-scheduler", false); addBean(scheduler); + resolver = new SocketAddressResolver(executor, scheduler, getAddressResolutionTimeout()); + selectorManager = newSelectorManager(); selectorManager.setConnectTimeout(getConnectTimeout()); addBean(selectorManager); @@ -439,10 +444,10 @@ public class HttpClient extends ContainerLifeCycle */ public Destination getDestination(String scheme, String host, int port) { - return provideDestination(scheme, host, port); + return destinationFor(scheme, host, port); } - protected HttpDestination provideDestination(String scheme, String host, int port) + protected HttpDestination destinationFor(String scheme, String host, int port) { port = normalizePort(scheme, port); @@ -480,34 +485,48 @@ public class HttpClient extends ContainerLifeCycle if (!Arrays.asList("http", "https").contains(scheme)) throw new IllegalArgumentException("Invalid protocol " + scheme); - HttpDestination destination = provideDestination(scheme, request.getHost(), request.getPort()); + HttpDestination destination = destinationFor(scheme, request.getHost(), request.getPort()); destination.send(request, listeners); } - protected void newConnection(HttpDestination destination, Promise promise) + protected void newConnection(final HttpDestination destination, final Promise promise) { - SocketChannel channel = null; - try + Destination.Address address = destination.getConnectAddress(); + resolver.resolve(address.getHost(), address.getPort(), new Promise() { - channel = SocketChannel.open(); - SocketAddress bindAddress = getBindAddress(); - if (bindAddress != null) - channel.bind(bindAddress); - configure(channel); - channel.configureBlocking(false); - channel.connect(destination.getConnectAddress()); + @Override + public void succeeded(SocketAddress socketAddress) + { + SocketChannel channel = null; + try + { + channel = SocketChannel.open(); + SocketAddress bindAddress = getBindAddress(); + if (bindAddress != null) + channel.bind(bindAddress); + configure(channel); + channel.configureBlocking(false); + channel.connect(socketAddress); - Future result = new ConnectionCallback(destination, promise); - selectorManager.connect(channel, result); - } - // Must catch all exceptions, since some like - // UnresolvedAddressException are not IOExceptions. - catch (Exception x) - { - if (channel != null) - close(channel); - promise.failed(x); - } + Future futureConnection = new ConnectionCallback(destination, promise); + selectorManager.connect(channel, futureConnection); + } + // Must catch all exceptions, since some like + // UnresolvedAddressException are not IOExceptions. + catch (Throwable x) + { + if (channel != null) + close(channel); + promise.failed(x); + } + } + + @Override + public void failed(Throwable x) + { + promise.failed(x); + } + }); } protected void configure(SocketChannel channel) throws SocketException @@ -544,7 +563,7 @@ public class HttpClient extends ContainerLifeCycle protected void removeConversation(HttpConversation conversation) { - conversations.remove(conversation.id()); + conversations.remove(conversation.getID()); LOG.debug("{} removed", conversation); } @@ -599,6 +618,22 @@ public class HttpClient extends ContainerLifeCycle this.connectTimeout = connectTimeout; } + /** + * @return the timeout, in milliseconds, for the DNS resolution of host addresses + */ + public long getAddressResolutionTimeout() + { + return addressResolutionTimeout; + } + + /** + * @param addressResolutionTimeout the timeout, in milliseconds, for the DNS resolution of host addresses + */ + public void setAddressResolutionTimeout(long addressResolutionTimeout) + { + this.addressResolutionTimeout = addressResolutionTimeout; + } + /** * @return the max time a connection can be idle (that is, without traffic of bytes in either direction) */ diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java index e291532df4c..64f1b22dfdd 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java @@ -117,11 +117,15 @@ public class HttpConnection extends AbstractConnection implements Connection } if (listener != null) listeners.add(listener); - send(request, listeners); + + HttpConversation conversation = client.getConversation(request.getConversationID(), true); + HttpExchange exchange = new HttpExchange(conversation, getDestination(), request, listeners); + send(exchange); } - public void send(Request request, List listeners) + public void send(HttpExchange exchange) { + Request request = exchange.getRequest(); normalizeRequest(request); // Save the old idle timeout to restore it @@ -129,10 +133,8 @@ public class HttpConnection extends AbstractConnection implements Connection idleTimeout = endPoint.getIdleTimeout(); endPoint.setIdleTimeout(request.getIdleTimeout()); - HttpConversation conversation = client.getConversation(request.getConversationID(), true); - HttpExchange exchange = new HttpExchange(conversation, this, request, listeners); - setExchange(exchange); - conversation.getExchanges().offer(exchange); + // Associate the exchange to the connection + associate(exchange); sender.send(exchange); } @@ -279,12 +281,21 @@ public class HttpConnection extends AbstractConnection implements Connection return exchange.get(); } - protected void setExchange(HttpExchange exchange) + protected void associate(HttpExchange exchange) { if (!this.exchange.compareAndSet(null, exchange)) throw new UnsupportedOperationException("Pipelined requests not supported"); - else - LOG.debug("{} associated to {}", exchange, this); + exchange.setConnection(this); + LOG.debug("{} associated to {}", exchange, this); + } + + protected HttpExchange disassociate() + { + HttpExchange exchange = this.exchange.getAndSet(null); + if (exchange != null) + exchange.setConnection(null); + LOG.debug("{} disassociated from {}", exchange, this); + return exchange; } @Override @@ -293,7 +304,7 @@ public class HttpConnection extends AbstractConnection implements Connection HttpExchange exchange = getExchange(); if (exchange != null) { - exchange.receive(); + receive(); } else { @@ -310,7 +321,7 @@ public class HttpConnection extends AbstractConnection implements Connection public void complete(HttpExchange exchange, boolean success) { - HttpExchange existing = this.exchange.getAndSet(null); + HttpExchange existing = disassociate(); if (existing == exchange) { exchange.awaitTermination(); @@ -356,13 +367,13 @@ public class HttpConnection extends AbstractConnection implements Connection } } - public boolean abort(HttpExchange exchange, Throwable cause) + public boolean abort(Throwable cause) { // We want the return value to be that of the response // because if the response has already successfully // arrived then we failed to abort the exchange - sender.abort(exchange, cause); - return receiver.abort(exchange, cause); + sender.abort(cause); + return receiver.abort(cause); } public void proceed(boolean proceed) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConversation.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConversation.java index 233f291fdb7..cca98c54c73 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConversation.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConversation.java @@ -18,24 +18,21 @@ package org.eclipse.jetty.client; -import java.util.Collections; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Deque; -import java.util.Enumeration; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import org.eclipse.jetty.client.api.Response; -import org.eclipse.jetty.util.Attributes; +import org.eclipse.jetty.util.AttributesMap; -public class HttpConversation implements Attributes +public class HttpConversation extends AttributesMap { - private final Map attributes = new ConcurrentHashMap<>(); private final Deque exchanges = new ConcurrentLinkedDeque<>(); private final HttpClient client; private final long id; - private volatile List listeners; + private volatile Response.ResponseListener listener; public HttpConversation(HttpClient client, long id) { @@ -43,7 +40,7 @@ public class HttpConversation implements Attributes this.id = id; } - public long id() + public long getID() { return id; } @@ -53,55 +50,112 @@ public class HttpConversation implements Attributes return exchanges; } + /** + * Returns the list of response listeners that needs to be notified of response events. + * This list changes as the conversation proceeds, as follows: + *
    + *
  1. + * request R1 send => conversation.setResponseListener(null) + *
      + *
    • exchanges in conversation: E1
    • + *
    • listeners to be notified: E1.listeners
    • + *
    + *
  2. + *
  3. + * response R1 arrived, 401 => conversation.setResponseListener(AuthenticationProtocolHandler.listener) + *
      + *
    • exchanges in conversation: E1
    • + *
    • listeners to be notified: AuthenticationProtocolHandler.listener
    • + *
    + *
  4. + *
  5. + * request R2 send => conversation.setResponseListener(null) + *
      + *
    • exchanges in conversation: E1 + E2
    • + *
    • listeners to be notified: E2.listeners + E1.listeners
    • + *
    + *
  6. + *
  7. + * response R2 arrived, 302 => conversation.setResponseListener(RedirectProtocolHandler.listener) + *
      + *
    • exchanges in conversation: E1 + E2
    • + *
    • listeners to be notified: E2.listeners + RedirectProtocolHandler.listener
    • + *
    + *
  8. + *
  9. + * request R3 send => conversation.setResponseListener(null) + *
      + *
    • exchanges in conversation: E1 + E2 + E3
    • + *
    • listeners to be notified: E3.listeners + E1.listeners
    • + *
    + *
  10. + *
  11. + * response R3 arrived, 200 => conversation.setResponseListener(null) + *
      + *
    • exchanges in conversation: E1 + E2 + E3
    • + *
    • listeners to be notified: E3.listeners + E1.listeners
    • + *
    + *
  12. + *
+ * Basically the override conversation listener replaces the first exchange response listener, + * and we also notify the last exchange response listeners (if it's not also the first). + * + * This scheme allows for protocol handlers to not worry about other protocol handlers, or to worry + * too much about notifying the first exchange response listeners, but still allowing a protocol + * handler to perform completion activities while another protocol handler performs new ones (as an + * example, the {@link AuthenticationProtocolHandler} stores the successful authentication credentials + * while the {@link RedirectProtocolHandler} performs a redirect). + * + * @return the list of response listeners that needs to be notified of response events + */ public List getResponseListeners() { - return listeners; + HttpExchange firstExchange = exchanges.peekFirst(); + HttpExchange lastExchange = exchanges.peekLast(); + if (firstExchange == lastExchange) + { + if (listener != null) + return Arrays.asList(listener); + else + return firstExchange.getResponseListeners(); + } + else + { + // Order is important, we want to notify the last exchange first + List result = new ArrayList<>(lastExchange.getResponseListeners()); + if (listener != null) + result.add(listener); + else + result.addAll(firstExchange.getResponseListeners()); + return result; + } } - public void setResponseListeners(List listeners) + /** + * Sets an override response listener that must be notified instead of the first exchange response listeners. + * This works in conjunction with {@link #getResponseListeners()}, returning the appropriate response + * listeners that needs to be notified of response events. + * + * @param listener the override response listener + */ + public void setResponseListener(Response.ResponseListener listener) { - this.listeners = listeners; + this.listener = listener; } public void complete() { - client.removeConversation(this); - } - - @Override - public Object getAttribute(String name) - { - return attributes.get(name); - } - - @Override - public void setAttribute(String name, Object attribute) - { - attributes.put(name, attribute); - } - - @Override - public void removeAttribute(String name) - { - attributes.remove(name); - } - - @Override - public Enumeration getAttributeNames() - { - return Collections.enumeration(attributes.keySet()); - } - - @Override - public void clearAttributes() - { - attributes.clear(); + // The conversation is really terminated only + // when there is no conversation listener that + // may have continued the conversation. + if (listener == null) + client.removeConversation(this); } public boolean abort(Throwable cause) { HttpExchange exchange = exchanges.peekLast(); - return exchange != null && exchange.abort(cause); + return exchange.abort(cause); } @Override diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java index 4463d519bdf..af4b8209000 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java @@ -19,13 +19,11 @@ package org.eclipse.jetty.client; import java.io.IOException; -import java.net.InetSocketAddress; import java.nio.channels.AsynchronousCloseException; import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -41,7 +39,6 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.util.BlockingArrayQueue; -import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.component.Dumpable; @@ -56,13 +53,13 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable private final HttpClient client; private final String scheme; private final String host; - private final InetSocketAddress address; - private final Queue requests; + private final Address address; + private final Queue exchanges; private final BlockingQueue idleConnections; private final BlockingQueue activeConnections; private final RequestNotifier requestNotifier; private final ResponseNotifier responseNotifier; - private final InetSocketAddress proxyAddress; + private final Address proxyAddress; private final HttpField hostField; public HttpDestination(HttpClient client, String scheme, String host, int port) @@ -70,11 +67,11 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable this.client = client; this.scheme = scheme; this.host = host; - this.address = new InetSocketAddress(host, port); + this.address = new Address(host, port); int maxRequestsQueued = client.getMaxRequestsQueuedPerDestination(); int capacity = Math.min(32, maxRequestsQueued); - this.requests = new BlockingArrayQueue<>(capacity, capacity, maxRequestsQueued); + this.exchanges = new BlockingArrayQueue<>(capacity, capacity, maxRequestsQueued); int maxConnections = client.getMaxConnectionsPerDestination(); capacity = Math.min(8, maxConnections); @@ -86,7 +83,7 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable ProxyConfiguration proxyConfig = client.getProxyConfiguration(); proxyAddress = proxyConfig != null && proxyConfig.matches(host, port) ? - new InetSocketAddress(proxyConfig.getHost(), proxyConfig.getPort()) : null; + new Address(proxyConfig.getHost(), proxyConfig.getPort()) : null; hostField = new HttpField(HttpHeader.HOST, host + ":" + port); } @@ -101,6 +98,16 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable return activeConnections; } + public RequestNotifier getRequestNotifier() + { + return requestNotifier; + } + + public ResponseNotifier getResponseNotifier() + { + return responseNotifier; + } + @Override public String getScheme() { @@ -121,7 +128,7 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable return address.getPort(); } - public InetSocketAddress getConnectAddress() + public Address getConnectAddress() { return isProxied() ? proxyAddress : address; } @@ -146,12 +153,14 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable if (port >= 0 && getPort() != port) throw new IllegalArgumentException("Invalid request port " + port + " for destination " + this); - RequestContext requestContext = new RequestContext(request, listeners); + HttpConversation conversation = client.getConversation(request.getConversationID(), true); + HttpExchange exchange = new HttpExchange(conversation, this, request, listeners); + if (client.isRunning()) { - if (requests.offer(requestContext)) + if (exchanges.offer(exchange)) { - if (!client.isRunning() && requests.remove(requestContext)) + if (!client.isRunning() && exchanges.remove(exchange)) { throw new RejectedExecutionException(client + " is stopping"); } @@ -175,14 +184,12 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable } } - public Future newConnection() + public void newConnection(Promise promise) { - FuturePromise result = new FuturePromise<>(); - newConnection(new ProxyPromise(result)); - return result; + createConnection(new ProxyPromise(promise)); } - protected void newConnection(Promise promise) + protected void createConnection(Promise promise) { client.newConnection(this, promise); } @@ -227,7 +234,7 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable @Override public void run() { - drain(x); + abort(x); } }); } @@ -236,7 +243,7 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable // Create a new connection, and pass a ProxyPromise to establish a proxy tunnel, if needed. // Differently from the case where the connection is created explicitly by applications, here // we need to do a bit more logging and keep track of the connection count in case of failures. - newConnection(new ProxyPromise(promise) + createConnection(new ProxyPromise(promise) { @Override public void succeeded(Connection connection) @@ -260,18 +267,11 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable } } - private void drain(Throwable x) + private void abort(Throwable cause) { - RequestContext requestContext; - while ((requestContext = requests.poll()) != null) - { - Request request = requestContext.request; - requestNotifier.notifyFailure(request, x); - List listeners = requestContext.listeners; - HttpResponse response = new HttpResponse(request, listeners); - responseNotifier.notifyFailure(listeners, response, x); - responseNotifier.notifyComplete(listeners, new Result(request, x, response, x)); - } + HttpExchange exchange; + while ((exchange = exchanges.poll()) != null) + abort(exchange, cause); } /** @@ -282,14 +282,15 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable *

If a request is waiting to be executed, it will be dequeued and executed by the new connection.

* * @param connection the new connection + * @param dispatch whether to dispatch the processing to another thread */ protected void process(Connection connection, boolean dispatch) { // Ugly cast, but lack of generic reification forces it final HttpConnection httpConnection = (HttpConnection)connection; - RequestContext requestContext = requests.poll(); - if (requestContext == null) + final HttpExchange exchange = exchanges.poll(); + if (exchange == null) { LOG.debug("{} idle", httpConnection); if (!idleConnections.offer(httpConnection)) @@ -306,13 +307,12 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable } else { - final Request request = requestContext.request; - final List listeners = requestContext.listeners; + final Request request = exchange.getRequest(); Throwable cause = request.getAbortCause(); if (cause != null) { - abort(request, listeners, cause); - LOG.debug("Aborted {} before processing", request); + abort(exchange, cause); + LOG.debug("Aborted before processing {}: {}", exchange, cause); } else { @@ -328,13 +328,13 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable @Override public void run() { - httpConnection.send(request, listeners); + httpConnection.send(exchange); } }); } else { - httpConnection.send(request, listeners); + httpConnection.send(exchange); } } } @@ -372,7 +372,7 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable // We need to execute queued requests even if this connection failed. // We may create a connection that is not needed, but it will eventually // idle timeout, so no worries - if (!requests.isEmpty()) + if (!exchanges.isEmpty()) { connection = acquire(); if (connection != null) @@ -391,36 +391,26 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable connection.close(); activeConnections.clear(); - drain(new AsynchronousCloseException()); + abort(new AsynchronousCloseException()); connectionCount.set(0); LOG.debug("Closed {}", this); } - public boolean abort(Request request, Throwable cause) + public boolean remove(HttpExchange exchange) { - for (RequestContext requestContext : requests) - { - if (requestContext.request == request) - { - if (requests.remove(requestContext)) - { - // We were able to remove the pair, so it won't be processed - abort(request, requestContext.listeners, cause); - LOG.debug("Aborted {} while queued", request); - return true; - } - } - } - return false; + return exchanges.remove(exchange); } - private void abort(Request request, List listeners, Throwable cause) + protected void abort(HttpExchange exchange, Throwable cause) { - HttpResponse response = new HttpResponse(request, listeners); - responseNotifier.notifyFailure(listeners, response, cause); - responseNotifier.notifyComplete(listeners, new Result(request, cause, response, cause)); + Request request = exchange.getRequest(); + HttpResponse response = exchange.getResponse(); + getRequestNotifier().notifyFailure(request, cause); + List listeners = exchange.getConversation().getResponseListeners(); + getResponseNotifier().notifyFailure(listeners, response, cause); + getResponseNotifier().notifyComplete(listeners, new Result(request, cause, response, cause)); } @Override @@ -432,7 +422,7 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable @Override public void dump(Appendable out, String indent) throws IOException { - ContainerLifeCycle.dumpObject(out, this + " - requests queued: " + requests.size()); + ContainerLifeCycle.dumpObject(out, this + " - requests queued: " + exchanges.size()); List connections = new ArrayList<>(); for (Connection connection : idleConnections) connections.add(connection + " - IDLE"); @@ -449,19 +439,7 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable getScheme(), getHost(), getPort(), - proxyAddress == null ? "" : " via " + proxyAddress.getHostString() + ":" + proxyAddress.getPort()); - } - - private static class RequestContext - { - private final Request request; - private final List listeners; - - private RequestContext(Request request, List listeners) - { - this.request = request; - this.listeners = listeners; - } + proxyAddress == null ? "" : " via " + proxyAddress.getHost() + ":" + proxyAddress.getPort()); } /** @@ -499,8 +477,8 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable private void tunnel(final Connection connection) { - String target = address.getHostString() + ":" + address.getPort(); - Request connect = client.newRequest(proxyAddress.getHostString(), proxyAddress.getPort()) + String target = address.getHost() + ":" + address.getPort(); + Request connect = client.newRequest(proxyAddress.getHost(), proxyAddress.getPort()) .scheme(HttpScheme.HTTP.asString()) .method(HttpMethod.CONNECT) .path(target) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java index fa01cf91161..56bc5bc2a96 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java @@ -36,21 +36,23 @@ public class HttpExchange private final AtomicInteger complete = new AtomicInteger(); private final CountDownLatch terminate = new CountDownLatch(2); private final HttpConversation conversation; - private final HttpConnection connection; + private final HttpDestination destination; private final Request request; private final List listeners; private final HttpResponse response; - private volatile boolean last; + private volatile HttpConnection connection; private volatile Throwable requestFailure; private volatile Throwable responseFailure; - public HttpExchange(HttpConversation conversation, HttpConnection connection, Request request, List listeners) + public HttpExchange(HttpConversation conversation, HttpDestination destination, Request request, List listeners) { this.conversation = conversation; - this.connection = connection; + this.destination = destination; this.request = request; this.listeners = listeners; this.response = new HttpResponse(request, listeners); + conversation.getExchanges().offer(this); + conversation.setResponseListener(null); } public HttpConversation getConversation() @@ -83,25 +85,9 @@ public class HttpExchange return responseFailure; } - /** - * @return whether this exchange is the last in the conversation - */ - public boolean isLast() + public void setConnection(HttpConnection connection) { - return last; - } - - /** - * @param last whether this exchange is the last in the conversation - */ - public void setLast(boolean last) - { - this.last = last; - } - - public void receive() - { - connection.receive(); + this.connection = connection; } public AtomicMarkableReference requestComplete(Throwable failure) @@ -177,8 +163,7 @@ public class HttpExchange { // Request and response completed LOG.debug("{} complete", this); - if (isLast()) - conversation.complete(); + conversation.complete(); } result = new Result(getRequest(), getRequestFailure(), getResponse(), getResponseFailure()); } @@ -188,10 +173,23 @@ public class HttpExchange public boolean abort(Throwable cause) { - LOG.debug("Aborting {} reason {}", this, cause); - boolean aborted = connection.abort(this, cause); - LOG.debug("Aborted {}: {}", this, aborted); - return aborted; + if (destination.remove(this)) + { + destination.abort(this, cause); + LOG.debug("Aborted while queued {}: {}", this, cause); + return true; + } + else + { + HttpConnection connection = this.connection; + // If there is no connection, this exchange is already completed + if (connection == null) + return false; + + boolean aborted = connection.abort(cause); + LOG.debug("Aborted while active ({}) {}: {}", aborted, this, cause); + return aborted; + } } public void resetResponse(boolean success) @@ -204,7 +202,9 @@ public class HttpExchange public void proceed(boolean proceed) { - connection.proceed(proceed); + HttpConnection connection = this.connection; + if (connection != null) + connection.proceed(proceed); } public void terminateRequest() diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java index 10b7a2e116b..8f4e593caef 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java @@ -22,7 +22,6 @@ import java.io.EOFException; import java.io.IOException; import java.net.URI; import java.nio.ByteBuffer; -import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; @@ -52,13 +51,11 @@ public class HttpReceiver implements HttpParser.ResponseHandler private final AtomicReference state = new AtomicReference<>(State.IDLE); private final HttpParser parser = new HttpParser(this); private final HttpConnection connection; - private final ResponseNotifier responseNotifier; private ContentDecoder decoder; public HttpReceiver(HttpConnection connection) { this.connection = connection; - this.responseNotifier = new ResponseNotifier(connection.getHttpClient()); } public void receive() @@ -146,41 +143,14 @@ public class HttpReceiver implements HttpParser.ResponseHandler response.version(version).status(status).reason(reason); // Probe the protocol handlers - HttpExchange initialExchange = conversation.getExchanges().peekFirst(); HttpClient client = connection.getHttpClient(); ProtocolHandler protocolHandler = client.findProtocolHandler(exchange.getRequest(), response); Response.Listener handlerListener = protocolHandler == null ? null : protocolHandler.getResponseListener(); - if (handlerListener == null) - { - exchange.setLast(true); - if (initialExchange == exchange) - { - conversation.setResponseListeners(exchange.getResponseListeners()); - } - else - { - List listeners = new ArrayList<>(exchange.getResponseListeners()); - listeners.addAll(initialExchange.getResponseListeners()); - conversation.setResponseListeners(listeners); - } - } - else - { - LOG.debug("Found protocol handler {}", protocolHandler); - if (initialExchange == exchange) - { - conversation.setResponseListeners(Collections.singletonList(handlerListener)); - } - else - { - List listeners = new ArrayList<>(exchange.getResponseListeners()); - listeners.add(handlerListener); - conversation.setResponseListeners(listeners); - } - } + exchange.getConversation().setResponseListener(handlerListener); LOG.debug("Receiving {}", response); - responseNotifier.notifyBegin(conversation.getResponseListeners(), response); + ResponseNotifier notifier = connection.getDestination().getResponseNotifier(); + notifier.notifyBegin(conversation.getResponseListeners(), response); } } return false; @@ -197,7 +167,8 @@ public class HttpReceiver implements HttpParser.ResponseHandler { HttpConversation conversation = exchange.getConversation(); HttpResponse response = exchange.getResponse(); - boolean process = responseNotifier.notifyHeader(conversation.getResponseListeners(), response, field); + ResponseNotifier notifier = connection.getDestination().getResponseNotifier(); + boolean process = notifier.notifyHeader(conversation.getResponseListeners(), response, field); if (process) { response.getHeaders().add(field); @@ -250,7 +221,8 @@ public class HttpReceiver implements HttpParser.ResponseHandler HttpConversation conversation = exchange.getConversation(); HttpResponse response = exchange.getResponse(); LOG.debug("Headers {}", response); - responseNotifier.notifyHeaders(conversation.getResponseListeners(), response); + ResponseNotifier notifier = connection.getDestination().getResponseNotifier(); + notifier.notifyHeaders(conversation.getResponseListeners(), response); Enumeration contentEncodings = response.getHeaders().getValues(HttpHeader.CONTENT_ENCODING.asString(), ","); if (contentEncodings != null) @@ -292,7 +264,8 @@ public class HttpReceiver implements HttpParser.ResponseHandler LOG.debug("{} {}: {} bytes", decoder, response, buffer.remaining()); } - responseNotifier.notifyContent(conversation.getResponseListeners(), response, buffer); + ResponseNotifier notifier = connection.getDestination().getResponseNotifier(); + notifier.notifyContent(conversation.getResponseListeners(), response, buffer); } } return false; @@ -326,15 +299,15 @@ public class HttpReceiver implements HttpParser.ResponseHandler HttpResponse response = exchange.getResponse(); List listeners = exchange.getConversation().getResponseListeners(); - responseNotifier.notifySuccess(listeners, response); + ResponseNotifier notifier = connection.getDestination().getResponseNotifier(); + notifier.notifySuccess(listeners, response); LOG.debug("Received {}", response); Result result = completion.getReference(); if (result != null) { connection.complete(exchange, !result.isFailed()); - - responseNotifier.notifyComplete(listeners, result); + notifier.notifyComplete(listeners, result); } return true; @@ -368,7 +341,8 @@ public class HttpReceiver implements HttpParser.ResponseHandler HttpResponse response = exchange.getResponse(); HttpConversation conversation = exchange.getConversation(); - responseNotifier.notifyFailure(conversation.getResponseListeners(), response, failure); + ResponseNotifier notifier = connection.getDestination().getResponseNotifier(); + notifier.notifyFailure(conversation.getResponseListeners(), response, failure); LOG.debug("Failed {} {}", response, failure); Result result = completion.getReference(); @@ -376,7 +350,7 @@ public class HttpReceiver implements HttpParser.ResponseHandler { connection.complete(exchange, false); - responseNotifier.notifyComplete(conversation.getResponseListeners(), result); + notifier.notifyComplete(conversation.getResponseListeners(), result); } return true; @@ -411,7 +385,7 @@ public class HttpReceiver implements HttpParser.ResponseHandler fail(new TimeoutException()); } - public boolean abort(HttpExchange exchange, Throwable cause) + public boolean abort(Throwable cause) { return fail(cause); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java index c0c320eb255..ae27d407ac3 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java @@ -82,7 +82,7 @@ public class HttpRequest implements Request scheme = uri.getScheme(); host = uri.getHost(); port = client.normalizePort(scheme, uri.getPort()); - path = uri.getPath(); + path = uri.getRawPath(); String query = uri.getRawQuery(); if (query != null) { @@ -468,8 +468,7 @@ public class HttpRequest implements Request public boolean abort(Throwable cause) { aborted = Objects.requireNonNull(cause); - if (client.provideDestination(getScheme(), getHost(), getPort()).abort(this, cause)) - return true; + // The conversation may be null if it is already completed HttpConversation conversation = client.getConversation(getConversationID(), false); return conversation != null && conversation.abort(cause); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java index ef8ece5901c..0c0e064355b 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java @@ -19,10 +19,8 @@ package org.eclipse.jetty.client; import java.nio.ByteBuffer; -import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; -import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicMarkableReference; @@ -30,7 +28,6 @@ import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.client.api.ContentProvider; import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.http.HttpGenerator; import org.eclipse.jetty.http.HttpHeader; @@ -51,16 +48,12 @@ public class HttpSender implements AsyncContentProvider.Listener private final AtomicReference sendState = new AtomicReference<>(SendState.IDLE); private final HttpGenerator generator = new HttpGenerator(); private final HttpConnection connection; - private final RequestNotifier requestNotifier; - private final ResponseNotifier responseNotifier; private Iterator contentIterator; private ContinueContentChunk continueContentChunk; public HttpSender(HttpConnection connection) { this.connection = connection; - this.requestNotifier = new RequestNotifier(connection.getHttpClient()); - this.responseNotifier = new ResponseNotifier(connection.getHttpClient()); } @Override @@ -108,20 +101,6 @@ public class HttpSender implements AsyncContentProvider.Listener if (!updateState(State.IDLE, State.BEGIN)) throw new IllegalStateException(); - // Arrange the listeners, so that if there is a request failure the proper listeners are notified - HttpConversation conversation = exchange.getConversation(); - HttpExchange initialExchange = conversation.getExchanges().peekFirst(); - if (initialExchange == exchange) - { - conversation.setResponseListeners(exchange.getResponseListeners()); - } - else - { - List listeners = new ArrayList<>(exchange.getResponseListeners()); - listeners.addAll(initialExchange.getResponseListeners()); - conversation.setResponseListeners(listeners); - } - Request request = exchange.getRequest(); Throwable cause = request.getAbortCause(); if (cause != null) @@ -131,7 +110,8 @@ public class HttpSender implements AsyncContentProvider.Listener else { LOG.debug("Sending {}", request); - requestNotifier.notifyBegin(request); + RequestNotifier notifier = connection.getDestination().getRequestNotifier(); + notifier.notifyBegin(request); ContentProvider content = request.getContent(); this.contentIterator = content == null ? Collections.emptyIterator() : content.iterator(); @@ -214,7 +194,8 @@ public class HttpSender implements AsyncContentProvider.Listener { if (!updateState(currentState, State.HEADERS)) continue; - requestNotifier.notifyHeaders(request); + RequestNotifier notifier = connection.getDestination().getRequestNotifier(); + notifier.notifyHeaders(request); break out; } case HEADERS: @@ -461,7 +442,8 @@ public class HttpSender implements AsyncContentProvider.Listener if (!updateState(current, State.COMMIT)) continue; LOG.debug("Committed {}", request); - requestNotifier.notifyCommit(request); + RequestNotifier notifier = connection.getDestination().getRequestNotifier(); + notifier.notifyCommit(request); return true; case COMMIT: if (!updateState(current, State.COMMIT)) @@ -495,8 +477,9 @@ public class HttpSender implements AsyncContentProvider.Listener // It is important to notify completion *after* we reset because // the notification may trigger another request/response + HttpDestination destination = connection.getDestination(); Request request = exchange.getRequest(); - requestNotifier.notifySuccess(request); + destination.getRequestNotifier().notifySuccess(request); LOG.debug("Sent {}", request); Result result = completion.getReference(); @@ -505,7 +488,7 @@ public class HttpSender implements AsyncContentProvider.Listener connection.complete(exchange, !result.isFailed()); HttpConversation conversation = exchange.getConversation(); - responseNotifier.notifyComplete(conversation.getResponseListeners(), result); + destination.getResponseNotifier().notifyComplete(conversation.getResponseListeners(), result); } return true; @@ -533,8 +516,9 @@ public class HttpSender implements AsyncContentProvider.Listener exchange.terminateRequest(); + HttpDestination destination = connection.getDestination(); Request request = exchange.getRequest(); - requestNotifier.notifyFailure(request, failure); + destination.getRequestNotifier().notifyFailure(request, failure); LOG.debug("Failed {} {}", request, failure); Result result = completion.getReference(); @@ -551,13 +535,13 @@ public class HttpSender implements AsyncContentProvider.Listener connection.complete(exchange, false); HttpConversation conversation = exchange.getConversation(); - responseNotifier.notifyComplete(conversation.getResponseListeners(), result); + destination.getResponseNotifier().notifyComplete(conversation.getResponseListeners(), result); } return true; } - public boolean abort(HttpExchange exchange, Throwable cause) + public boolean abort(Throwable cause) { State current = state.get(); boolean abortable = isBeforeCommit(current) || diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/RedirectProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/RedirectProtocolHandler.java index 2e88dc08537..0d5fa02840f 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/RedirectProtocolHandler.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/RedirectProtocolHandler.java @@ -19,7 +19,10 @@ package org.eclipse.jetty.client; import java.net.URI; +import java.net.URISyntaxException; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; @@ -28,6 +31,14 @@ import org.eclipse.jetty.http.HttpMethod; public class RedirectProtocolHandler extends Response.Listener.Empty implements ProtocolHandler { + private static String SCHEME_REGEXP = "(^https?)"; + private static String AUTHORITY_REGEXP = "([^/\\?#]+)"; + // The location may be relative so the scheme://authority part may be missing + private static String DESTINATION_REGEXP = "(" + SCHEME_REGEXP + "://" + AUTHORITY_REGEXP + ")?"; + private static String PATH_REGEXP = "([^\\?#]*)"; + private static String QUERY_REGEXP = "([^#]*)"; + private static String FRAGMENT_REGEXP = "(.*)"; + private static Pattern URI_PATTERN = Pattern.compile(DESTINATION_REGEXP + PATH_REGEXP + QUERY_REGEXP + FRAGMENT_REGEXP); private static final String ATTRIBUTE = RedirectProtocolHandler.class.getName() + ".redirects"; private final HttpClient client; @@ -66,37 +77,55 @@ public class RedirectProtocolHandler extends Response.Listener.Empty implements { Request request = result.getRequest(); Response response = result.getResponse(); - URI location = URI.create(response.getHeaders().get("location")); - int status = response.getStatus(); - switch (status) + String location = response.getHeaders().get("location"); + if (location != null) { - case 301: + URI newURI = sanitize(location); + if (newURI != null) { - if (request.getMethod() == HttpMethod.GET || request.getMethod() == HttpMethod.HEAD) - redirect(result, request.getMethod(), location); - else - fail(result, new HttpResponseException("HTTP protocol violation: received 301 for non GET or HEAD request", response)); - break; + if (!newURI.isAbsolute()) + newURI = request.getURI().resolve(newURI); + + int status = response.getStatus(); + switch (status) + { + case 301: + { + if (request.getMethod() == HttpMethod.GET || request.getMethod() == HttpMethod.HEAD) + redirect(result, request.getMethod(), newURI); + else + fail(result, new HttpResponseException("HTTP protocol violation: received 301 for non GET or HEAD request", response)); + break; + } + case 302: + case 303: + { + // Redirect must be done using GET + redirect(result, HttpMethod.GET, newURI); + break; + } + case 307: + { + // Keep same method + redirect(result, request.getMethod(), newURI); + break; + } + default: + { + fail(result, new HttpResponseException("Unhandled HTTP status code " + status, response)); + break; + } + } } - case 302: - case 303: + else { - // Redirect must be done using GET - redirect(result, HttpMethod.GET, location); - break; - } - case 307: - { - // Keep same method - redirect(result, request.getMethod(), location); - break; - } - default: - { - fail(result, new HttpResponseException("Unhandled HTTP status code " + status, response)); - break; + fail(result, new HttpResponseException("Malformed Location header " + location, response)); } } + else + { + fail(result, new HttpResponseException("Missing Location header " + location, response)); + } } else { @@ -104,6 +133,43 @@ public class RedirectProtocolHandler extends Response.Listener.Empty implements } } + private URI sanitize(String location) + { + // Redirects should be valid, absolute, URIs, with properly escaped paths and encoded + // query parameters. However, shit happens, and here we try our best to recover. + + try + { + // Direct hit first: if passes, we're good + return new URI(location); + } + catch (URISyntaxException x) + { + Matcher matcher = URI_PATTERN.matcher(location); + if (matcher.matches()) + { + String scheme = matcher.group(2); + String authority = matcher.group(3); + String path = matcher.group(4); + String query = matcher.group(5); + if (query.length() == 0) + query = null; + String fragment = matcher.group(6); + if (fragment.length() == 0) + fragment = null; + try + { + return new URI(scheme, authority, path, query, fragment); + } + catch (URISyntaxException xx) + { + // Give up + } + } + return null; + } + } + private void redirect(Result result, HttpMethod method, URI location) { final Request request = result.getRequest(); @@ -147,7 +213,6 @@ public class RedirectProtocolHandler extends Response.Listener.Empty implements Response response = result.getResponse(); HttpConversation conversation = client.getConversation(request.getConversationID(), false); List listeners = conversation.getExchanges().peekFirst().getResponseListeners(); - // TODO: should we replay all events, or just the failure ? notifier.notifyFailure(listeners, response, failure); notifier.notifyComplete(listeners, new Result(request, response, failure)); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Connection.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Connection.java index 838b4073241..df001b2030e 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Connection.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Connection.java @@ -18,13 +18,15 @@ package org.eclipse.jetty.client.api; +import org.eclipse.jetty.util.Promise; + /** * {@link Connection} represent a connection to a {@link Destination} and allow applications to send * requests via {@link #send(Request, Response.CompleteListener)}. *

* {@link Connection}s are normally pooled by {@link Destination}s, but unpooled {@link Connection}s * may be created by applications that want to do their own connection management via - * {@link Destination#newConnection()} and {@link Connection#close()}. + * {@link Destination#newConnection(Promise)} and {@link Connection#close()}. */ public interface Connection extends AutoCloseable { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Destination.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Destination.java index c406e81ba35..bf09e7a88d1 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Destination.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Destination.java @@ -18,16 +18,16 @@ package org.eclipse.jetty.client.api; -import java.util.concurrent.Future; - import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.util.FuturePromise; +import org.eclipse.jetty.util.Promise; /** * {@link Destination} represents the triple made of the {@link #getScheme}, the {@link #getHost} * and the {@link #getPort}. *

* {@link Destination} holds a pool of {@link Connection}s, but allows to create unpooled - * connections if the application wants full control over connection management via {@link #newConnection()}. + * connections if the application wants full control over connection management via {@link #newConnection(Promise)}. *

* {@link Destination}s may be obtained via {@link HttpClient#getDestination(String, String, int)} */ @@ -49,7 +49,40 @@ public interface Destination int getPort(); /** - * @return a future to a new, unpooled, {@link Connection} + * Creates asynchronously a new, unpooled, {@link Connection} that will be returned + * at a later time through the given {@link Promise}. + *

+ * Use {@link FuturePromise} to wait for the connection: + *

+     * Destination destination = ...;
+     * FuturePromise<Connection> futureConnection = new FuturePromise<>();
+     * destination.newConnection(futureConnection);
+     * Connection connection = futureConnection.get(5, TimeUnit.SECONDS);
+     * 
+ * + * @param promise the promise of a new, unpooled, {@link Connection} */ - Future newConnection(); + void newConnection(Promise promise); + + public static class Address + { + private final String host; + private final int port; + + public Address(String host, int port) + { + this.host = host; + this.port = port; + } + + public String getHost() + { + return host; + } + + public int getPort() + { + return port; + } + } } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java index 85792846b43..c49b37b97d7 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.client.api.Destination; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.util.FutureResponseListener; import org.eclipse.jetty.toolchain.test.annotation.Slow; +import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.Assert; import org.junit.Test; @@ -43,7 +44,9 @@ public class HttpClientExplicitConnectionTest extends AbstractHttpClientServerTe start(new EmptyServerHandler()); Destination destination = client.getDestination(scheme, "localhost", connector.getLocalPort()); - try (Connection connection = destination.newConnection().get(5, TimeUnit.SECONDS)) + FuturePromise futureConnection = new FuturePromise<>(); + destination.newConnection(futureConnection); + try (Connection connection = futureConnection.get(5, TimeUnit.SECONDS)) { Request request = client.newRequest(destination.getHost(), destination.getPort()).scheme(scheme); FutureResponseListener listener = new FutureResponseListener(request); @@ -66,7 +69,9 @@ public class HttpClientExplicitConnectionTest extends AbstractHttpClientServerTe start(new EmptyServerHandler()); Destination destination = client.getDestination(scheme, "localhost", connector.getLocalPort()); - Connection connection = destination.newConnection().get(5, TimeUnit.SECONDS); + FuturePromise futureConnection = new FuturePromise<>(); + destination.newConnection(futureConnection); + Connection connection = futureConnection.get(5, TimeUnit.SECONDS); Request request = client.newRequest(destination.getHost(), destination.getPort()).scheme(scheme); FutureResponseListener listener = new FutureResponseListener(request); connection.send(request, listener); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientRedirectTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientRedirectTest.java index eb13ece5549..be0af57cc13 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientRedirectTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientRedirectTest.java @@ -18,13 +18,12 @@ package org.eclipse.jetty.client; -import static org.junit.Assert.fail; - import java.io.IOException; +import java.net.URLDecoder; import java.nio.ByteBuffer; +import java.nio.channels.UnresolvedAddressException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; - import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -38,10 +37,13 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.toolchain.test.IO; import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import static org.junit.Assert.fail; + public class HttpClientRedirectTest extends AbstractHttpClientServerTest { public HttpClientRedirectTest(SslContextFactory sslContextFactory) @@ -199,6 +201,62 @@ public class HttpClientRedirectTest extends AbstractHttpClientServerTest Assert.assertTrue(response.getHeaders().containsKey(HttpHeader.LOCATION.asString())); } + @Test + public void testRelativeLocation() throws Exception + { + Response response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .path("/303/localhost/done?relative=true") + .timeout(5, TimeUnit.SECONDS) + .send(); + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + Assert.assertFalse(response.getHeaders().containsKey(HttpHeader.LOCATION.asString())); + } + + @Test + public void testAbsoluteURIPathWithSpaces() throws Exception + { + Response response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .path("/303/localhost/a+space?decode=true") + .timeout(5, TimeUnit.SECONDS) + .send(); + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + Assert.assertFalse(response.getHeaders().containsKey(HttpHeader.LOCATION.asString())); + } + + @Test + public void testRelativeURIPathWithSpaces() throws Exception + { + Response response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .path("/303/localhost/a+space?relative=true&decode=true") + .timeout(5, TimeUnit.SECONDS) + .send(); + Assert.assertNotNull(response); + Assert.assertEquals(200, response.getStatus()); + Assert.assertFalse(response.getHeaders().containsKey(HttpHeader.LOCATION.asString())); + } + + @Test + public void testRedirectFailed() throws Exception + { + try + { + client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .path("/303/doesNotExist/done") + .timeout(5, TimeUnit.SECONDS) + .send(); + } + catch (ExecutionException x) + { + Assert.assertThat(x.getCause(), Matchers.instanceOf(UnresolvedAddressException.class)); + } + } + private class RedirectHandler extends AbstractHandler { @Override @@ -212,10 +270,17 @@ public class HttpClientRedirectTest extends AbstractHttpClientServerTest response.setStatus(status); String host = paths[2]; - response.setHeader("Location", request.getScheme() + "://" + host + ":" + request.getServerPort() + "/" + paths[3]); + String path = paths[3]; + boolean relative = Boolean.parseBoolean(request.getParameter("relative")); + String location = relative ? "" : request.getScheme() + "://" + host + ":" + request.getServerPort(); + location += "/" + path; - String close = request.getParameter("close"); - if (Boolean.parseBoolean(close)) + if (Boolean.parseBoolean(request.getParameter("decode"))) + location = URLDecoder.decode(location, "UTF-8"); + + response.setHeader("Location", location); + + if (Boolean.parseBoolean(request.getParameter("close"))) response.setHeader("Connection", "close"); } catch (NumberFormatException x) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTimeoutTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTimeoutTest.java index 6ac176795ea..3297b80802e 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTimeoutTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTimeoutTest.java @@ -40,6 +40,7 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ssl.SslConnection; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.toolchain.test.annotation.Slow; +import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.hamcrest.Matchers; @@ -175,7 +176,9 @@ public class HttpClientTimeoutTest extends AbstractHttpClientServerTest final CountDownLatch latch = new CountDownLatch(1); Destination destination = client.getDestination(scheme, "localhost", connector.getLocalPort()); - try (Connection connection = destination.newConnection().get(5, TimeUnit.SECONDS)) + FuturePromise futureConnection = new FuturePromise<>(); + destination.newConnection(futureConnection); + try (Connection connection = futureConnection.get(5, TimeUnit.SECONDS)) { Request request = client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) @@ -203,7 +206,9 @@ public class HttpClientTimeoutTest extends AbstractHttpClientServerTest final CountDownLatch latch = new CountDownLatch(1); Destination destination = client.getDestination(scheme, "localhost", connector.getLocalPort()); - try (Connection connection = destination.newConnection().get(5, TimeUnit.SECONDS)) + FuturePromise futureConnection = new FuturePromise<>(); + destination.newConnection(futureConnection); + try (Connection connection = futureConnection.get(5, TimeUnit.SECONDS)) { Request request = client.newRequest(destination.getHost(), destination.getPort()) .scheme(scheme) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpReceiverTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpReceiverTest.java index 2cfdc3b665b..ad78b1ed968 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpReceiverTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpReceiverTest.java @@ -74,9 +74,9 @@ public class HttpReceiverTest { HttpRequest request = new HttpRequest(client, URI.create("http://localhost")); FutureResponseListener listener = new FutureResponseListener(request); - HttpExchange exchange = new HttpExchange(conversation, connection, request, Collections.singletonList(listener)); + HttpExchange exchange = new HttpExchange(conversation, destination, request, Collections.singletonList(listener)); conversation.getExchanges().offer(exchange); - connection.setExchange(exchange); + connection.associate(exchange); exchange.requestComplete(null); exchange.terminateRequest(); return exchange; @@ -91,7 +91,7 @@ public class HttpReceiverTest "\r\n"); HttpExchange exchange = newExchange(); FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0); - exchange.receive(); + connection.receive(); Response response = listener.get(5, TimeUnit.SECONDS); Assert.assertNotNull(response); @@ -115,7 +115,7 @@ public class HttpReceiverTest content); HttpExchange exchange = newExchange(); FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0); - exchange.receive(); + connection.receive(); Response response = listener.get(5, TimeUnit.SECONDS); Assert.assertNotNull(response); @@ -142,9 +142,9 @@ public class HttpReceiverTest content1); HttpExchange exchange = newExchange(); FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0); - exchange.receive(); + connection.receive(); endPoint.setInputEOF(); - exchange.receive(); + connection.receive(); try { @@ -166,7 +166,7 @@ public class HttpReceiverTest "\r\n"); HttpExchange exchange = newExchange(); FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0); - exchange.receive(); + connection.receive(); // Simulate an idle timeout connection.idleTimeout(); @@ -190,7 +190,7 @@ public class HttpReceiverTest "\r\n"); HttpExchange exchange = newExchange(); FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0); - exchange.receive(); + connection.receive(); try { @@ -221,20 +221,20 @@ public class HttpReceiverTest "\r\n"); HttpExchange exchange = newExchange(); FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0); - exchange.receive(); + connection.receive(); endPoint.reset(); ByteBuffer buffer = ByteBuffer.wrap(gzip); int fragment = buffer.limit() - 1; buffer.limit(fragment); endPoint.setInput(buffer); - exchange.receive(); + connection.receive(); endPoint.reset(); buffer.limit(gzip.length); buffer.position(fragment); endPoint.setInput(buffer); - exchange.receive(); + connection.receive(); ContentResponse response = listener.get(5, TimeUnit.SECONDS); Assert.assertNotNull(response); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java b/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java index f5caedbef42..cd7d09e0989 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java @@ -39,6 +39,7 @@ import org.eclipse.jetty.client.util.InputStreamResponseListener; import org.eclipse.jetty.client.util.OutputStreamContentProvider; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.util.FuturePromise; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; @@ -149,7 +150,9 @@ public class Usage client.start(); // Create an explicit connection, and use try-with-resources to manage it - try (Connection connection = client.getDestination("http", "localhost", 8080).newConnection().get(5, TimeUnit.SECONDS)) + FuturePromise futureConnection = new FuturePromise<>(); + client.getDestination("http", "localhost", 8080).newConnection(futureConnection); + try (Connection connection = futureConnection.get(5, TimeUnit.SECONDS)) { Request request = client.newRequest("localhost", 8080); diff --git a/jetty-distribution/src/main/resources/start.ini b/jetty-distribution/src/main/resources/start.ini index 495b098c598..b927a2bddf6 100644 --- a/jetty-distribution/src/main/resources/start.ini +++ b/jetty-distribution/src/main/resources/start.ini @@ -200,4 +200,5 @@ etc/jetty-requestlog.xml # etc/jetty-stats.xml # etc/jetty-debug.xml # etc/jetty-ipaccess.xml +# etc/jetty-lowresources.xml #=========================================================== diff --git a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/ContextFactory.java b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/ContextFactory.java index 84e6b87ae5f..b3c69074f88 100644 --- a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/ContextFactory.java +++ b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/ContextFactory.java @@ -51,9 +51,11 @@ import org.eclipse.jetty.util.log.Logger; * specific to a webapp). * * The context selected is based on classloaders. First - * we try looking in at the classloader that is associated - * with the current webapp context (if there is one). If - * not, we use the thread context classloader. + * we try looking at the thread context classloader if it is set, and walk its + * hierarchy, creating a context if none is found. If the thread context classloader + * is not set, then we use the classloader associated with the current Context. + * + * If there is no current context, or no classloader, we return null. * * Created: Fri Jun 27 09:26:40 2003 * @@ -79,9 +81,16 @@ public class ContextFactory implements ObjectFactory /** * Find or create a context which pertains to a classloader. * - * We use either the classloader for the current ContextHandler if - * we are handling a request, OR we use the thread context classloader - * if we are not processing a request. + * If the thread context classloader is set, we try to find an already-created naming context + * for it. If one does not exist, we walk its classloader hierarchy until one is found, or we + * run out of parent classloaders. In the latter case, we will create a new naming context associated + * with the original thread context classloader. + * + * If the thread context classloader is not set, we obtain the classloader from the current + * jetty Context, and look for an already-created naming context. + * + * If there is no current jetty Context, or it has no associated classloader, we + * return null. * @see javax.naming.spi.ObjectFactory#getObjectInstance(java.lang.Object, javax.naming.Name, javax.naming.Context, java.util.Hashtable) */ public Object getObjectInstance (Object obj, @@ -98,41 +107,89 @@ public class ContextFactory implements ObjectFactory return ctx; } - ClassLoader loader = null; - - loader = Thread.currentThread().getContextClassLoader(); - if (__log.isDebugEnabled() && loader != null) __log.debug("Using thread context classloader"); - - if (loader == null && ContextHandler.getCurrentContext() != null) + + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + ClassLoader loader = tccl; + //If the thread context classloader is set, then try its hierarchy to find a matching context + if (loader != null) { + if (__log.isDebugEnabled() && loader != null) __log.debug("Trying thread context classloader"); + while (ctx == null && loader != null) + { + ctx = getContextForClassLoader(loader); + if (ctx == null && loader != null) + loader = loader.getParent(); + } + + if (ctx == null) + { + ctx = newNamingContext(obj, tccl, env, name, nameCtx); + __contextMap.put (tccl, ctx); + if(__log.isDebugEnabled())__log.debug("Made context "+name.get(0)+" for classloader: "+tccl); + } + return ctx; + } + + + //If trying thread context classloader hierarchy failed, try the + //classloader associated with the current context + if (ContextHandler.getCurrentContext() != null) + { + + if (__log.isDebugEnabled() && loader != null) __log.debug("Trying classloader of current org.eclipse.jetty.server.handler.ContextHandler"); loader = ContextHandler.getCurrentContext().getContextHandler().getClassLoader(); - if (__log.isDebugEnabled() && loader != null) __log.debug("Using classloader of current org.eclipse.jetty.server.handler.ContextHandler"); + ctx = (Context)__contextMap.get(loader); + + if (ctx == null && loader != null) + { + ctx = newNamingContext(obj, loader, env, name, nameCtx); + __contextMap.put (loader, ctx); + if(__log.isDebugEnabled())__log.debug("Made context "+name.get(0)+" for classloader: "+loader); + } + + return ctx; } + return null; + } - //Get the context matching the classloader - ctx = (Context)__contextMap.get(loader); - //The map does not contain an entry for this classloader - if (ctx == null) - { - //Didn't find a context to match, make one - Reference ref = (Reference)obj; - StringRefAddr parserAddr = (StringRefAddr)ref.get("parser"); - String parserClassName = (parserAddr==null?null:(String)parserAddr.getContent()); - NameParser parser = (NameParser)(parserClassName==null?null:loader.loadClass(parserClassName).newInstance()); + /** + * Create a new NamingContext. + * @param obj + * @param loader + * @param env + * @param name + * @param parentCtx + * @return + * @throws Exception + */ + public NamingContext newNamingContext(Object obj, ClassLoader loader, Hashtable env, Name name, Context parentCtx) + throws Exception + { + Reference ref = (Reference)obj; + StringRefAddr parserAddr = (StringRefAddr)ref.get("parser"); + String parserClassName = (parserAddr==null?null:(String)parserAddr.getContent()); + NameParser parser = (NameParser)(parserClassName==null?null:loader.loadClass(parserClassName).newInstance()); - ctx = new NamingContext (env, - name.get(0), - (NamingContext)nameCtx, - parser); - if(__log.isDebugEnabled())__log.debug("Made context "+name.get(0)+" for classloader: "+loader); - __contextMap.put (loader, ctx); - } - - return ctx; + return new NamingContext (env, + name.get(0), + (NamingContext)parentCtx, + parser); } + /** + * Find the naming Context for the given classloader + * @param loader + * @return + */ + public Context getContextForClassLoader(ClassLoader loader) + { + if (loader == null) + return null; + + return (Context)__contextMap.get(loader); + } /** diff --git a/jetty-jndi/src/test/java/org/eclipse/jetty/jndi/java/TestJNDI.java b/jetty-jndi/src/test/java/org/eclipse/jetty/jndi/java/TestJNDI.java index cbdfd85103d..82e283d1463 100644 --- a/jetty-jndi/src/test/java/org/eclipse/jetty/jndi/java/TestJNDI.java +++ b/jetty-jndi/src/test/java/org/eclipse/jetty/jndi/java/TestJNDI.java @@ -41,10 +41,14 @@ import javax.naming.NamingException; import javax.naming.Reference; import javax.naming.StringRefAddr; import javax.naming.spi.ObjectFactory; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; import org.eclipse.jetty.jndi.NamingContext; +import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.junit.Ignore; import org.junit.Test; /** * @@ -68,70 +72,138 @@ public class TestJNDI } } - + + @Test - public void testIt() throws Exception + public void testThreadContextClassloaderAndCurrentContext() + throws Exception { - //set up some classloaders - Thread currentThread = Thread.currentThread(); - ClassLoader currentLoader = currentThread.getContextClassLoader(); - ClassLoader childLoader1 = new URLClassLoader(new URL[0], currentLoader); - ClassLoader childLoader2 = new URLClassLoader(new URL[0], currentLoader); + //create a jetty context, and start it so that its classloader it created + //and it is the current context + ClassLoader currentLoader = Thread.currentThread().getContextClassLoader(); + ContextHandler ch = new ContextHandler(); + URLClassLoader chLoader = new URLClassLoader(new URL[0], currentLoader); + ch.setClassLoader(chLoader); + + //Create another one + ContextHandler ch2 = new ContextHandler(); + URLClassLoader ch2Loader = new URLClassLoader(new URL[0], currentLoader); + ch2.setClassLoader(ch2Loader); try { - - //Uncomment to aid with debug - /* - javaRootURLContext.getRoot().addListener(new NamingContext.Listener() + ch.setContextPath("/ch"); + ch.addEventListener(new ServletContextListener() { - public void unbind(NamingContext ctx, Binding binding) + private Context comp; + private Object testObj = new Object(); + + public void contextInitialized(ServletContextEvent sce) { - System.err.println("java unbind "+binding+" from "+ctx.getName()); + try + { + InitialContext initCtx = new InitialContext(); + Context java = (Context)initCtx.lookup("java:"); + assertNotNull(java); + comp = (Context)initCtx.lookup("java:comp"); + assertNotNull(comp); + Context env = ((Context)comp).createSubcontext("env"); + assertNotNull(env); + env.bind("ch", testObj); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } } - public Binding bind(NamingContext ctx, Binding binding) + public void contextDestroyed(ServletContextEvent sce) { - System.err.println("java bind "+binding+" to "+ctx.getName()); - return binding; + try + { + assertNotNull(comp); + assertEquals(testObj,comp.lookup("env/ch")); + comp.destroySubcontext("env"); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } } }); + //Starting the context makes it current and creates a classloader for it + ch.start(); - localContextRoot.getRoot().addListener(new NamingContext.Listener() + + ch2.setContextPath("/ch2"); + ch2.addEventListener(new ServletContextListener() { - public void unbind(NamingContext ctx, Binding binding) - { - System.err.println("local unbind "+binding+" from "+ctx.getName()); - } + private Context comp; + private Object testObj = new Object(); - public Binding bind(NamingContext ctx, Binding binding) + public void contextInitialized(ServletContextEvent sce) { - System.err.println("local bind "+binding+" to "+ctx.getName()); - return binding; + try + { + InitialContext initCtx = new InitialContext(); + comp = (Context)initCtx.lookup("java:comp"); + assertNotNull(comp); + + //another context's bindings should not be visible + Context env = ((Context)comp).createSubcontext("env"); + try + { + env.lookup("ch"); + fail("java:comp/env visible from another context!"); + } + catch (NameNotFoundException e) + { + //expected + } + } + catch (Exception e) + { + throw new IllegalStateException(e); + } + } + + public void contextDestroyed(ServletContextEvent sce) + { + try + { + assertNotNull(comp); + comp.destroySubcontext("env"); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } } }); - */ + //make the new context the current one + ch2.start(); + } + finally + { + ch.stop(); + ch2.stop(); + Thread.currentThread().setContextClassLoader(currentLoader); + } + } + + @Test + public void testJavaNameParsing() throws Exception + { + Thread currentThread = Thread.currentThread(); + ClassLoader currentLoader = currentThread.getContextClassLoader(); + ClassLoader childLoader1 = new URLClassLoader(new URL[0], currentLoader); + + + //set the current thread's classloader + currentThread.setContextClassLoader(childLoader1); - - //set the current thread's classloader - currentThread.setContextClassLoader(childLoader1); - - InitialContext initCtxA = new InitialContext(); - initCtxA.bind ("blah", "123"); - assertEquals ("123", initCtxA.lookup("blah")); - - initCtxA.destroySubcontext("blah"); - try - { - initCtxA.lookup("blah"); - fail("context blah was not destroyed"); - } - catch (NameNotFoundException e) - { - //expected - } - - + try + { InitialContext initCtx = new InitialContext(); Context sub0 = (Context)initCtx.lookup("java:"); @@ -181,10 +253,74 @@ public class TestJNDI Context fee = ncontext.createSubcontext("fee"); fee.bind ("fi", "88"); - assertEquals("88", initCtxA.lookup("java:/fee/fi")); - assertEquals("88", initCtxA.lookup("java:/fee/fi/")); - assertTrue (initCtxA.lookup("java:/fee/") instanceof javax.naming.Context); + assertEquals("88", initCtx.lookup("java:/fee/fi")); + assertEquals("88", initCtx.lookup("java:/fee/fi/")); + assertTrue (initCtx.lookup("java:/fee/") instanceof javax.naming.Context); + } + finally + { + InitialContext ic = new InitialContext(); + Context java = (Context)ic.lookup("java:"); + java.destroySubcontext("fee"); + currentThread.setContextClassLoader(currentLoader); + } + } + + + @Test + public void testIt() throws Exception + { + //set up some classloaders + Thread currentThread = Thread.currentThread(); + ClassLoader currentLoader = currentThread.getContextClassLoader(); + ClassLoader childLoader1 = new URLClassLoader(new URL[0], currentLoader); + ClassLoader childLoader2 = new URLClassLoader(new URL[0], currentLoader); + + try + { + + //Uncomment to aid with debug + /* + javaRootURLContext.getRoot().addListener(new NamingContext.Listener() + { + public void unbind(NamingContext ctx, Binding binding) + { + System.err.println("java unbind "+binding+" from "+ctx.getName()); + } + + public Binding bind(NamingContext ctx, Binding binding) + { + System.err.println("java bind "+binding+" to "+ctx.getName()); + return binding; + } + }); + + localContextRoot.getRoot().addListener(new NamingContext.Listener() + { + public void unbind(NamingContext ctx, Binding binding) + { + System.err.println("local unbind "+binding+" from "+ctx.getName()); + } + + public Binding bind(NamingContext ctx, Binding binding) + { + System.err.println("local bind "+binding+" to "+ctx.getName()); + return binding; + } + }); + */ + + //Set up the tccl before doing any jndi operations + currentThread.setContextClassLoader(childLoader1); + InitialContext initCtx = new InitialContext(); + + //Test we can lookup the root java: naming tree + Context sub0 = (Context)initCtx.lookup("java:"); + assertNotNull(sub0); + + //Test that we cannot bind java:comp as it should + //already be bound try { Context sub1 = sub0.createSubcontext ("comp"); @@ -197,8 +333,10 @@ public class TestJNDI //check bindings at comp Context sub1 = (Context)initCtx.lookup("java:comp"); + assertNotNull(sub1); Context sub2 = sub1.createSubcontext ("env"); + assertNotNull(sub2); initCtx.bind ("java:comp/env/rubbish", "abc"); assertEquals ("abc", initCtx.lookup("java:comp/env/rubbish")); @@ -302,7 +440,6 @@ public class TestJNDI } - //test what happens when you close an initial context that was used initCtx.close(); } finally @@ -317,61 +454,7 @@ public class TestJNDI comp.destroySubcontext("env"); comp.unbind("crud"); comp.unbind("crud2"); - } - } - - - @Test - public void testParent() - throws Exception - { - //set up some classloaders - Thread currentThread = Thread.currentThread(); - ClassLoader parentLoader = currentThread.getContextClassLoader(); - ClassLoader childLoader1 = new URLClassLoader(new URL[0], parentLoader); - - try - { - //Test creating a comp for the parent loader does not leak to child - InitialContext initCtx = new InitialContext(); - Context comp = (Context)initCtx.lookup("java:comp"); - assertNotNull(comp); - - Context env = (Context)comp.createSubcontext("env"); - assertNotNull(env); - - env.bind("foo", "aaabbbcccddd"); - assertEquals("aaabbbcccddd", (String)initCtx.lookup("java:comp/env/foo")); - - //Change to child loader - currentThread.setContextClassLoader(childLoader1); - comp = (Context)initCtx.lookup("java:comp"); - - Context childEnv = (Context)comp.createSubcontext("env"); - assertNotSame(env, childEnv); - - childEnv.bind("foo", "eeefffggghhh"); - assertEquals("eeefffggghhh", (String)initCtx.lookup("java:comp/env/foo")); - - //Change back to parent - currentThread.setContextClassLoader(parentLoader); - assertEquals("aaabbbcccddd", (String)initCtx.lookup("java:comp/env/foo")); - - - } - finally - { - //make some effort to clean up - InitialContext ic = new InitialContext(); - currentThread.setContextClassLoader(parentLoader); - Context comp = (Context)ic.lookup("java:comp"); - comp.destroySubcontext("env"); - - currentThread.setContextClassLoader(childLoader1); - comp = (Context)ic.lookup("java:comp"); - comp.destroySubcontext("env"); - - + currentThread.setContextClassLoader(currentLoader); } } } diff --git a/jetty-jndi/src/test/java/org/eclipse/jetty/jndi/java/TestLocalJNDI.java b/jetty-jndi/src/test/java/org/eclipse/jetty/jndi/java/TestLocalJNDI.java index 489f2f7b6bb..7fd65502209 100644 --- a/jetty-jndi/src/test/java/org/eclipse/jetty/jndi/java/TestLocalJNDI.java +++ b/jetty-jndi/src/test/java/org/eclipse/jetty/jndi/java/TestLocalJNDI.java @@ -224,6 +224,15 @@ public class TestLocalJNDI assertEquals("333", (String)o); assertEquals("333", ic.lookup(name)); ic.destroySubcontext("a"); + try + { + ic.lookup("a"); + fail("context a was not destroyed"); + } + catch (NameNotFoundException e) + { + //expected + } name = parser.parse(""); name.add("x"); diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/OverlayConfig.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/OverlayConfig.java index d7ce310596e..e4bd0f851a1 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/OverlayConfig.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/OverlayConfig.java @@ -25,7 +25,7 @@ import java.util.List; import org.codehaus.plexus.util.xml.Xpp3Dom; -import edu.emory.mathcs.backport.java.util.Arrays; +import java.util.Arrays; /** * OverlayConfig diff --git a/jetty-server/src/main/config/etc/jetty-lowresources.xml b/jetty-server/src/main/config/etc/jetty-lowresources.xml new file mode 100644 index 00000000000..5b264f1749f --- /dev/null +++ b/jetty-server/src/main/config/etc/jetty-lowresources.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + 1000 + 200 + true + 0 + 0 + 5000 + + + + diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java index 8c7945c19f7..ca40a390df5 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java @@ -23,10 +23,13 @@ import java.net.Socket; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.Future; @@ -35,6 +38,7 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ssl.SslConnection; import org.eclipse.jetty.util.FutureCallback; import org.eclipse.jetty.util.annotation.ManagedAttribute; @@ -141,16 +145,19 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co private final Scheduler _scheduler; private final ByteBufferPool _byteBufferPool; private final Thread[] _acceptors; + private final Set _endpoints = Collections.newSetFromMap(new ConcurrentHashMap()); + private final Set _immutableEndPoints = Collections.unmodifiableSet(_endpoints); private volatile CountDownLatch _stopping; private long _idleTimeout = 30000; private String _defaultProtocol; private ConnectionFactory _defaultConnectionFactory; + /** * @param server The server this connector will be added to. Must not be null. * @param executor An executor for this connector or null to use the servers executor - * @param scheduler A scheduler for this connector or null to a new {@link TimerScheduler} instance. - * @param pool A buffer pool for this connector or null to use a default {@link ByteBufferPool} + * @param scheduler A scheduler for this connector or null to either a {@link Scheduler} set as a server bean or if none set, then a new {@link TimerScheduler} instance. + * @param pool A buffer pool for this connector or null to either a {@link ByteBufferPool} set as a server bean or none set, the new {@link ArrayByteBufferPool} instance. * @param acceptors the number of acceptor threads to use, or 0 for a default value. * @param factories The Connection Factories to use. */ @@ -164,7 +171,11 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co { _server=server; _executor=executor!=null?executor:_server.getThreadPool(); + if (scheduler==null) + scheduler=_server.getBean(Scheduler.class); _scheduler=scheduler!=null?scheduler:new TimerScheduler(); + if (pool==null) + pool=_server.getBean(ByteBufferPool.class); _byteBufferPool = pool!=null?pool:new ArrayByteBufferPool(); addBean(_server,false); @@ -468,6 +479,9 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co } } + + + // protected void connectionOpened(Connection connection) // { // _stats.connectionOpened(); @@ -488,6 +502,22 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co // newConnection.onOpen(); // } + @Override + public Collection getConnectedEndPoints() + { + return _immutableEndPoints; + } + + protected void onEndPointOpened(EndPoint endp) + { + _endpoints.add(endp); + } + + protected void onEndPointClosed(EndPoint endp) + { + _endpoints.remove(endp); + } + @Override public Scheduler getScheduler() { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Connector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Connector.java index 85e0708ff88..3454c7869ec 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Connector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Connector.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.concurrent.Executor; import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.component.Graceful; @@ -85,5 +86,8 @@ public interface Connector extends LifeCycle, Graceful */ public Object getTransport(); - + /** + * @return immutable collection of connected endpoints + */ + public Collection getConnectedEndPoints(); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java index b425007c76c..6d161def3a1 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java @@ -167,6 +167,7 @@ public class LocalConnector extends AbstractConnector LOG.debug("accepting {}", acceptorID); LocalEndPoint endPoint = _connects.take(); endPoint.onOpen(); + onEndPointOpened(endPoint); Connection connection = getDefaultConnectionFactory().newConnection(this, endPoint); endPoint.setConnection(connection); @@ -209,6 +210,7 @@ public class LocalConnector extends AbstractConnector @Override public void onClose() { + LocalConnector.this.onEndPointClosed(this); super.onClose(); _closed.countDown(); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/LowResourceMonitor.java b/jetty-server/src/main/java/org/eclipse/jetty/server/LowResourceMonitor.java new file mode 100644 index 00000000000..d4a1dbb0290 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/LowResourceMonitor.java @@ -0,0 +1,349 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.annotation.ManagedAttribute; +import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.annotation.Name; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.thread.Scheduler; +import org.eclipse.jetty.util.thread.ThreadPool; +import org.eclipse.jetty.util.thread.TimerScheduler; + + +/* ------------------------------------------------------------ */ +/** A monitor for low resources + *

An instance of this class will monitor all the connectors of a server (or a set of connectors + * configured with {@link #setMonitoredConnectors(Collection)}) for a low resources state. + * Low resources can be detected by:

    + *
  • {@link ThreadPool#isLowOnThreads()} if {@link Connector#getExecutor()} is + * an instance of {@link ThreadPool} and {@link #setMonitorThreads(boolean)} is true.
  • + *
  • If {@link #setMaxMemory(long)} is non zero then low resources is detected if the JVMs + * {@link Runtime} instance has {@link Runtime#totalMemory()} minus {@link Runtime#freeMemory()} + * greater than {@link #getMaxMemory()}
  • + *
  • If {@link #setMaxConnections(int)} is non zero then low resources is dected if the total number + * of connections exceeds {@link #getMaxConnections()}
  • + *
+ *

+ *

Once low resources state is detected, the cause is logged and all existing connections returned + * by {@link Connector#getConnectedEndPoints()} have {@link EndPoint#setIdleTimeout(long)} set + * to {@link #getLowResourcesIdleTimeout()}. New connections are not affected, however if the low + * resources state persists for more than {@link #getMaxLowResourcesTime()}, then the + * {@link #getLowResourcesIdleTimeout()} to all connections again. Once the low resources state is + * cleared, the idle timeout is reset to the connector default given by {@link Connector#getIdleTimeout()}. + *

+ */ +@ManagedObject ("Monitor for low resource conditions and activate a low resource mode if detected") +public class LowResourceMonitor extends AbstractLifeCycle +{ + private static final Logger LOG = Log.getLogger(LowResourceMonitor.class); + private final Server _server; + private Scheduler _scheduler; + private Connector[] _monitoredConnectors; + private int _period=1000; + private int _maxConnections; + private long _maxMemory; + private int _lowResourcesIdleTimeout=1000; + private int _maxLowResourcesTime=0; + private boolean _monitorThreads=true; + private final AtomicBoolean _low = new AtomicBoolean(); + private String _cause; + private String _reasons; + private long _lowStarted; + + + private final Runnable _monitor = new Runnable() + { + @Override + public void run() + { + if (isRunning()) + { + monitor(); + _scheduler.schedule(_monitor,_period,TimeUnit.MILLISECONDS); + } + } + }; + + public LowResourceMonitor(@Name("server") Server server) + { + _server=server; + } + + @ManagedAttribute("Are the monitored connectors low on resources?") + public boolean isLowOnResources() + { + return _low.get(); + } + + @ManagedAttribute("The reason(s) the monitored connectors are low on resources") + public String getLowResourcesReasons() + { + return _reasons; + } + + @ManagedAttribute("Get the timestamp in ms since epoch that low resources state started") + public long getLowResourcesStarted() + { + return _lowStarted; + } + + @ManagedAttribute("The monitored connectors. If null then all server connectors are monitored") + public Collection getMonitoredConnectors() + { + if (_monitoredConnectors==null) + return Collections.emptyList(); + return Arrays.asList(_monitoredConnectors); + } + + /** + * @param monitoredConnectors The collections of Connectors that should be monitored for low resources. + */ + public void setMonitoredConnectors(Collection monitoredConnectors) + { + if (monitoredConnectors==null || monitoredConnectors.size()==0) + _monitoredConnectors=null; + else + _monitoredConnectors = monitoredConnectors.toArray(new Connector[monitoredConnectors.size()]); + } + + @ManagedAttribute("The monitor period in ms") + public int getPeriod() + { + return _period; + } + + /** + * @param periodMS The period in ms to monitor for low resources + */ + public void setPeriod(int periodMS) + { + _period = periodMS; + } + + @ManagedAttribute("True if low available threads status is monitored") + public boolean getMonitorThreads() + { + return _monitorThreads; + } + + /** + * @param monitorThreads If true, check connectors executors to see if they are + * {@link ThreadPool} instances that are low on threads. + */ + public void setMonitorThreads(boolean monitorThreads) + { + _monitorThreads = monitorThreads; + } + + @ManagedAttribute("The maximum connections allowed for the monitored connectors before low resource handling is activated") + public int getMaxConnections() + { + return _maxConnections; + } + + /** + * @param maxConnections The maximum connections before low resources state is triggered + */ + public void setMaxConnections(int maxConnections) + { + _maxConnections = maxConnections; + } + + @ManagedAttribute("The maximum memory (in bytes) that can be used before low resources is triggered. Memory used is calculated as (totalMemory-freeMemory).") + public long getMaxMemory() + { + return _maxMemory; + } + + /** + * @param maxMemoryBytes The maximum memory in bytes in use before low resources is triggered. + */ + public void setMaxMemory(long maxMemoryBytes) + { + _maxMemory = maxMemoryBytes; + } + + @ManagedAttribute("The idletimeout in ms to apply to all existing connections when low resources is detected") + public int getLowResourcesIdleTimeout() + { + return _lowResourcesIdleTimeout; + } + + /** + * @param lowResourcesIdleTimeoutMS The timeout in ms to apply to EndPoints when in the low resources state. + */ + public void setLowResourcesIdleTimeout(int lowResourcesIdleTimeoutMS) + { + _lowResourcesIdleTimeout = lowResourcesIdleTimeoutMS; + } + + @ManagedAttribute("The maximum time in ms that low resources condition can persist before lowResourcesIdleTimeout is applied to new connections as well as existing connections") + public int getMaxLowResourcesTime() + { + return _maxLowResourcesTime; + } + + /** + * @param maxLowResourcesTimeMS The time in milliseconds that a low resource state can persist before the low resource idle timeout is reapplied to all connections + */ + public void setMaxLowResourcesTime(int maxLowResourcesTimeMS) + { + _maxLowResourcesTime = maxLowResourcesTimeMS; + } + + @Override + protected void doStart() throws Exception + { + _scheduler = _server.getBean(Scheduler.class); + + if (_scheduler==null) + { + _scheduler=new LRMScheduler(); + _scheduler.start(); + } + super.doStart(); + + _scheduler.schedule(_monitor,_period,TimeUnit.MILLISECONDS); + } + + @Override + protected void doStop() throws Exception + { + if (_scheduler instanceof LRMScheduler) + _scheduler.stop(); + super.doStop(); + } + + protected Connector[] getMonitoredOrServerConnectors() + { + if (_monitoredConnectors!=null && _monitoredConnectors.length>0) + return _monitoredConnectors; + return _server.getConnectors(); + } + + protected void monitor() + { + String reasons=null; + String cause=""; + int connections=0; + + for(Connector connector : getMonitoredOrServerConnectors()) + { + connections+=connector.getConnectedEndPoints().size(); + + Executor executor = connector.getExecutor(); + if (executor instanceof ThreadPool) + { + ThreadPool threadpool=(ThreadPool) executor; + if (_monitorThreads && threadpool.isLowOnThreads()) + { + reasons=low(reasons,"Low on threads: "+threadpool); + cause+="T"; + } + } + } + + if (_maxConnections>0 && connections>_maxConnections) + { + reasons=low(reasons,"Max Connections exceeded: "+connections+">"+_maxConnections); + cause+="C"; + } + + long memory=Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory(); + if (_maxMemory>0 && memory>_maxMemory) + { + reasons=low(reasons,"Max memory exceeded: "+memory+">"+_maxMemory); + cause+="M"; + } + + + if (reasons!=null) + { + // Log the reasons if there is any change in the cause + if (!cause.equals(_cause)) + { + LOG.warn("Low Resources: {}",reasons); + _cause=cause; + } + + // Enter low resources state? + if (_low.compareAndSet(false,true)) + { + _reasons=reasons; + _lowStarted=System.currentTimeMillis(); + setLowResources(); + } + + // Too long in low resources state? + if (_maxLowResourcesTime>0 && (System.currentTimeMillis()-_lowStarted)>_maxLowResourcesTime) + setLowResources(); + } + else + { + if (_low.compareAndSet(true,false)) + { + LOG.info("Low Resources cleared"); + _reasons=null; + _lowStarted=0; + clearLowResources(); + } + } + } + + protected void setLowResources() + { + for(Connector connector : getMonitoredOrServerConnectors()) + { + for (EndPoint endPoint : connector.getConnectedEndPoints()) + endPoint.setIdleTimeout(_lowResourcesIdleTimeout); + } + } + + protected void clearLowResources() + { + for(Connector connector : getMonitoredOrServerConnectors()) + { + for (EndPoint endPoint : connector.getConnectedEndPoints()) + endPoint.setIdleTimeout(connector.getIdleTimeout()); + } + } + + private String low(String reasons, String newReason) + { + if (reasons==null) + return newReason; + return reasons+", "+newReason; + } + + + private static class LRMScheduler extends TimerScheduler + { + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java index 8ffea32f88f..eadb2f413c9 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java @@ -400,5 +400,21 @@ public class ServerConnector extends AbstractNetworkConnector { return getDefaultConnectionFactory().newConnection(ServerConnector.this, endpoint); } + + @Override + protected void endPointOpened(EndPoint endpoint) + { + super.endPointOpened(endpoint); + onEndPointOpened(endpoint); + } + + @Override + protected void endPointClosed(EndPoint endpoint) + { + onEndPointClosed(endpoint); + super.endPointClosed(endpoint); + } + + } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/LowResourcesMonitorTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/LowResourcesMonitorTest.java new file mode 100644 index 00000000000..365a85b0798 --- /dev/null +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/LowResourcesMonitorTest.java @@ -0,0 +1,197 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server; + + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; + +import java.net.Socket; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; + +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.TimerScheduler; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +public class LowResourcesMonitorTest +{ + QueuedThreadPool _threadPool; + Server _server; + ServerConnector _connector; + LowResourceMonitor _lowResourcesMonitor; + + @Before + public void before() throws Exception + { + _threadPool = new QueuedThreadPool(); + _threadPool.setMaxThreads(50); + + _server = new Server(_threadPool); + _server.manage(_threadPool); + + _server.addBean(new TimerScheduler()); + + _connector = new ServerConnector(_server); + _connector.setPort(0); + _connector.setIdleTimeout(35000); + _server.addConnector(_connector); + + _server.setHandler(new DumpHandler()); + + _lowResourcesMonitor=new LowResourceMonitor(_server); + _lowResourcesMonitor.setLowResourcesIdleTimeout(200); + _lowResourcesMonitor.setMaxConnections(20); + _lowResourcesMonitor.setPeriod(900); + _server.addBean(_lowResourcesMonitor); + + _server.start(); + } + + @After + public void after() throws Exception + { + _server.stop(); + } + + + @Test + public void testLowOnThreads() throws Exception + { + _threadPool.setMaxThreads(_threadPool.getThreads()-_threadPool.getIdleThreads()+10); + Thread.sleep(1200); + Assert.assertFalse(_lowResourcesMonitor.isLowOnResources()); + + final CountDownLatch latch = new CountDownLatch(1); + + for (int i=0;i<20;i++) + { + _threadPool.dispatch(new Runnable() + { + @Override + public void run() + { + try + { + latch.await(); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + } + }); + } + + Thread.sleep(1200); + Assert.assertTrue(_lowResourcesMonitor.isLowOnResources()); + + latch.countDown(); + Thread.sleep(1200); + System.err.println(_threadPool.dump()); + Assert.assertFalse(_lowResourcesMonitor.isLowOnResources()); + } + + + @Ignore ("not reliable") + @Test + public void testLowOnMemory() throws Exception + { + _lowResourcesMonitor.setMaxMemory(Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory()+(100*1024*1024)); + Thread.sleep(1200); + Assert.assertFalse(_lowResourcesMonitor.isLowOnResources()); + + byte[] data = new byte[100*1024*1024]; + Arrays.fill(data,(byte)1); + int hash = Arrays.hashCode(data); + assertThat(hash,not(equalTo(0))); + + Thread.sleep(1200); + Assert.assertTrue(_lowResourcesMonitor.isLowOnResources()); + data=null; + System.gc(); + System.gc(); + + Thread.sleep(1200); + Assert.assertFalse(_lowResourcesMonitor.isLowOnResources()); + } + + + @Test + public void testMaxConnectionsAndMaxIdleTime() throws Exception + { + _lowResourcesMonitor.setMaxMemory(0); + Assert.assertFalse(_lowResourcesMonitor.isLowOnResources()); + + Socket[] socket = new Socket[_lowResourcesMonitor.getMaxConnections()+1]; + for (int i=0;i _methods=new HashSet(); protected Set _excludedAgents; protected Set _excludedAgentPatterns; protected Set _excludedPaths; @@ -166,6 +170,16 @@ public class GzipFilter extends UserAgentFilter if (tmp!=null) _deflateNoWrap=Boolean.parseBoolean(tmp); + tmp=filterConfig.getInitParameter("methods"); + if (tmp!=null) + { + StringTokenizer tok = new StringTokenizer(tmp,",",false); + while (tok.hasMoreTokens()) + _methods.add(tok.nextToken().trim().toUpperCase()); + } + else + _methods.add(HttpMethod.GET.asString()); + tmp=filterConfig.getInitParameter("mimeTypes"); if (tmp!=null) { @@ -235,9 +249,9 @@ public class GzipFilter extends UserAgentFilter HttpServletRequest request=(HttpServletRequest)req; HttpServletResponse response=(HttpServletResponse)res; - // If not a GET or an Excluded URI - no Vary because no matter what client, this URI is always excluded + // If not a supported method or it is an Excluded URI - no Vary because no matter what client, this URI is always excluded String requestURI = request.getRequestURI(); - if (!HttpMethod.GET.is(request.getMethod()) || isExcludedPath(requestURI)) + if (!_methods.contains(request.getMethod()) || isExcludedPath(requestURI)) { super.doFilter(request,response,chain); return; diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipFilterContentLengthTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipFilterContentLengthTest.java index a9ca8352901..b033f33f7ac 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipFilterContentLengthTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipFilterContentLengthTest.java @@ -112,7 +112,7 @@ public class GzipFilterContentLengthTest try { tester.start(); - tester.assertIsResponseGzipCompressed(testfile.getName()); + tester.assertIsResponseGzipCompressed("GET",testfile.getName()); } finally { @@ -132,7 +132,7 @@ public class GzipFilterContentLengthTest try { tester.start(); - tester.assertIsResponseNotGzipCompressed(testfile.getName(),filesize,HttpStatus.OK_200); + tester.assertIsResponseNotGzipCompressed("GET",testfile.getName(),filesize,HttpStatus.OK_200); } finally { diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipFilterDefaultTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipFilterDefaultTest.java index 7b28572e670..055dc921c5c 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipFilterDefaultTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipFilterDefaultTest.java @@ -103,6 +103,50 @@ public class GzipFilterDefaultTest @Rule public TestingDir testingdir = new TestingDir(); + + + @Test + public void testIsGzipByMethod() throws Exception + { + GzipTester tester = new GzipTester(testingdir, compressionType); + + // Test content that is smaller than the buffer. + int filesize = CompressedResponseWrapper.DEFAULT_BUFFER_SIZE * 2; + tester.prepareServerFile("file.txt",filesize); + + FilterHolder holder = tester.setContentServlet(GetServlet.class); + holder.setInitParameter("mimeTypes","text/plain"); + holder.setInitParameter("methods","POST,WIBBLE"); + + try + { + tester.start(); + tester.assertIsResponseGzipCompressed("POST","file.txt"); + tester.assertIsResponseGzipCompressed("WIBBLE","file.txt"); + tester.assertIsResponseNotGzipCompressed("GET","file.txt",filesize,200); + } + finally + { + tester.stop(); + } + } + + public static class GetServlet extends DefaultServlet + { + public GetServlet() + { + super(); + } + + @Override + public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException,ServletException + { + doGet(req,resp); + } + } + + + @Test public void testIsGzipCompressedTiny() throws Exception { @@ -118,7 +162,7 @@ public class GzipFilterDefaultTest try { tester.start(); - HttpTester.Response http = tester.assertIsResponseGzipCompressed("file.txt"); + HttpTester.Response http = tester.assertIsResponseGzipCompressed("GET","file.txt"); Assert.assertEquals("Accept-Encoding",http.get("Vary")); } finally @@ -142,7 +186,7 @@ public class GzipFilterDefaultTest try { tester.start(); - HttpTester.Response http = tester.assertIsResponseGzipCompressed("file.txt"); + HttpTester.Response http = tester.assertIsResponseGzipCompressed("GET","file.txt"); Assert.assertEquals("Accept-Encoding",http.get("Vary")); } finally @@ -166,7 +210,7 @@ public class GzipFilterDefaultTest try { tester.start(); - HttpTester.Response http = tester.assertIsResponseGzipCompressed("file.txt"); + HttpTester.Response http = tester.assertIsResponseGzipCompressed("GET","file.txt"); Assert.assertEquals("Accept-Encoding",http.get("Vary")); } finally @@ -190,7 +234,7 @@ public class GzipFilterDefaultTest try { tester.start(); - HttpTester.Response http = tester.assertIsResponseGzipCompressed("file.txt"); + HttpTester.Response http = tester.assertIsResponseGzipCompressed("GET","file.txt"); Assert.assertEquals("Accept-Encoding",http.get("Vary")); } finally @@ -213,7 +257,7 @@ public class GzipFilterDefaultTest try { tester.start(); - HttpTester.Response http = tester.assertIsResponseNotGzipCompressed("file.txt", filesize, HttpStatus.OK_200); + HttpTester.Response http = tester.assertIsResponseNotGzipCompressed("GET","file.txt", filesize, HttpStatus.OK_200); Assert.assertEquals("Accept-Encoding",http.get("Vary")); } finally @@ -236,7 +280,7 @@ public class GzipFilterDefaultTest try { tester.start(); - HttpTester.Response http = tester.assertIsResponseNotGzipCompressed("file.mp3", filesize, HttpStatus.OK_200); + HttpTester.Response http = tester.assertIsResponseNotGzipCompressed("GET","file.mp3", filesize, HttpStatus.OK_200); Assert.assertNull(http.get("Vary")); } finally @@ -257,7 +301,7 @@ public class GzipFilterDefaultTest try { tester.start(); - tester.assertIsResponseNotGzipCompressed(-1, 204); + tester.assertIsResponseNotGzipCompressed("GET",-1, 204); } finally { @@ -278,7 +322,7 @@ public class GzipFilterDefaultTest try { tester.start(); - tester.assertIsResponseNotGzipCompressedAndEqualToExpectedString("error message", -1, 400); + tester.assertIsResponseNotGzipCompressedAndEqualToExpectedString("GET","error message", -1, 400); } finally { @@ -302,7 +346,7 @@ public class GzipFilterDefaultTest try { tester.start(); - tester.assertIsResponseNotGzipCompressed("file.txt",filesize,HttpStatus.OK_200); + tester.assertIsResponseNotGzipCompressed("GET","file.txt",filesize,HttpStatus.OK_200); } finally { @@ -326,7 +370,7 @@ public class GzipFilterDefaultTest try { tester.start(); - tester.assertIsResponseNotGzipCompressed("file.txt",filesize,HttpStatus.OK_200); + tester.assertIsResponseNotGzipCompressed("GET","file.txt",filesize,HttpStatus.OK_200); } finally { @@ -348,7 +392,7 @@ public class GzipFilterDefaultTest try { tester.start(); - tester.assertIsResponseNotGzipCompressed("file.txt",filesize,HttpStatus.OK_200); + tester.assertIsResponseNotGzipCompressed("GET","file.txt",filesize,HttpStatus.OK_200); } finally { @@ -370,7 +414,7 @@ public class GzipFilterDefaultTest try { tester.start(); - tester.assertIsResponseNotGzipCompressed("file.txt",filesize,HttpStatus.OK_200); + tester.assertIsResponseNotGzipCompressed("GET","file.txt",filesize,HttpStatus.OK_200); } finally { diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludableGzipFilterMinSizeTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludableGzipFilterMinSizeTest.java index 03681a93306..a899d1a4d11 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludableGzipFilterMinSizeTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludableGzipFilterMinSizeTest.java @@ -107,7 +107,7 @@ public class IncludableGzipFilterMinSizeTest try { tester.start(); - tester.assertIsResponseGzipCompressed("big_script.js"); + tester.assertIsResponseGzipCompressed("GET","big_script.js"); } finally { tester.stop(); } diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/gzip/GzipTester.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/gzip/GzipTester.java index a40a8372e2e..8138022a950 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/gzip/GzipTester.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/gzip/GzipTester.java @@ -74,18 +74,18 @@ public class GzipTester // DOES NOT WORK IN WINDOWS - this.testdir.ensureEmpty(); } - public HttpTester.Response assertIsResponseGzipCompressed(String filename) throws Exception + public HttpTester.Response assertIsResponseGzipCompressed(String method, String filename) throws Exception { - return assertIsResponseGzipCompressed(filename,filename); + return assertIsResponseGzipCompressed(method,filename,filename); } - public HttpTester.Response assertIsResponseGzipCompressed(String requestedFilename, String serverFilename) throws Exception + public HttpTester.Response assertIsResponseGzipCompressed(String method, String requestedFilename, String serverFilename) throws Exception { // System.err.printf("[GzipTester] requesting /context/%s%n",requestedFilename); HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; - request.setMethod("GET"); + request.setMethod(method); request.setVersion("HTTP/1.0"); request.setHeader("Host","tester"); request.setHeader("Accept-Encoding",compressionType); @@ -238,10 +238,10 @@ public class GzipTester * passing -1 will disable the Content-Length assertion) * @throws Exception */ - public HttpTester.Response assertIsResponseNotGzipCompressed(String filename, int expectedFilesize, int status) throws Exception + public HttpTester.Response assertIsResponseNotGzipCompressed(String method, String filename, int expectedFilesize, int status) throws Exception { String uri = "/context/"+filename; - HttpTester.Response response = executeRequest(uri); + HttpTester.Response response = executeRequest(method,uri); assertResponseHeaders(expectedFilesize,status,response); // Assert that the contents are what we expect. @@ -269,10 +269,10 @@ public class GzipTester * passing -1 will disable the Content-Length assertion) * @throws Exception */ - public void assertIsResponseNotGzipCompressedAndEqualToExpectedString(String expectedResponse, int expectedFilesize, int status) throws Exception + public void assertIsResponseNotGzipCompressedAndEqualToExpectedString(String method,String expectedResponse, int expectedFilesize, int status) throws Exception { String uri = "/context/"; - HttpTester.Response response = executeRequest(uri); + HttpTester.Response response = executeRequest(method,uri); assertResponseHeaders(expectedFilesize,status,response); String actual = readResponse(response); @@ -288,10 +288,10 @@ public class GzipTester * passing -1 will disable the Content-Length assertion) * @throws Exception */ - public void assertIsResponseNotGzipCompressed(int expectedFilesize, int status) throws Exception + public void assertIsResponseNotGzipCompressed(String method,int expectedFilesize, int status) throws Exception { String uri = "/context/"; - HttpTester.Response response = executeRequest(uri); + HttpTester.Response response = executeRequest(method,uri); assertResponseHeaders(expectedFilesize,status,response); } @@ -312,13 +312,13 @@ public class GzipTester Assert.assertThat("Response.header[Content-Encoding]",response.get("Content-Encoding"),not(containsString(compressionType))); } - private HttpTester.Response executeRequest(String uri) throws IOException, Exception + private HttpTester.Response executeRequest(String method, String uri) throws IOException, Exception { //System.err.printf("[GzipTester] requesting %s%n",uri); HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; - request.setMethod("GET"); + request.setMethod(method); request.setVersion("HTTP/1.0"); request.setHeader("Host","tester"); request.setHeader("Accept-Encoding",compressionType); diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/IStream.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/IStream.java index 53f0671d9ca..fc6c4d96bfc 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/IStream.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/IStream.java @@ -59,6 +59,9 @@ public interface IStream extends Stream, Callback */ public void setStreamFrameListener(StreamFrameListener listener); + //TODO: javadoc thomas + public StreamFrameListener getStreamFrameListener(); + /** *

A stream can be open, {@link #isHalfClosed() half closed} or * {@link #isClosed() closed} and this method updates the close state diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardSession.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardSession.java index 704c595e979..dcce655edeb 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardSession.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardSession.java @@ -45,9 +45,10 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.spdy.api.ByteBufferDataInfo; import org.eclipse.jetty.spdy.api.DataInfo; import org.eclipse.jetty.spdy.api.GoAwayInfo; -import org.eclipse.jetty.spdy.api.GoAwayReceivedInfo; +import org.eclipse.jetty.spdy.api.GoAwayResultInfo; import org.eclipse.jetty.spdy.api.PingInfo; import org.eclipse.jetty.spdy.api.PingResultInfo; +import org.eclipse.jetty.spdy.api.PushInfo; import org.eclipse.jetty.spdy.api.RstInfo; import org.eclipse.jetty.spdy.api.SPDYException; import org.eclipse.jetty.spdy.api.Session; @@ -498,8 +499,17 @@ public class StandardSession implements ISession, Parser.Listener, Dumpable stream.process(frame); // Update the last stream id before calling the application (which may send a GO_AWAY) updateLastStreamId(stream); - SynInfo synInfo = new SynInfo(frame.getHeaders(), frame.isClose(), frame.getPriority()); - StreamFrameListener streamListener = notifyOnSyn(listener, stream, synInfo); + StreamFrameListener streamListener; + if (stream.isUnidirectional()) + { + PushInfo pushInfo = new PushInfo(frame.getHeaders(), frame.isClose()); + streamListener = notifyOnPush(stream.getAssociatedStream().getStreamFrameListener(), stream, pushInfo); + } + else + { + SynInfo synInfo = new SynInfo(frame.getHeaders(), frame.isClose(), frame.getPriority()); + streamListener = notifyOnSyn(listener, stream, synInfo); + } stream.setStreamFrameListener(streamListener); flush(); // The onSyn() listener may have sent a frame that closed the stream @@ -680,9 +690,9 @@ public class StandardSession implements ISession, Parser.Listener, Dumpable { if (goAwayReceived.compareAndSet(false, true)) { - //TODO: Find a better name for GoAwayReceivedInfo - GoAwayReceivedInfo goAwayReceivedInfo = new GoAwayReceivedInfo(frame.getLastStreamId(), SessionStatus.from(frame.getStatusCode())); - notifyOnGoAway(listener, goAwayReceivedInfo); + //TODO: Find a better name for GoAwayResultInfo + GoAwayResultInfo goAwayResultInfo = new GoAwayResultInfo(frame.getLastStreamId(), SessionStatus.from(frame.getStatusCode())); + notifyOnGoAway(listener, goAwayResultInfo); flush(); // SPDY does not require to send back a response to a GO_AWAY. // We notified the application of the last good stream id and @@ -755,6 +765,27 @@ public class StandardSession implements ISession, Parser.Listener, Dumpable } } + private StreamFrameListener notifyOnPush(StreamFrameListener listener, Stream stream, PushInfo pushInfo) + { + try + { + if (listener == null) + return null; + LOG.debug("Invoking callback with {} on listener {}", pushInfo, listener); + return listener.onPush(stream, pushInfo); + } + catch (Exception x) + { + LOG.info("Exception while notifying listener " + listener, x); + return null; + } + catch (Error x) + { + LOG.info("Exception while notifying listener " + listener, x); + throw x; + } + } + private StreamFrameListener notifyOnSyn(SessionFrameListener listener, Stream stream, SynInfo synInfo) { try @@ -839,14 +870,14 @@ public class StandardSession implements ISession, Parser.Listener, Dumpable } } - private void notifyOnGoAway(SessionFrameListener listener, GoAwayReceivedInfo goAwayReceivedInfo) + private void notifyOnGoAway(SessionFrameListener listener, GoAwayResultInfo goAwayResultInfo) { try { if (listener != null) { - LOG.debug("Invoking callback with {} on listener {}", goAwayReceivedInfo, listener); - listener.onGoAway(this, goAwayReceivedInfo); + LOG.debug("Invoking callback with {} on listener {}", goAwayResultInfo, listener); + listener.onGoAway(this, goAwayResultInfo); } } catch (Exception x) diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardStream.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardStream.java index 78ee9d51599..7e9afac7a0e 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardStream.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardStream.java @@ -148,6 +148,7 @@ public class StandardStream implements IStream this.listener = listener; } + @Override public StreamFrameListener getStreamFrameListener() { return listener; diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/GoAwayReceivedInfo.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/GoAwayResultInfo.java similarity index 87% rename from jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/GoAwayReceivedInfo.java rename to jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/GoAwayResultInfo.java index ef50de19fcf..eb75f886785 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/GoAwayReceivedInfo.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/GoAwayResultInfo.java @@ -22,18 +22,18 @@ package org.eclipse.jetty.spdy.api; *

A container for GOAWAY frames metadata: the last good stream id and * the session status.

*/ -public class GoAwayReceivedInfo +public class GoAwayResultInfo { private final int lastStreamId; private final SessionStatus sessionStatus; /** - *

Creates a new {@link GoAwayReceivedInfo} with the given last good stream id and session status

+ *

Creates a new {@link GoAwayResultInfo} with the given last good stream id and session status

* * @param lastStreamId the last good stream id * @param sessionStatus the session status */ - public GoAwayReceivedInfo(int lastStreamId, SessionStatus sessionStatus) + public GoAwayResultInfo(int lastStreamId, SessionStatus sessionStatus) { this.lastStreamId = lastStreamId; this.sessionStatus = sessionStatus; diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/SessionFrameListener.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/SessionFrameListener.java index b950698d977..ffff51fe50d 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/SessionFrameListener.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/SessionFrameListener.java @@ -36,7 +36,7 @@ public interface SessionFrameListener extends EventListener *

Application code should implement this method and reply to the stream creation, eventually * sending data:

*
-     * public Stream.FrameListener onSyn(Stream stream, SynInfo synInfo)
+     * public StreamFrameListener onSyn(Stream stream, SynInfo synInfo)
      * {
      *     // Do something with the metadata contained in synInfo
      *
@@ -52,7 +52,7 @@ public interface SessionFrameListener extends EventListener
      * 
*

Alternatively, if the stream creation requires reading data sent from the other peer:

*
-     * public Stream.FrameListener onSyn(Stream stream, SynInfo synInfo)
+     * public StreamFrameListener onSyn(Stream stream, SynInfo synInfo)
      * {
      *     // Do something with the metadata contained in synInfo
      *
@@ -106,9 +106,9 @@ public interface SessionFrameListener extends EventListener
      * 

Callback invoked when the other peer signals that it is closing the connection.

* * @param session the session - * @param goAwayReceivedInfo the metadata sent + * @param goAwayResultInfo the metadata sent */ - public void onGoAway(Session session, GoAwayReceivedInfo goAwayReceivedInfo); + public void onGoAway(Session session, GoAwayResultInfo goAwayResultInfo); /** *

Callback invoked when an exception is thrown during the processing of an event on a @@ -119,6 +119,7 @@ public interface SessionFrameListener extends EventListener */ public void onException(Throwable x); + /** *

Empty implementation of {@link SessionFrameListener}

*/ @@ -148,7 +149,7 @@ public interface SessionFrameListener extends EventListener } @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayReceivedInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayResultInfo) { } diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/StreamFrameListener.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/StreamFrameListener.java index 6a91fc77f67..0a4248a1f97 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/StreamFrameListener.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/StreamFrameListener.java @@ -50,6 +50,15 @@ public interface StreamFrameListener extends EventListener */ public void onHeaders(Stream stream, HeadersInfo headersInfo); + /** + *

Callback invoked when a push syn has been received on a stream.

+ * + * @param stream the push stream just created + * @param pushInfo + * @return a listener for stream events or null if there is no interest in being notified of stream events + */ + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo); + /** *

Callback invoked when data bytes are received on a stream.

*

Implementers should be read or consume the content of the @@ -75,6 +84,12 @@ public interface StreamFrameListener extends EventListener { } + @Override + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) + { + return null; + } + @Override public void onData(Stream stream, DataInfo dataInfo) { diff --git a/jetty-spdy/spdy-core/src/test/java/org/eclipse/jetty/spdy/api/ClientUsageTest.java b/jetty-spdy/spdy-core/src/test/java/org/eclipse/jetty/spdy/api/ClientUsageTest.java index ebb98d63666..455b58e0fd7 100644 --- a/jetty-spdy/spdy-core/src/test/java/org/eclipse/jetty/spdy/api/ClientUsageTest.java +++ b/jetty-spdy/spdy-core/src/test/java/org/eclipse/jetty/spdy/api/ClientUsageTest.java @@ -60,14 +60,24 @@ public class ClientUsageTest } @Test - public void testClientRequestWithBodyResponseNoBody() throws Exception + public void testClientReceivesPush1() throws InterruptedException, ExecutionException, TimeoutException { Session session = new StandardSession(SPDY.V2, null, null, null, null, null, null, 1, null, null, null); - Stream stream = session.syn(new SynInfo(5, TimeUnit.SECONDS, new Fields(), false, (byte)0), - new StreamFrameListener.Adapter() + session.syn(new SynInfo(new Fields(), true), new StreamFrameListener.Adapter() + { + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) + { + return new Adapter() { @Override + public void onData(Stream stream, DataInfo dataInfo) + { + } + }; + }; + + @Override public void onReply(Stream stream, ReplyInfo replyInfo) { // Do something with the response @@ -83,6 +93,71 @@ public class ClientUsageTest throw new IllegalStateException(e); } } + }); + } + + @Test + public void testClientReceivesPush2() throws InterruptedException, ExecutionException, TimeoutException + { + Session session = new StandardSession(SPDY.V2, null, null, null, null, null, null, 1, new SessionFrameListener.Adapter() + { + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) + { + return new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + } + }; + } + }, null, null); + + session.syn(new SynInfo(new Fields(), true), new StreamFrameListener.Adapter() + { + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + // Do something with the response + replyInfo.getHeaders().get("host"); + + // Then issue another similar request + try + { + stream.getSession().syn(new SynInfo(new Fields(), true), this); + } + catch (ExecutionException | InterruptedException | TimeoutException e) + { + throw new IllegalStateException(e); + } + } + }); + } + + @Test + public void testClientRequestWithBodyResponseNoBody() throws Exception + { + Session session = new StandardSession(SPDY.V2, null, null, null, null, null, null, 1, null, null, null); + + Stream stream = session.syn(new SynInfo(5, TimeUnit.SECONDS, new Fields(), false, (byte)0), + new StreamFrameListener.Adapter() + { + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + // Do something with the response + replyInfo.getHeaders().get("host"); + + // Then issue another similar request + try + { + stream.getSession().syn(new SynInfo(new Fields(), true), this); + } + catch (ExecutionException | InterruptedException | TimeoutException e) + { + throw new IllegalStateException(e); + } + } }); // Send-and-forget the data stream.data(new StringDataInfo("data", true)); @@ -96,38 +171,39 @@ public class ClientUsageTest final String context = "context"; session.syn(new SynInfo(new Fields(), false), new StreamFrameListener.Adapter() { - @Override - public void onReply(Stream stream, ReplyInfo replyInfo) - { - // Do something with the response - replyInfo.getHeaders().get("host"); + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + // Do something with the response + replyInfo.getHeaders().get("host"); - // Then issue another similar request - try - { - stream.getSession().syn(new SynInfo(new Fields(), true), this); - } - catch (ExecutionException | InterruptedException | TimeoutException e) - { - throw new IllegalStateException(e); - } - } + // Then issue another similar request + try + { + stream.getSession().syn(new SynInfo(new Fields(), true), this); + } + catch (ExecutionException | InterruptedException | TimeoutException e) + { + throw new IllegalStateException(e); + } + } }, new Promise.Adapter() { @Override - public void succeeded(Stream stream) - { - // Differently from JDK 7 AIO, there is no need to - // have an explicit parameter for the context since - // that is captured while the handler is created anyway, - // and it is used only by the handler as parameter + public void succeeded(Stream stream) + { + // Differently from JDK 7 AIO, there is no need to + // have an explicit parameter for the context since + // that is captured while the handler is created anyway, + // and it is used only by the handler as parameter - // The style below is fire-and-forget, since - // we do not pass the handler nor we call get() - // to wait for the data to be sent - stream.data(new StringDataInfo(context, true), new Callback.Adapter()); - } - }); + // The style below is fire-and-forget, since + // we do not pass the handler nor we call get() + // to wait for the data to be sent + stream.data(new StringDataInfo(context, true), new Callback.Adapter()); + } + } + ); } @Test @@ -136,48 +212,49 @@ public class ClientUsageTest Session session = new StandardSession(SPDY.V2, null, null, null, null, null, null, 1, null, null, null); session.syn(new SynInfo(new Fields(), false), new StreamFrameListener.Adapter() - { - // The good of passing the listener to push() is that applications can safely - // accumulate info from the reply headers to be used in the data callback, - // e.g. content-type, charset, etc. - - @Override - public void onReply(Stream stream, ReplyInfo replyInfo) - { - // Do something with the response - Fields headers = replyInfo.getHeaders(); - int contentLength = headers.get("content-length").valueAsInt(); - stream.setAttribute("content-length", contentLength); - if (!replyInfo.isClose()) - stream.setAttribute("builder", new StringBuilder()); - - // May issue another similar request while waiting for data - try { - stream.getSession().syn(new SynInfo(new Fields(), true), this); - } - catch (ExecutionException | InterruptedException | TimeoutException e) + // The good of passing the listener to push() is that applications can safely + // accumulate info from the reply headers to be used in the data callback, + // e.g. content-type, charset, etc. + + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + // Do something with the response + Fields headers = replyInfo.getHeaders(); + int contentLength = headers.get("content-length").valueAsInt(); + stream.setAttribute("content-length", contentLength); + if (!replyInfo.isClose()) + stream.setAttribute("builder", new StringBuilder()); + + // May issue another similar request while waiting for data + try + { + stream.getSession().syn(new SynInfo(new Fields(), true), this); + } + catch (ExecutionException | InterruptedException | TimeoutException e) + { + throw new IllegalStateException(e); + } + } + + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + StringBuilder builder = (StringBuilder)stream.getAttribute("builder"); + builder.append(dataInfo.asString("UTF-8", true)); + + } + }, new Promise.Adapter() { - throw new IllegalStateException(e); + @Override + public void succeeded(Stream stream) + { + stream.data(new BytesDataInfo("wee".getBytes(Charset.forName("UTF-8")), false), new Callback.Adapter()); + stream.data(new StringDataInfo("foo", false), new Callback.Adapter()); + stream.data(new ByteBufferDataInfo(Charset.forName("UTF-8").encode("bar"), true), new Callback.Adapter()); + } } - } - - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - StringBuilder builder = (StringBuilder)stream.getAttribute("builder"); - builder.append(dataInfo.asString("UTF-8", true)); - - } - }, new Promise.Adapter() - { - @Override - public void succeeded(Stream stream) - { - stream.data(new BytesDataInfo("wee".getBytes(Charset.forName("UTF-8")), false), new Callback.Adapter()); - stream.data(new StringDataInfo("foo", false), new Callback.Adapter()); - stream.data(new ByteBufferDataInfo(Charset.forName("UTF-8").encode("bar"), true), new Callback.Adapter()); - } - }); + ); } } diff --git a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HTTPSPDYServerConnectionFactory.java b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HTTPSPDYServerConnectionFactory.java index 395f9dcf3e2..1569f749150 100644 --- a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HTTPSPDYServerConnectionFactory.java +++ b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HTTPSPDYServerConnectionFactory.java @@ -24,6 +24,7 @@ import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.spdy.api.DataInfo; import org.eclipse.jetty.spdy.api.HeadersInfo; +import org.eclipse.jetty.spdy.api.PushInfo; import org.eclipse.jetty.spdy.api.ReplyInfo; import org.eclipse.jetty.spdy.api.Stream; import org.eclipse.jetty.spdy.api.StreamFrameListener; @@ -131,6 +132,12 @@ public class HTTPSPDYServerConnectionFactory extends SPDYServerConnectionFactory channel.requestHeaders(headersInfo.getHeaders(), headersInfo.isClose()); } + @Override + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) + { + return null; + } + @Override public void onData(Stream stream, final DataInfo dataInfo) { diff --git a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyEngineSelector.java b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyEngineSelector.java index 75b6a4e5062..452e32b95eb 100644 --- a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyEngineSelector.java +++ b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyEngineSelector.java @@ -23,7 +23,7 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.eclipse.jetty.spdy.api.GoAwayReceivedInfo; +import org.eclipse.jetty.spdy.api.GoAwayResultInfo; import org.eclipse.jetty.spdy.api.PingResultInfo; import org.eclipse.jetty.spdy.api.RstInfo; import org.eclipse.jetty.spdy.api.Session; @@ -104,7 +104,7 @@ public class ProxyEngineSelector extends ServerSessionFrameListener.Adapter } @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayReceivedInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayResultInfo) { // TODO: } diff --git a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPSPDYConnection.java b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPSPDYConnection.java index 0c1b9202087..7d5c8784026 100644 --- a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPSPDYConnection.java +++ b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPSPDYConnection.java @@ -42,7 +42,7 @@ import org.eclipse.jetty.spdy.StandardStream; import org.eclipse.jetty.spdy.api.ByteBufferDataInfo; import org.eclipse.jetty.spdy.api.DataInfo; import org.eclipse.jetty.spdy.api.GoAwayInfo; -import org.eclipse.jetty.spdy.api.GoAwayReceivedInfo; +import org.eclipse.jetty.spdy.api.GoAwayResultInfo; import org.eclipse.jetty.spdy.api.HeadersInfo; import org.eclipse.jetty.spdy.api.PushInfo; import org.eclipse.jetty.spdy.api.ReplyInfo; @@ -136,7 +136,7 @@ public class ProxyHTTPSPDYConnection extends HttpConnection implements HttpParse { assert content == null; if (headers.isEmpty()) - proxyEngineSelector.onGoAway(session, new GoAwayReceivedInfo(0, SessionStatus.OK)); + proxyEngineSelector.onGoAway(session, new GoAwayResultInfo(0, SessionStatus.OK)); else syn(true); } diff --git a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/SPDYProxyEngine.java b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/SPDYProxyEngine.java index 874402ecc34..67db8bff185 100644 --- a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/SPDYProxyEngine.java +++ b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/SPDYProxyEngine.java @@ -29,8 +29,9 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jetty.spdy.api.ByteBufferDataInfo; import org.eclipse.jetty.spdy.api.DataInfo; import org.eclipse.jetty.spdy.api.GoAwayInfo; -import org.eclipse.jetty.spdy.api.GoAwayReceivedInfo; +import org.eclipse.jetty.spdy.api.GoAwayResultInfo; import org.eclipse.jetty.spdy.api.HeadersInfo; +import org.eclipse.jetty.spdy.api.Info; import org.eclipse.jetty.spdy.api.PushInfo; import org.eclipse.jetty.spdy.api.ReplyInfo; import org.eclipse.jetty.spdy.api.RstInfo; @@ -50,8 +51,8 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; /** - *

{@link SPDYProxyEngine} implements a SPDY to SPDY proxy, that is, converts SPDY events received by - * clients into SPDY events for the servers.

+ *

{@link SPDYProxyEngine} implements a SPDY to SPDY proxy, that is, converts SPDY events received by clients into + * SPDY events for the servers.

*/ public class SPDYProxyEngine extends ProxyEngine implements StreamFrameListener { @@ -131,6 +132,12 @@ public class SPDYProxyEngine extends ProxyEngine implements StreamFrameListener } } + @Override + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) + { + throw new IllegalStateException("We shouldn't receive pushes from clients"); + } + @Override public void onReply(Stream stream, ReplyInfo replyInfo) { @@ -222,6 +229,61 @@ public class SPDYProxyEngine extends ProxyEngine implements StreamFrameListener this.clientStream = clientStream; } + @Override + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) + { + LOG.debug("S -> P pushed {} on {}", pushInfo, stream); + + Fields headers = new Fields(pushInfo.getHeaders(), false); + + addResponseProxyHeaders(stream, headers); + customizeResponseHeaders(stream, headers); + Stream clientStream = (Stream)stream.getAssociatedStream().getAttribute + (CLIENT_STREAM_ATTRIBUTE); + convert(stream.getSession().getVersion(), clientStream.getSession().getVersion(), + headers); + + StreamHandler handler = new StreamHandler(clientStream, pushInfo); + stream.setAttribute(STREAM_HANDLER_ATTRIBUTE, handler); + clientStream.push(new PushInfo(getTimeout(), TimeUnit.MILLISECONDS, headers, + pushInfo.isClose()), + handler); + return new Adapter() + { + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + // Push streams never send a reply + throw new UnsupportedOperationException(); + } + + @Override + public void onHeaders(Stream stream, HeadersInfo headersInfo) + { + throw new UnsupportedOperationException(); + } + + @Override + public void onData(Stream serverStream, final DataInfo serverDataInfo) + { + LOG.debug("S -> P pushed {} on {}", serverDataInfo, serverStream); + + ByteBufferDataInfo clientDataInfo = new ByteBufferDataInfo(serverDataInfo.asByteBuffer(false), serverDataInfo.isClose()) + { + @Override + public void consume(int delta) + { + super.consume(delta); + serverDataInfo.consume(delta); + } + }; + + StreamHandler handler = (StreamHandler)serverStream.getAttribute(STREAM_HANDLER_ATTRIBUTE); + handler.data(clientDataInfo); + } + }; + } + @Override public void onReply(final Stream stream, ReplyInfo replyInfo) { @@ -304,30 +366,30 @@ public class SPDYProxyEngine extends ProxyEngine implements StreamFrameListener } /** - *

{@link StreamHandler} implements the forwarding of DATA frames from the client to the server.

- *

Instances of this class buffer DATA frames sent by clients and send them to the server. - * The buffering happens between the send of the SYN_STREAM to the server (where DATA frames may arrive - * from the client before the SYN_STREAM has been fully sent), and between DATA frames, if the client - * is a fast producer and the server a slow consumer, or if the client is a SPDY v2 client (and hence - * without flow control) while the server is a SPDY v3 server (and hence with flow control).

+ *

{@link StreamHandler} implements the forwarding of DATA frames from the client to the server.

Instances + * of this class buffer DATA frames sent by clients and send them to the server. The buffering happens between the + * send of the SYN_STREAM to the server (where DATA frames may arrive from the client before the SYN_STREAM has been + * fully sent), and between DATA frames, if the client is a fast producer and the server a slow consumer, or if the + * client is a SPDY v2 client (and hence without flow control) while the server is a SPDY v3 server (and hence with + * flow control).

*/ private class StreamHandler implements Promise { private final Queue queue = new LinkedList<>(); private final Stream clientStream; - private final SynInfo serverSynInfo; + private final Info info; private Stream serverStream; - private StreamHandler(Stream clientStream, SynInfo serverSynInfo) + private StreamHandler(Stream clientStream, Info info) { this.clientStream = clientStream; - this.serverSynInfo = serverSynInfo; + this.info = info; } @Override public void succeeded(Stream serverStream) { - LOG.debug("P -> S {} from {} to {}", serverSynInfo, clientStream, serverStream); + LOG.debug("P -> S {} from {} to {}", info, clientStream, serverStream); serverStream.setAttribute(CLIENT_STREAM_ATTRIBUTE, clientStream); @@ -449,26 +511,8 @@ public class SPDYProxyEngine extends ProxyEngine implements StreamFrameListener } } - private class ProxySessionFrameListener extends SessionFrameListener.Adapter implements StreamFrameListener + private class ProxySessionFrameListener extends SessionFrameListener.Adapter { - @Override - public StreamFrameListener onSyn(Stream serverStream, SynInfo serverSynInfo) - { - LOG.debug("S -> P pushed {} on {}", serverSynInfo, serverStream); - - Fields headers = new Fields(serverSynInfo.getHeaders(), false); - - addResponseProxyHeaders(serverStream, headers); - customizeResponseHeaders(serverStream, headers); - Stream clientStream = (Stream)serverStream.getAssociatedStream().getAttribute(CLIENT_STREAM_ATTRIBUTE); - convert(serverStream.getSession().getVersion(), clientStream.getSession().getVersion(), headers); - - StreamHandler handler = new StreamHandler(clientStream, serverSynInfo); - serverStream.setAttribute(STREAM_HANDLER_ATTRIBUTE, handler); - clientStream.push(new PushInfo(getTimeout(), TimeUnit.MILLISECONDS, headers, serverSynInfo.isClose()), - handler); - return this; - } @Override public void onRst(Session serverSession, RstInfo serverRstInfo) @@ -487,41 +531,9 @@ public class SPDYProxyEngine extends ProxyEngine implements StreamFrameListener } @Override - public void onGoAway(Session serverSession, GoAwayReceivedInfo goAwayReceivedInfo) + public void onGoAway(Session serverSession, GoAwayResultInfo goAwayResultInfo) { serverSessions.values().remove(serverSession); } - - @Override - public void onReply(Stream stream, ReplyInfo replyInfo) - { - // Push streams never send a reply - throw new UnsupportedOperationException(); - } - - @Override - public void onHeaders(Stream stream, HeadersInfo headersInfo) - { - throw new UnsupportedOperationException(); - } - - @Override - public void onData(Stream serverStream, final DataInfo serverDataInfo) - { - LOG.debug("S -> P pushed {} on {}", serverDataInfo, serverStream); - - ByteBufferDataInfo clientDataInfo = new ByteBufferDataInfo(serverDataInfo.asByteBuffer(false), serverDataInfo.isClose()) - { - @Override - public void consume(int delta) - { - super.consume(delta); - serverDataInfo.consume(delta); - } - }; - - StreamHandler handler = (StreamHandler)serverStream.getAttribute(STREAM_HANDLER_ATTRIBUTE); - handler.data(clientDataInfo); - } } } diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategyTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategyTest.java index d4c5e2a0395..7d65a6f5c17 100644 --- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategyTest.java +++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategyTest.java @@ -18,16 +18,12 @@ package org.eclipse.jetty.spdy.server.http; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - import java.io.IOException; import java.io.PrintWriter; import java.net.InetSocketAddress; import java.util.Arrays; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; - import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -37,6 +33,7 @@ import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.spdy.api.DataInfo; +import org.eclipse.jetty.spdy.api.PushInfo; import org.eclipse.jetty.spdy.api.ReplyInfo; import org.eclipse.jetty.spdy.api.RstInfo; import org.eclipse.jetty.spdy.api.SPDY; @@ -50,10 +47,15 @@ import org.eclipse.jetty.spdy.server.NPNServerConnectionFactory; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + public class ReferrerPushStrategyTest extends AbstractHTTPSPDYTest { private final int referrerPushPeriod = 1000; @@ -107,39 +109,14 @@ public class ReferrerPushStrategyTest extends AbstractHTTPSPDYTest sendMainRequestAndCSSRequest(); final CountDownLatch pushDataLatch = new CountDownLatch(1); final CountDownLatch pushSynHeadersValid = new CountDownLatch(1); - Session session = startClient(version, serverAddress, new SessionFrameListener.Adapter() - { - @Override - public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) - { - validateHeaders(synInfo.getHeaders(), pushSynHeadersValid); - - assertThat("Stream is unidirectional", stream.isUnidirectional(), is(true)); - assertThat("URI header ends with css", synInfo.getHeaders().get(HTTPSPDYHeader.URI.name(version)) - .value().endsWith - ("" + - ".css"), - is(true)); - stream.getSession().rst(new RstInfo(stream.getId(), StreamStatus.REFUSED_STREAM), new Callback.Adapter()); - return new StreamFrameListener.Adapter() - { - - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - pushDataLatch.countDown(); - } - }; - } - }); + Session session = startClient(version, serverAddress, null); // Send main request. That should initiate the push push's which get reset by the client - sendRequest(session, mainRequestHeaders); + sendRequest(session, mainRequestHeaders, pushSynHeadersValid, pushDataLatch); assertThat("No push data is received", pushDataLatch.await(1, TimeUnit.SECONDS), is(false)); assertThat("Push push headers valid", pushSynHeadersValid.await(5, TimeUnit.SECONDS), is(true)); - sendRequest(session, associatedCSSRequestHeaders); + sendRequest(session, associatedCSSRequestHeaders, pushSynHeadersValid, pushDataLatch); } @Test @@ -157,7 +134,7 @@ public class ReferrerPushStrategyTest extends AbstractHTTPSPDYTest // Sleep for pushPeriod This should prevent application.js from being mapped as pushResource Thread.sleep(referrerPushPeriod + 1); - sendRequest(session, associatedJSRequestHeaders); + sendRequest(session, associatedJSRequestHeaders, null, null); run2ndClientRequests(false, true); } @@ -171,7 +148,7 @@ public class ReferrerPushStrategyTest extends AbstractHTTPSPDYTest Session session = sendMainRequestAndCSSRequest(); - sendRequest(session, associatedJSRequestHeaders); + sendRequest(session, associatedJSRequestHeaders, null, null); run2ndClientRequests(false, true); } @@ -200,18 +177,43 @@ public class ReferrerPushStrategyTest extends AbstractHTTPSPDYTest { Session session = startClient(version, serverAddress, null); - sendRequest(session, mainRequestHeaders); - sendRequest(session, associatedCSSRequestHeaders); + sendRequest(session, mainRequestHeaders, null, null); + sendRequest(session, associatedCSSRequestHeaders, null, null); return session; } - private void sendRequest(Session session, Fields requestHeaders) throws InterruptedException + private void sendRequest(Session session, Fields requestHeaders, final CountDownLatch pushSynHeadersValid, + final CountDownLatch pushDataLatch) throws InterruptedException { final CountDownLatch dataReceivedLatch = new CountDownLatch(1); final CountDownLatch received200OKLatch = new CountDownLatch(1); session.syn(new SynInfo(requestHeaders, true), new StreamFrameListener.Adapter() { + @Override + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) + { + validateHeaders(pushInfo.getHeaders(), pushSynHeadersValid); + + assertThat("Stream is unidirectional", stream.isUnidirectional(), is(true)); + assertThat("URI header ends with css", pushInfo.getHeaders().get(HTTPSPDYHeader.URI.name(version)) + .value().endsWith + ("" + + ".css"), + is(true)); + stream.getSession().rst(new RstInfo(stream.getId(), StreamStatus.REFUSED_STREAM), new Callback.Adapter()); + return new StreamFrameListener.Adapter() + { + + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + pushDataLatch.countDown(); + } + }; + } + @Override public void onReply(Stream stream, ReplyInfo replyInfo) { @@ -238,16 +240,17 @@ public class ReferrerPushStrategyTest extends AbstractHTTPSPDYTest final CountDownLatch mainStreamLatch = new CountDownLatch(2); final CountDownLatch pushDataLatch = new CountDownLatch(1); final CountDownLatch pushSynHeadersValid = new CountDownLatch(1); - Session session2 = startClient(version, serverAddress, new SessionFrameListener.Adapter() + Session session2 = startClient(version, serverAddress, null); + session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() { @Override - public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) { if (validateHeaders) - validateHeaders(synInfo.getHeaders(), pushSynHeadersValid); + validateHeaders(pushInfo.getHeaders(), pushSynHeadersValid); assertThat("Stream is unidirectional", stream.isUnidirectional(), is(true)); - assertThat("URI header ends with css", synInfo.getHeaders().get(HTTPSPDYHeader.URI.name(version)) + assertThat("URI header ends with css", pushInfo.getHeaders().get(HTTPSPDYHeader.URI.name(version)) .value().endsWith ("" + ".css"), @@ -264,9 +267,7 @@ public class ReferrerPushStrategyTest extends AbstractHTTPSPDYTest } }; } - }); - session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() - { + @Override public void onReply(Stream stream, ReplyInfo replyInfo) { @@ -292,6 +293,8 @@ public class ReferrerPushStrategyTest extends AbstractHTTPSPDYTest assertThat("Push push headers valid", pushSynHeadersValid.await(5, TimeUnit.SECONDS), is(true)); } + private static final Logger LOG = Log.getLogger(ReferrerPushStrategyTest.class); + @Test public void testAssociatedResourceIsPushed() throws Exception { @@ -326,16 +329,17 @@ public class ReferrerPushStrategyTest extends AbstractHTTPSPDYTest }); Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); - sendRequest(session1, createHeaders(cssResource)); + sendRequest(session1, createHeaders(cssResource), null, null); // Create another client, and perform the same request for the main resource, we expect the css being pushed final CountDownLatch mainStreamLatch = new CountDownLatch(2); final CountDownLatch pushDataLatch = new CountDownLatch(1); - Session session2 = startClient(version, address, new SessionFrameListener.Adapter() + Session session2 = startClient(version, address, null); + session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() { @Override - public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) { Assert.assertTrue(stream.isUnidirectional()); return new StreamFrameListener.Adapter() @@ -349,9 +353,7 @@ public class ReferrerPushStrategyTest extends AbstractHTTPSPDYTest } }; } - }); - session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() - { + @Override public void onReply(Stream stream, ReplyInfo replyInfo) { @@ -452,13 +454,15 @@ public class ReferrerPushStrategyTest extends AbstractHTTPSPDYTest final CountDownLatch mainStreamLatch = new CountDownLatch(2); final CountDownLatch pushDataLatch = new CountDownLatch(1); - Session session2 = startClient(version, address, new SessionFrameListener.Adapter() + Session session2 = startClient(version, address, null); + session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() { @Override - public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) { Assert.assertTrue(stream.isUnidirectional()); - Assert.assertTrue(synInfo.getHeaders().get(HTTPSPDYHeader.URI.name(version)).value().endsWith(".css")); + Assert.assertTrue(pushInfo.getHeaders().get(HTTPSPDYHeader.URI.name(version)).value().endsWith("" + + ".css")); return new StreamFrameListener.Adapter() { @Override @@ -470,9 +474,7 @@ public class ReferrerPushStrategyTest extends AbstractHTTPSPDYTest } }; } - }); - session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() - { + @Override public void onReply(Stream stream, ReplyInfo replyInfo) { @@ -563,14 +565,31 @@ public class ReferrerPushStrategyTest extends AbstractHTTPSPDYTest final CountDownLatch mainStreamLatch = new CountDownLatch(2); final CountDownLatch pushDataLatch = new CountDownLatch(2); - Session session2 = startClient(version, address, new SessionFrameListener.Adapter() + Session session2 = startClient(version, address, null); + LOG.warn("REQUEST FOR PUSHED RESOURCES"); + session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() { @Override - public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) { Assert.assertTrue(stream.isUnidirectional()); return new StreamFrameListener.Adapter() { + @Override + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) + { + return new Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + pushDataLatch.countDown(); + } + }; + } + @Override public void onData(Stream stream, DataInfo dataInfo) { @@ -580,9 +599,7 @@ public class ReferrerPushStrategyTest extends AbstractHTTPSPDYTest } }; } - }); - session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() - { + @Override public void onReply(Stream stream, ReplyInfo replyInfo) { diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPToSPDYTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPToSPDYTest.java index e65cebb4051..11f44dc2a32 100644 --- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPToSPDYTest.java +++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPToSPDYTest.java @@ -33,7 +33,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.spdy.api.BytesDataInfo; import org.eclipse.jetty.spdy.api.DataInfo; -import org.eclipse.jetty.spdy.api.GoAwayReceivedInfo; +import org.eclipse.jetty.spdy.api.GoAwayResultInfo; import org.eclipse.jetty.spdy.api.PushInfo; import org.eclipse.jetty.spdy.api.ReplyInfo; import org.eclipse.jetty.spdy.api.RstInfo; @@ -164,7 +164,7 @@ public class ProxyHTTPToSPDYTest } @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayInfo) { closeLatch.countDown(); } diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToHTTPTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToHTTPTest.java index f3c2e9d3a28..6a6c2ef6289 100644 --- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToHTTPTest.java +++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToHTTPTest.java @@ -37,7 +37,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.spdy.api.DataInfo; -import org.eclipse.jetty.spdy.api.GoAwayReceivedInfo; +import org.eclipse.jetty.spdy.api.GoAwayResultInfo; import org.eclipse.jetty.spdy.api.PingInfo; import org.eclipse.jetty.spdy.api.PingResultInfo; import org.eclipse.jetty.spdy.api.ReplyInfo; @@ -429,7 +429,7 @@ public class ProxySPDYToHTTPTest Session client = factory.newSPDYClient(version).connect(proxyAddress, new SessionFrameListener.Adapter() { @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayReceivedInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayReceivedInfo) { goAwayLatch.countDown(); } diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToSPDYTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToSPDYTest.java index 211a2c4a790..506c5ed571d 100644 --- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToSPDYTest.java +++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToSPDYTest.java @@ -281,10 +281,16 @@ public class ProxySPDYToSPDYTest final CountDownLatch pushSynLatch = new CountDownLatch(1); final CountDownLatch pushDataLatch = new CountDownLatch(1); - Session client = factory.newSPDYClient(version).connect(proxyAddress, new SessionFrameListener.Adapter() + Session client = factory.newSPDYClient(version).connect(proxyAddress, null).get(5, TimeUnit.SECONDS); + + Fields headers = new Fields(); + headers.put(HTTPSPDYHeader.HOST.name(version), "localhost:" + proxyAddress.getPort()); + final CountDownLatch replyLatch = new CountDownLatch(1); + final CountDownLatch dataLatch = new CountDownLatch(1); + client.syn(new SynInfo(headers, true), new StreamFrameListener.Adapter() { @Override - public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) { pushSynLatch.countDown(); return new StreamFrameListener.Adapter() @@ -298,14 +304,7 @@ public class ProxySPDYToSPDYTest } }; } - }).get(5, TimeUnit.SECONDS); - Fields headers = new Fields(); - headers.put(HTTPSPDYHeader.HOST.name(version), "localhost:" + proxyAddress.getPort()); - final CountDownLatch replyLatch = new CountDownLatch(1); - final CountDownLatch dataLatch = new CountDownLatch(1); - client.syn(new SynInfo(headers, true), new StreamFrameListener.Adapter() - { @Override public void onReply(Stream stream, ReplyInfo replyInfo) { diff --git a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/ClosedStreamTest.java b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/ClosedStreamTest.java index 27f06da7c43..ca14e68ae4c 100644 --- a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/ClosedStreamTest.java +++ b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/ClosedStreamTest.java @@ -35,7 +35,7 @@ import org.eclipse.jetty.spdy.StandardCompressionFactory; import org.eclipse.jetty.spdy.api.BytesDataInfo; import org.eclipse.jetty.spdy.api.DataInfo; import org.eclipse.jetty.spdy.api.GoAwayInfo; -import org.eclipse.jetty.spdy.api.GoAwayReceivedInfo; +import org.eclipse.jetty.spdy.api.GoAwayResultInfo; import org.eclipse.jetty.spdy.api.ReplyInfo; import org.eclipse.jetty.spdy.api.SPDY; import org.eclipse.jetty.spdy.api.Session; @@ -211,7 +211,7 @@ public class ClosedStreamTest extends AbstractTest }; } @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayInfo) { goAwayReceivedLatch.countDown(); } diff --git a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/GoAwayTest.java b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/GoAwayTest.java index 509fb130a63..9a4b3f35d9b 100644 --- a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/GoAwayTest.java +++ b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/GoAwayTest.java @@ -27,7 +27,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.io.EofException; import org.eclipse.jetty.spdy.api.DataInfo; import org.eclipse.jetty.spdy.api.GoAwayInfo; -import org.eclipse.jetty.spdy.api.GoAwayReceivedInfo; +import org.eclipse.jetty.spdy.api.GoAwayResultInfo; import org.eclipse.jetty.spdy.api.ReplyInfo; import org.eclipse.jetty.spdy.api.Session; import org.eclipse.jetty.spdy.api.SessionFrameListener; @@ -61,7 +61,7 @@ public class GoAwayTest extends AbstractTest } @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayInfo) { Assert.assertEquals(0, goAwayInfo.getLastStreamId()); Assert.assertSame(SessionStatus.OK, goAwayInfo.getSessionStatus()); @@ -90,12 +90,12 @@ public class GoAwayTest extends AbstractTest return null; } }; - final AtomicReference ref = new AtomicReference<>(); + final AtomicReference ref = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(1); SessionFrameListener clientSessionFrameListener = new SessionFrameListener.Adapter() { @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayInfo) { ref.set(goAwayInfo); latch.countDown(); @@ -106,10 +106,10 @@ public class GoAwayTest extends AbstractTest Stream stream1 = session.syn(new SynInfo(5, TimeUnit.SECONDS, new Fields(), true, (byte)0), null); Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); - GoAwayReceivedInfo goAwayReceivedInfo = ref.get(); - Assert.assertNotNull(goAwayReceivedInfo); - Assert.assertEquals(stream1.getId(), goAwayReceivedInfo.getLastStreamId()); - Assert.assertSame(SessionStatus.OK, goAwayReceivedInfo.getSessionStatus()); + GoAwayResultInfo goAwayResultInfo = ref.get(); + Assert.assertNotNull(goAwayResultInfo); + Assert.assertEquals(stream1.getId(), goAwayResultInfo.getLastStreamId()); + Assert.assertSame(SessionStatus.OK, goAwayResultInfo.getSessionStatus()); } @Test @@ -139,7 +139,7 @@ public class GoAwayTest extends AbstractTest SessionFrameListener clientSessionFrameListener = new SessionFrameListener.Adapter() { @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayInfo) { session.syn(new SynInfo(new Fields(), true), null, new FuturePromise()); } @@ -184,12 +184,12 @@ public class GoAwayTest extends AbstractTest } } }; - final AtomicReference goAwayRef = new AtomicReference<>(); + final AtomicReference goAwayRef = new AtomicReference<>(); final CountDownLatch goAwayLatch = new CountDownLatch(1); SessionFrameListener clientSessionFrameListener = new SessionFrameListener.Adapter() { @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayInfo) { goAwayRef.set(goAwayInfo); goAwayLatch.countDown(); @@ -228,7 +228,7 @@ public class GoAwayTest extends AbstractTest // The last good stream is the second, because it was received by the server Assert.assertTrue(goAwayLatch.await(5, TimeUnit.SECONDS)); - GoAwayReceivedInfo goAway = goAwayRef.get(); + GoAwayResultInfo goAway = goAwayRef.get(); Assert.assertNotNull(goAway); Assert.assertEquals(stream2.getId(), goAway.getLastStreamId()); } diff --git a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/IdleTimeoutTest.java b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/IdleTimeoutTest.java index 3005b2cd99c..708e6dc9be9 100644 --- a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/IdleTimeoutTest.java +++ b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/IdleTimeoutTest.java @@ -23,7 +23,7 @@ import java.net.InetSocketAddress; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import org.eclipse.jetty.spdy.api.GoAwayReceivedInfo; +import org.eclipse.jetty.spdy.api.GoAwayResultInfo; import org.eclipse.jetty.spdy.api.ReplyInfo; import org.eclipse.jetty.spdy.api.SPDY; import org.eclipse.jetty.spdy.api.Session; @@ -63,7 +63,7 @@ public class IdleTimeoutTest extends AbstractTest Session session = startClient(startServer(null), new SessionFrameListener.Adapter() { @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayInfo) { latch.countDown(); } @@ -85,7 +85,7 @@ public class IdleTimeoutTest extends AbstractTest Session session = startClient(startServer(null), new SessionFrameListener.Adapter() { @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayInfo) { latch.countDown(); } @@ -125,7 +125,7 @@ public class IdleTimeoutTest extends AbstractTest Session session = startClient(startServer(null), new SessionFrameListener.Adapter() { @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayInfo) { goAwayLatch.countDown(); } @@ -161,7 +161,7 @@ public class IdleTimeoutTest extends AbstractTest } @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayInfo) { latch.countDown(); } @@ -187,7 +187,7 @@ public class IdleTimeoutTest extends AbstractTest InetSocketAddress address = startServer(new ServerSessionFrameListener.Adapter() { @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayInfo) { latch.countDown(); } @@ -220,7 +220,7 @@ public class IdleTimeoutTest extends AbstractTest } @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayInfo) { latch.countDown(); } diff --git a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/PushStreamTest.java b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/PushStreamTest.java index 0a38ae72fb7..344667e8c95 100644 --- a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/PushStreamTest.java +++ b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/PushStreamTest.java @@ -19,12 +19,6 @@ package org.eclipse.jetty.spdy.server; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.sameInstance; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; - import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; @@ -44,7 +38,7 @@ import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.spdy.StandardCompressionFactory; import org.eclipse.jetty.spdy.api.BytesDataInfo; import org.eclipse.jetty.spdy.api.DataInfo; -import org.eclipse.jetty.spdy.api.GoAwayReceivedInfo; +import org.eclipse.jetty.spdy.api.GoAwayResultInfo; import org.eclipse.jetty.spdy.api.PushInfo; import org.eclipse.jetty.spdy.api.ReplyInfo; import org.eclipse.jetty.spdy.api.RstInfo; @@ -75,6 +69,12 @@ import org.eclipse.jetty.util.log.Logger; import org.junit.Assert; import org.junit.Test; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + public class PushStreamTest extends AbstractTest { private static final Logger LOG = Log.getLogger(PushStreamTest.class); @@ -94,10 +94,12 @@ public class PushStreamTest extends AbstractTest stream.push(new PushInfo(new Fields(), true), new Promise.Adapter()); return null; } - }), new SessionFrameListener.Adapter() + }), null); + + Stream stream = clientSession.syn(new SynInfo(new Fields(), true), new StreamFrameListener.Adapter() { @Override - public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) { assertThat("streamId is even", stream.getId() % 2, is(0)); assertThat("stream is unidirectional", stream.isUnidirectional(), is(true)); @@ -117,8 +119,6 @@ public class PushStreamTest extends AbstractTest return null; } }); - - Stream stream = clientSession.syn(new SynInfo(new Fields(), true), null); assertThat("onSyn has been called", pushStreamLatch.await(5, TimeUnit.SECONDS), is(true)); Stream pushStream = pushStreamRef.get(); assertThat("main stream and associated stream are the same", stream, sameInstance(pushStream.getAssociatedStream())); @@ -177,10 +177,12 @@ public class PushStreamTest extends AbstractTest } } - }), new SessionFrameListener.Adapter() + }), null); + + Stream stream = clientSession.syn(new SynInfo(new Fields(), false), new StreamFrameListener.Adapter() { @Override - public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) { pushStreamSynLatch.countDown(); return new StreamFrameListener.Adapter() @@ -193,10 +195,7 @@ public class PushStreamTest extends AbstractTest } }; } - }); - Stream stream = clientSession.syn(new SynInfo(new Fields(), false), new StreamFrameListener.Adapter() - { @Override public void onReply(Stream stream, ReplyInfo replyInfo) { @@ -298,10 +297,12 @@ public class PushStreamTest extends AbstractTest throw new IllegalStateException(e); } } - }), new SessionFrameListener.Adapter() + }), null); + + Stream stream = clientSession.syn(new SynInfo(new Fields(), true), new StreamFrameListener.Adapter() { @Override - public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) { return new StreamFrameListener.Adapter() { @@ -327,10 +328,7 @@ public class PushStreamTest extends AbstractTest } }; } - }); - Stream stream = clientSession.syn(new SynInfo(new Fields(), true), new StreamFrameListener.Adapter() - { @Override public void onReply(Stream stream, ReplyInfo replyInfo) { @@ -427,7 +425,7 @@ public class PushStreamTest extends AbstractTest } @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayInfo) { goAwayReceivedLatch.countDown(); } @@ -543,20 +541,14 @@ public class PushStreamTest extends AbstractTest stream.push(new PushInfo(new Fields(), false), new Promise.Adapter()); return null; } - }), new SessionFrameListener.Adapter() - { - @Override - public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) - { - assertStreamIdIsEven(stream); - pushStreamIdIsEvenLatch.countDown(); - return super.onSyn(stream, synInfo); - } - }); + }), null); - Stream stream = clientSession.syn(new SynInfo(new Fields(), false), null); - Stream stream2 = clientSession.syn(new SynInfo(new Fields(), false), null); - Stream stream3 = clientSession.syn(new SynInfo(new Fields(), false), null); + Stream stream = clientSession.syn(new SynInfo(new Fields(), false), + new VerifyPushStreamIdIsEvenStreamFrameListener(pushStreamIdIsEvenLatch)); + Stream stream2 = clientSession.syn(new SynInfo(new Fields(), false), + new VerifyPushStreamIdIsEvenStreamFrameListener(pushStreamIdIsEvenLatch)); + Stream stream3 = clientSession.syn(new SynInfo(new Fields(), false), + new VerifyPushStreamIdIsEvenStreamFrameListener(pushStreamIdIsEvenLatch)); assertStreamIdIsOdd(stream); assertStreamIdIsOdd(stream2); assertStreamIdIsOdd(stream3); @@ -564,6 +556,24 @@ public class PushStreamTest extends AbstractTest assertThat("all pushStreams had even ids", pushStreamIdIsEvenLatch.await(5, TimeUnit.SECONDS), is(true)); } + private class VerifyPushStreamIdIsEvenStreamFrameListener extends StreamFrameListener.Adapter + { + final CountDownLatch pushStreamIdIsEvenLatch; + + private VerifyPushStreamIdIsEvenStreamFrameListener(CountDownLatch pushStreamIdIsEvenLatch) + { + this.pushStreamIdIsEvenLatch = pushStreamIdIsEvenLatch; + } + + @Override + public StreamFrameListener onPush(Stream stream, PushInfo pushInfo) + { + assertStreamIdIsEven(stream); + pushStreamIdIsEvenLatch.countDown(); + return super.onPush(stream, pushInfo); + } + } + private void assertStreamIdIsEven(Stream stream) { assertThat("streamId is odd", stream.getId() % 2, is(0)); diff --git a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/SPDYClientFactoryTest.java b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/SPDYClientFactoryTest.java index 076c7ce7945..fc40469dbea 100644 --- a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/SPDYClientFactoryTest.java +++ b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/SPDYClientFactoryTest.java @@ -23,7 +23,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.spdy.api.GoAwayInfo; -import org.eclipse.jetty.spdy.api.GoAwayReceivedInfo; +import org.eclipse.jetty.spdy.api.GoAwayResultInfo; import org.eclipse.jetty.spdy.api.Session; import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener; import org.junit.Assert; @@ -38,7 +38,7 @@ public class SPDYClientFactoryTest extends AbstractTest startClient(startServer(new ServerSessionFrameListener.Adapter() { @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayReceivedInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayResultInfo) { latch.countDown(); } diff --git a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/SPDYServerConnectorTest.java b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/SPDYServerConnectorTest.java index 02bc42a81a6..0dc771a150a 100644 --- a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/SPDYServerConnectorTest.java +++ b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/SPDYServerConnectorTest.java @@ -23,7 +23,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.spdy.api.GoAwayInfo; -import org.eclipse.jetty.spdy.api.GoAwayReceivedInfo; +import org.eclipse.jetty.spdy.api.GoAwayResultInfo; import org.eclipse.jetty.spdy.api.Session; import org.eclipse.jetty.spdy.api.SessionFrameListener; import org.junit.Assert; @@ -38,7 +38,7 @@ public class SPDYServerConnectorTest extends AbstractTest startClient(startServer(null), new SessionFrameListener.Adapter() { @Override - public void onGoAway(Session session, GoAwayReceivedInfo goAwayReceivedInfo) + public void onGoAway(Session session, GoAwayResultInfo goAwayResultInfo) { latch.countDown(); } diff --git a/jetty-start/src/test/resources/jetty.home/start.ini b/jetty-start/src/test/resources/jetty.home/start.ini index 65c0816fbbd..eda5499d5cb 100644 --- a/jetty-start/src/test/resources/jetty.home/start.ini +++ b/jetty-start/src/test/resources/jetty.home/start.ini @@ -45,6 +45,16 @@ # These control what classes are on the classpath # for a full listing do # java -jar start.jar --list-options +# +# Enable classpath OPTIONS. Each options represents one or more jars +# to be added to the classpath. The options can be listed with --help +# or --list-options. +# By convention, options starting with a capital letter (eg Server) +# are aggregations of other available options. +# Directories in $JETTY_HOME/lib can be added as dynamic OPTIONS by +# convention. E.g. put some logging jars in $JETTY_HOME/lib/logging +# and make them available in the classpath by adding a "logging" OPTION +# like so: OPTIONS=Server,jsp,logging #----------------------------------------------------------- OPTIONS=Server,jsp,resources,websocket,ext #----------------------------------------------------------- diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/SocketAddressResolver.java b/jetty-util/src/main/java/org/eclipse/jetty/util/SocketAddressResolver.java new file mode 100644 index 00000000000..6f94ded0669 --- /dev/null +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/SocketAddressResolver.java @@ -0,0 +1,175 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 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.util; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.channels.UnresolvedAddressException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.thread.Scheduler; + +/** + * Creates asynchronously {@link SocketAddress} instances, returning them through a {@link Promise}, + * in order to avoid blocking on DNS lookup. + *

+ * {@link InetSocketAddress#InetSocketAddress(String, int)} attempts to perform a DNS resolution of + * the host name, and this may block for several seconds. + * This class creates the {@link InetSocketAddress} in a separate thread and provides the result + * through a {@link Promise}, with the possibility to specify a timeout for the operation. + *

+ * Example usage: + *

+ * SocketAddressResolver resolver = new SocketAddressResolver(executor, scheduler);
+ * resolver.resolve("www.google.com", 80, new Promise<SocketAddress>()
+ * {
+ *     public void succeeded(SocketAddress result)
+ *     {
+ *         // The address was resolved
+ *     }
+ *
+ *     public void failed(Throwable failure)
+ *     {
+ *         // The address resolution failed
+ *     }
+ * });
+ * 
+ */ +public class SocketAddressResolver +{ + private static final Logger LOG = Log.getLogger(SocketAddressResolver.class); + + private final Executor executor; + private final Scheduler scheduler; + private final long timeout; + + /** + * Creates a new instance with the given executor (to perform DNS resolution in a separate thread), + * the given scheduler (to cancel the operation if it takes too long) and the given timeout, in milliseconds. + * + * @param executor the thread pool to use to perform DNS resolution in pooled threads + * @param scheduler the scheduler to schedule tasks to cancel DNS resolution if it takes too long + * @param timeout the timeout, in milliseconds, for the DNS resolution to complete + */ + public SocketAddressResolver(Executor executor, Scheduler scheduler, long timeout) + { + this.executor = executor; + this.scheduler = scheduler; + this.timeout = timeout; + } + + public Executor getExecutor() + { + return executor; + } + + public Scheduler getScheduler() + { + return scheduler; + } + + public long getTimeout() + { + return timeout; + } + + /** + * Resolves the given host and port, returning a {@link SocketAddress} through the given {@link Promise} + * with the default timeout. + * + * @param host the host to resolve + * @param port the port of the resulting socket address + * @param promise the callback invoked when the resolution succeeds or fails + * @see #resolve(String, int, long, Promise) + */ + public void resolve(String host, int port, Promise promise) + { + resolve(host, port, timeout, promise); + } + + /** + * Resolves the given host and port, returning a {@link SocketAddress} through the given {@link Promise} + * with the given timeout. + * + * @param host the host to resolve + * @param port the port of the resulting socket address + * @param timeout the timeout, in milliseconds, for the DNS resolution to complete + * @param promise the callback invoked when the resolution succeeds or fails + */ + protected void resolve(final String host, final int port, final long timeout, final Promise promise) + { + executor.execute(new Runnable() + { + @Override + public void run() + { + Scheduler.Task task = null; + final AtomicBoolean complete = new AtomicBoolean(); + if (timeout > 0) + { + final Thread thread = Thread.currentThread(); + task = scheduler.schedule(new Runnable() + { + @Override + public void run() + { + if (complete.compareAndSet(false, true)) + { + promise.failed(new TimeoutException()); + thread.interrupt(); + } + } + }, timeout, TimeUnit.MILLISECONDS); + } + + try + { + long start = System.nanoTime(); + InetSocketAddress result = new InetSocketAddress(host, port); + long elapsed = System.nanoTime() - start; + LOG.debug("Resolved {} in {} ms", host, TimeUnit.NANOSECONDS.toMillis(elapsed)); + if (complete.compareAndSet(false, true)) + { + if (result.isUnresolved()) + promise.failed(new UnresolvedAddressException()); + else + promise.succeeded(result); + } + } + catch (Throwable x) + { + if (complete.compareAndSet(false, true)) + promise.failed(x); + } + finally + { + if (task != null) + task.cancel(); + // Reset the interrupted status before releasing the thread to the pool + Thread.interrupted(); + } + } + }); + } +} diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java index 075521dc12a..6ee30147506 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java @@ -65,6 +65,19 @@ class JarFileResource extends JarResource _list=null; _entry=null; _file=null; + + if ( _jarFile != null ) + { + try + { + _jarFile.close(); + } + catch ( IOException ioe ) + { + LOG.ignore(ioe); + } + } + _jarFile=null; super.release(); } @@ -303,12 +316,11 @@ class JarFileResource extends JarResource throw new IllegalStateException(); } - Enumeration e=jarFile.entries(); + Enumeration e=jarFile.entries(); String dir=_urlString.substring(_urlString.indexOf("!/")+2); while(e.hasMoreElements()) { - - JarEntry entry = (JarEntry) e.nextElement(); + JarEntry entry = e.nextElement(); String name=entry.getName().replace('\\','/'); if(!name.startsWith(dir) || name.length()==dir.length()) { diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/TimerScheduler.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/TimerScheduler.java index 4f20739562d..eef62659cf2 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/TimerScheduler.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/TimerScheduler.java @@ -27,6 +27,10 @@ import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; + +/* ------------------------------------------------------------ */ +/** A scheduler based on the the JVM Timer class + */ public class TimerScheduler extends AbstractLifeCycle implements Scheduler, Runnable { private static final Logger LOG = Log.getLogger(TimerScheduler.class); diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java index 9dcb45a2d40..c77d735d748 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java @@ -32,8 +32,13 @@ import java.net.URL; import java.util.Arrays; import java.util.HashSet; import java.util.Set; + +import java.util.TimeZone; +import java.util.jar.JarFile; import java.util.zip.ZipFile; +import junit.framework.Assert; + import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.OS; import org.eclipse.jetty.util.IO; @@ -121,11 +126,15 @@ public class ResourceTest file=new File(file.getCanonicalPath()); URI uri = file.toURI(); __userURL=uri.toURL(); - - __userURL = new URL(__userURL.toString() + "src/test/resources/org/eclipse/jetty/util/resource/"); - FilePermission perm = (FilePermission) __userURL.openConnection().getPermission(); - __userDir = new File(perm.getName()).getCanonicalPath() + File.separatorChar; - __relDir = "src/test/resources/org/eclipse/jetty/util/resource/".replace('/', File.separatorChar); + + __userURL = MavenTestingUtils.getTestResourcesDir().toURI().toURL(); + FilePermission perm = (FilePermission) __userURL.openConnection().getPermission(); + __userDir = new File(perm.getName()).getCanonicalPath() + File.separatorChar; + __relDir = "src/test/resources/".replace('/', File.separatorChar); + + //System.err.println("User Dir="+__userDir); + //System.err.println("Rel Dir="+__relDir); + //System.err.println("User URL="+__userURL); tmpFile=File.createTempFile("test",null).getCanonicalFile(); tmpFile.deleteOnExit(); @@ -319,14 +328,16 @@ public class ResourceTest throws Exception { String s = "jar:"+__userURL+"TestData/test.zip!/subdir/numbers"; - ZipFile zf = new ZipFile(MavenTestingUtils.getProjectFile("src/test/resources/org/eclipse/jetty/util/resource/TestData/test.zip")); + + ZipFile zf = new ZipFile(MavenTestingUtils.getTestResourceFile("TestData/test.zip")); + long last = zf.getEntry("subdir/numbers").getTime(); Resource r = Resource.newResource(s); assertEquals(last,r.lastModified()); } - + /* ------------------------------------------------------------ */ @Test public void testJarFileCopyToDirectoryTraversal () throws Exception diff --git a/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/TestData/alphabet.txt b/jetty-util/src/test/resources/TestData/alphabet.txt similarity index 100% rename from jetty-util/src/test/resources/org/eclipse/jetty/util/resource/TestData/alphabet.txt rename to jetty-util/src/test/resources/TestData/alphabet.txt diff --git a/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/TestData/alt.zip b/jetty-util/src/test/resources/TestData/alt.zip similarity index 100% rename from jetty-util/src/test/resources/org/eclipse/jetty/util/resource/TestData/alt.zip rename to jetty-util/src/test/resources/TestData/alt.zip diff --git a/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/TestData/extract.zip b/jetty-util/src/test/resources/TestData/extract.zip similarity index 100% rename from jetty-util/src/test/resources/org/eclipse/jetty/util/resource/TestData/extract.zip rename to jetty-util/src/test/resources/TestData/extract.zip diff --git a/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/TestData/test.zip b/jetty-util/src/test/resources/TestData/test.zip similarity index 100% rename from jetty-util/src/test/resources/org/eclipse/jetty/util/resource/TestData/test.zip rename to jetty-util/src/test/resources/TestData/test.zip diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Descriptor.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Descriptor.java index 0e9d216c296..038230b4e11 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Descriptor.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Descriptor.java @@ -60,10 +60,14 @@ public abstract class Descriptor if (_root == null) { - //boolean oldValidating = _processor.getParser().getValidating(); - //_processor.getParser().setValidating(_validating); - _root = _parser.parse(_xml.getURL().toString()); - //_processor.getParser().setValidating(oldValidating); + try + { + _root = _parser.parse(_xml.getInputStream()); + } + finally + { + _xml.release(); + } } } diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/jsp/JspAndDefaultWithAliasesTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/jsp/JspAndDefaultWithAliasesTest.java index 019a3c70030..82ffc331813 100644 --- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/jsp/JspAndDefaultWithAliasesTest.java +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/jsp/JspAndDefaultWithAliasesTest.java @@ -29,12 +29,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import org.apache.jasper.servlet.JspServlet; import org.eclipse.jetty.security.HashLoginService; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.nio.SelectChannelConnector; import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.servlet.NoJspServlet; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; @@ -44,7 +42,6 @@ import org.eclipse.jetty.util.log.Logger; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -65,16 +62,22 @@ public class JspAndDefaultWithAliasesTest public static Collection data() { List data = new ArrayList(); + + double javaVersion = Double.parseDouble(System.getProperty("java.specification.version")); // @formatter:off data.add(new String[] { "false","/dump.jsp" }); data.add(new String[] { "true", "/dump.jsp%00" }); - data.add(new String[] { "false","/dump.jsp%00x" }); data.add(new String[] { "false","/dump.jsp%00/" }); - data.add(new String[] { "false","/dump.jsp%00x/" }); data.add(new String[] { "false","/dump.jsp%00x/dump.jsp" }); data.add(new String[] { "false","/dump.jsp%00/dump.jsp" }); - data.add(new String[] { "false","/dump.jsp%00/index.html" }); + + if (javaVersion >= 1.7) + { + data.add(new String[] { "false","/dump.jsp%00x" }); + data.add(new String[] { "false","/dump.jsp%00x/" }); + data.add(new String[] { "false","/dump.jsp%00/index.html" }); + } // @formatter:on return data; diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/jsp/JspAndDefaultWithoutAliasesTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/jsp/JspAndDefaultWithoutAliasesTest.java index 3a82d88b4c5..ab9f48473fc 100644 --- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/jsp/JspAndDefaultWithoutAliasesTest.java +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/jsp/JspAndDefaultWithoutAliasesTest.java @@ -29,7 +29,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import org.apache.jasper.servlet.JspServlet; import org.eclipse.jetty.security.HashLoginService; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.nio.SelectChannelConnector; @@ -64,16 +63,22 @@ public class JspAndDefaultWithoutAliasesTest public static Collection data() { List data = new ArrayList(); + + double javaVersion = Double.parseDouble(System.getProperty("java.specification.version")); // @formatter:off data.add(new Object[] { "/dump.jsp" }); data.add(new Object[] { "/dump.jsp%00" }); data.add(new Object[] { "/dump.jsp%00x" }); - data.add(new Object[] { "/dump.jsp%00/" }); - data.add(new Object[] { "/dump.jsp%00x/" }); data.add(new Object[] { "/dump.jsp%00x/dump.jsp" }); data.add(new Object[] { "/dump.jsp%00/dump.jsp" }); data.add(new Object[] { "/dump.jsp%00/index.html" }); + + if (javaVersion >= 1.7) + { + data.add(new Object[] { "/dump.jsp%00/" }); + data.add(new Object[] { "/dump.jsp%00x/" }); + } // @formatter:on return data;